diff --git a/configure.ac b/configure.ac
index 475880ee9dfb5976ddbadb32b8c4817fe62ad583..7ebef1b4f3786dc05e2f4303e1e4f3181a860ad6 100644
--- a/configure.ac
+++ b/configure.ac
@@ -661,6 +661,7 @@ AC_CONFIG_FILES([Makefile \
                  test/Makefile\
                  test/sip/Makefile
                  test/unitTest/Makefile \
+                 test/agent/Makefile \
                  man/Makefile \
                  doc/Makefile \
                  doc/doxygen/Makefile])
diff --git a/test/Makefile.am b/test/Makefile.am
index c727e98ec1dea2f036108572fadbadbc2fe2ca52..1d65e7b9e3a5332e10f73e8e1a5ae680faeef084 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -1,5 +1,5 @@
-SUBDIRS = unitTest
+SUBDIRS = unitTest agent
 SUBDIRS += sip
 if ENABLE_FUZZING
 SUBDIRS += fuzzing
-endif
\ No newline at end of file
+endif
diff --git a/test/agent/.gitignore b/test/agent/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..93d93c78cb8eeaea6050f5082cf303ee6a27a1dc
--- /dev/null
+++ b/test/agent/.gitignore
@@ -0,0 +1,3 @@
+agent
+
+
diff --git a/test/agent/Makefile.am b/test/agent/Makefile.am
new file mode 100644
index 0000000000000000000000000000000000000000..731fc008abd6f33d22afd5ce9452d0d12b42e84c
--- /dev/null
+++ b/test/agent/Makefile.am
@@ -0,0 +1,10 @@
+include $(top_srcdir)/globals.mk
+
+AM_CXXFLAGS += -I$(top_srcdir)/src -I.. \
+                -DTOP_BUILDDIR=\"$$(cd "$(top_builddir)"; pwd)\"
+
+check_PROGRAMS = agent
+
+agent_SOURCES = agent.cpp agent.h utils.cpp utils.h bt.cpp bt.h main.cpp
+
+agent_LDADD = $(top_builddir)/src/libring.la -ldl
diff --git a/test/agent/agent.cpp b/test/agent/agent.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4f7aefc7717f25b7916cccc564ade170c73ff3db
--- /dev/null
+++ b/test/agent/agent.cpp
@@ -0,0 +1,501 @@
+/*
+ *  Copyright (C) 2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Olivier Dion <olivier.dion@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+/* std */
+#include <atomic>
+#include <chrono>
+#include <condition_variable>
+#include <fstream>
+#include <memory>
+#include <mutex>
+#include <thread>
+
+/* Third parties */
+#include <yaml-cpp/yaml.h>
+
+/* DRing */
+#include "account_const.h"
+#include "dring/presencemanager_interface.h"
+#include "dring/callmanager_interface.h"
+#include "dring/configurationmanager_interface.h"
+#include "dring/conversation_interface.h"
+
+/* agent */
+#include "agent/agent.h"
+#include "agent/bt.h"
+#include "agent/utils.h"
+
+using usize = size_t;
+using u32 = uint32_t;
+
+#define LOG_AGENT_STATE()        JAMI_INFO("AGENT: In state %s", __FUNCTION__)
+#define AGENT_ERR(FMT, ARGS...)  JAMI_ERR("AGENT: " FMT, ##ARGS)
+#define AGENT_INFO(FMT, ARGS...) JAMI_INFO("AGENT: " FMT, ##ARGS)
+#define AGENT_DBG(FMT, ARGS...)  JAMI_DBG("AGENT: " FMT, ##ARGS)
+#define AGENT_ASSERT(COND, MSG, ARGS...) \
+    if (not(COND)) { \
+        AGENT_ERR(MSG, ##ARGS); \
+        exit(1); \
+    }
+
+void
+Agent::initBehavior()
+{
+    using std::bind;
+
+    BT::register_behavior("search-peer", bind(&Agent::searchPeer, this));
+    BT::register_behavior("wait", bind(&Agent::wait, this));
+    BT::register_behavior("echo", bind(&Agent::echo, this));
+    BT::register_behavior("make-call", bind(&Agent::makeCall, this));
+    BT::register_behavior("true", bind(&Agent::True, this));
+    BT::register_behavior("false", bind(&Agent::False, this));
+}
+
+void
+Agent::configure(const std::string& yaml_config)
+{
+    std::ifstream file = std::ifstream(yaml_config);
+
+    AGENT_ASSERT(file.is_open(), "Failed to open configuration file `%s`", yaml_config.c_str());
+
+    YAML::Node node = YAML::Load(file);
+
+    auto account = node["account-id"];
+
+    AGENT_ASSERT(account.IsScalar(), "Bad or missing field `account-id`");
+
+    accountID_ = account.as<std::string>();
+
+    auto peers = node["peers"];
+
+    AGENT_ASSERT(peers.IsSequence(), "Configuration node `peers` must be a sequence");
+
+    for (const auto& peer : peers) {
+        peers_.emplace_back(peer.as<std::string>());
+    }
+
+    root_ = BT::from_yaml(node["behavior"]);
+
+    /* params */
+    auto params = node["params"];
+
+    if (params) {
+        auto accountID_details = DRing::getAccountDetails(accountID_);
+        assert(params.IsSequence());
+        for (const auto& param : params) {
+            assert(param.IsSequence());
+            for (const auto& details : param) {
+                assert(details.IsMap());
+                for (const auto& detail : details) {
+                    auto first = detail.first.as<std::string>();
+                    auto second = detail.second.as<std::string>();
+                    accountID_details["Account." + first] = second;
+                }
+            }
+            params_.emplace_back([this, accountID_details = std::move(accountID_details)] {
+                DRing::setAccountDetails(accountID_, accountID_details);
+            });
+        }
+    }
+}
+
+void
+Agent::ensureAccount()
+{
+    std::map<std::string, std::string> details;
+
+    details = DRing::getAccountDetails(accountID_);
+
+    if (details.empty()) {
+        details[DRing::Account::ConfProperties::TYPE] = "RING";
+        details[DRing::Account::ConfProperties::DISPLAYNAME] = "AGENT";
+        details[DRing::Account::ConfProperties::ALIAS] = "AGENT";
+        details[DRing::Account::ConfProperties::ARCHIVE_PASSWORD] = "";
+        details[DRing::Account::ConfProperties::ARCHIVE_PIN] = "";
+        details[DRing::Account::ConfProperties::ARCHIVE_PATH] = "";
+
+        accountID_ = DRing::addAccount(details);
+
+        wait_for_announcement_of(accountID_);
+
+        details = DRing::getAccountDetails(accountID_);
+    }
+
+    peerID_ = details.at(DRing::Account::ConfProperties::USERNAME);
+}
+
+void
+Agent::getConversations()
+{
+    conversations_ = DRing::getConversations(accountID_);
+}
+
+void
+Agent::installSignalHandlers()
+{
+    using namespace std::placeholders;
+    using std::bind;
+
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> handlers;
+
+    handlers.insert(DRing::exportable_callback<DRing::CallSignal::IncomingCallWithMedia>(
+        bind(&Agent::Handler<const std::string&,
+                             const std::string&,
+                             const std::string&,
+                             const std::vector<DRing::MediaMap>>::execute,
+             &onIncomingCall_,
+             _1,
+             _2,
+             _3,
+             _4)));
+
+    handlers.insert(DRing::exportable_callback<DRing::CallSignal::StateChange>(
+        bind(&Agent::Handler<const std::string&, const std::string&, signed>::execute,
+             &onCallStateChanged_,
+             _1,
+             _2,
+             _3)));
+
+    handlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        bind(&Agent::Handler<const std::string&,
+                             const std::string&,
+                             std::map<std::string, std::string>>::execute,
+             &onMessageReceived_,
+             _1,
+             _2,
+             _3)));
+
+    handlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            bind(&Agent::Handler<const std::string&,
+                                 const std::string&,
+                                 std::map<std::string, std::string>>::execute,
+                 &onConversationRequestReceived_,
+                 _1,
+                 _2,
+                 _3)));
+
+    handlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        bind(&Agent::Handler<const std::string&, const std::string&>::execute,
+             &onConversationReady_,
+             _1,
+             _2)));
+
+    handlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::ContactAdded>(
+        bind(&Agent::Handler<const std::string&, const std::string&, bool>::execute,
+             &onContactAdded_,
+             _1,
+             _2,
+             _3)));
+
+    DRing::registerSignalHandlers(handlers);
+}
+
+void
+Agent::registerStaticCallbacks()
+{
+    onIncomingCall_.add([=](const std::string& accountID,
+                            const std::string& callID,
+                            const std::string& peerDisplayName,
+                            const std::vector<DRing::MediaMap> mediaList) {
+        (void) accountID;
+        (void) peerDisplayName;
+
+        AGENT_INFO("Incoming call from `%s`", peerDisplayName.c_str());
+
+        AGENT_ASSERT(DRing::acceptWithMedia(callID, mediaList),
+                     "Failed to accept call `%s`",
+                     callID.c_str());
+
+        return true;
+    });
+
+    onMessageReceived_.add([=](const std::string& accountID,
+                               const std::string& conversationID,
+                               std::map<std::string, std::string> message) {
+        (void) accountID;
+
+        /* Read only text message */
+        if ("text/plain" != message.at("type")) {
+            return true;
+        }
+
+        auto author = message.at("author");
+
+        /* Skip if sent by agent */
+        if (peerID_ == author) {
+            return true;
+        }
+
+        auto msg = message.at("body");
+
+        AGENT_INFO("Incomming message `%s` from %s", msg.c_str(), author.c_str());
+
+        /* Echo back */
+        DRing::sendMessage(accountID_, conversationID, msg, "");
+
+        return true;
+    });
+
+    onConversationRequestReceived_.add([=](const std::string& accountID,
+                                           const std::string& conversationID,
+                                           std::map<std::string, std::string> meta) {
+        (void) meta;
+
+        AGENT_INFO("Conversation request received for account %s", accountID.c_str());
+
+        DRing::acceptConversationRequest(accountID, conversationID);
+
+        return true;
+    });
+
+    onConversationReady_.add([=](const std::string& accountID, const std::string& conversationID) {
+        (void) accountID;
+        conversations_.emplace_back(conversationID);
+        return true;
+    });
+
+    onContactAdded_.add([=](const std::string& accountID, const std::string& URI, bool confirmed) {
+        AGENT_INFO("Contact added `%s` : %s", URI.c_str(), confirmed ? "accepted" : "refused");
+        if (confirmed) {
+            DRing::subscribeBuddy(accountID, URI, true);
+        }
+        return true;
+    });
+}
+
+bool
+Agent::searchPeer()
+{
+    LOG_AGENT_STATE();
+
+    std::set<std::string> peers;
+
+    /* Prune contacts already friend with */
+    for (auto it = peers_.begin(); it != peers_.end(); ++it) {
+        bool prune = false;
+        for (const auto& conv : conversations_) {
+            if (conv == *it) {
+                prune = true;
+                break;
+            }
+        }
+        if (not prune) {
+            peers.emplace(*it);
+        }
+    }
+
+    auto cv = std::make_shared<std::condition_variable>();
+
+    for (auto it = peers.begin(); it != peers.end(); ++it) {
+        DRing::sendTrustRequest(accountID_, it->c_str());
+        DRing::subscribeBuddy(accountID_, it->c_str(), true);
+    }
+
+    if (conversations_.size()) {
+        return true;
+    }
+
+    onContactAdded_.add([=](const std::string&, const std::string&, bool) {
+        if (conversations_.size()) {
+            cv->notify_one();
+            return false;
+        }
+        return true;
+    });
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lck(mtx);
+
+    cv->wait(lck);
+
+    return true;
+}
+
+void
+Agent::run(const std::string& yaml_config)
+{
+    static Agent agent;
+
+    agent.initBehavior();
+    agent.configure(yaml_config);
+    agent.ensureAccount();
+    agent.getConversations();
+    agent.installSignalHandlers();
+    agent.registerStaticCallbacks();
+
+    if (agent.params_.size()) {
+        while (true) {
+            for (auto& cb : agent.params_) {
+                cb();
+                (*agent.root_)();
+            }
+        }
+    } else {
+        while ((*agent.root_) ()) {
+            /* Until root fails */
+        }
+    }
+}
+
+/* Helper start here */
+void
+Agent::sendMessage(const std::string& to, const std::string& msg)
+{
+    auto parent = "";
+
+    DRing::sendMessage(accountID_, to, msg, parent);
+}
+
+/* Behavior start here */
+
+bool
+Agent::echo()
+{
+    LOG_AGENT_STATE();
+
+    if (conversations_.empty()) {
+        return false;
+    }
+
+    auto it = conversations_.begin();
+
+    std::advance(it, rand() % conversations_.size());
+
+    auto cv = std::make_shared<std::condition_variable>();
+    auto pongReceived = std::make_shared<std::atomic_bool>(false);
+    auto to = *it;
+
+    std::string alphabet = "0123456789ABCDEF";
+    std::string messageSent;
+
+    onMessageReceived_.add([=](const std::string& accountID,
+                               const std::string& conversationID,
+                               std::map<std::string, std::string> message) {
+        (void) accountID;
+        (void) conversationID;
+        (void) message;
+        (void) conversationID;
+
+        if ("text/plain" != message.at("type")) {
+            return true;
+        }
+
+        auto msg = message.at("body");
+
+        if (pongReceived->load()) {
+            return false;
+        }
+
+        if (to == message.at("author") and msg == messageSent) {
+            *pongReceived = true;
+            cv->notify_one();
+            return false;
+        }
+
+        return true;
+    });
+
+    /* Sending msg */
+    for (usize i = 0; i < 16; ++i) {
+        messageSent.push_back(alphabet[rand() % alphabet.size()]);
+    }
+
+    sendMessage(*it, messageSent);
+
+    /* Waiting for echo */
+
+    std::mutex mutex;
+    std::unique_lock<std::mutex> lck(mutex);
+
+    bool ret = std::cv_status::no_timeout == cv->wait_for(lck, std::chrono::seconds(30))
+               and pongReceived->load();
+
+    return ret;
+}
+
+bool
+Agent::makeCall()
+{
+    LOG_AGENT_STATE();
+
+    if (conversations_.empty()) {
+        return false;
+    }
+
+    auto it = conversations_.begin();
+
+    std::advance(it, rand() % conversations_.size());
+
+    auto cv = std::make_shared<std::condition_variable>();
+
+    onCallStateChanged_.add([=](const std::string&, const std::string& state, signed) {
+        if ("CURRENT" == state) {
+            cv->notify_one();
+            return false;
+        }
+
+        if ("OVER" == state) {
+            return false;
+        }
+
+        return true;
+    });
+
+    auto members = DRing::getConversationMembers(accountID_, *it);
+
+    std::string uri;
+
+    for (const auto& member : members) {
+        if (member.at("uri") != peerID_) {
+            uri = member.at("uri");
+            break;
+        }
+    }
+
+    if (uri.empty()) {
+        return false;
+    }
+
+    auto callID = DRing::placeCall(accountID_, uri);
+
+    bool ret = true;
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lck {mtx};
+
+    if (std::cv_status::timeout == cv->wait_for(lck, std::chrono::seconds(30))) {
+        ret = false;
+    }
+
+    DRing::hangUp(callID);
+
+    return ret;
+}
+
+bool
+Agent::wait()
+{
+    LOG_AGENT_STATE();
+
+    std::this_thread::sleep_for(std::chrono::seconds(30));
+
+    return true;
+}
diff --git a/test/agent/agent.h b/test/agent/agent.h
new file mode 100644
index 0000000000000000000000000000000000000000..6e6419eebcace23e266d54a39e0afb622f728b84
--- /dev/null
+++ b/test/agent/agent.h
@@ -0,0 +1,112 @@
+/*
+ *  Copyright (C) 2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Olivier Dion <olivier.dion@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#pragma once
+
+/* agent */
+#include "agent/bt.h"
+
+/* Dring */
+#include "dring/dring.h"
+
+/* std */
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+class Agent
+{
+    template<typename... Args>
+    class Handler
+    {
+        std::mutex mutex_;
+        std::vector<std::function<bool(Args...)>> callbacks_;
+
+    public:
+        void add(std::function<bool(Args...)>&& cb)
+        {
+            std::unique_lock<std::mutex> lck(mutex_);
+            callbacks_.emplace_back(std::move(cb));
+        }
+
+        void execute(Args... args)
+        {
+            std::vector<std::function<bool(Args...)>> to_keep;
+            std::unique_lock<std::mutex> lck(mutex_);
+
+            for (auto& cb : callbacks_) {
+                if (cb(args...)) {
+                    to_keep.emplace_back(std::move(cb));
+                }
+            }
+
+            callbacks_.swap(to_keep);
+        }
+    };
+
+    /* Signal handlers */
+    Handler<const std::string&, const std::string&, std::map<std::string, std::string>>
+        onMessageReceived_;
+
+    Handler<const std::string&, const std::string&, std::map<std::string, std::string>>
+        onConversationRequestReceived_;
+
+    Handler<const std::string&, const std::string&> onConversationReady_;
+
+    Handler<const std::string&, const std::string&, signed> onCallStateChanged_;
+
+    Handler<const std::string&,
+            const std::string&,
+            const std::string&,
+            const std::vector<DRing::MediaMap>>
+        onIncomingCall_;
+
+    Handler<const std::string&, const std::string&, bool> onContactAdded_;
+    /*  Initialize agent */
+    void configure(const std::string& yaml_config);
+    void getConversations();
+    void ensureAccount();
+    void initBehavior();
+    void installSignalHandlers();
+    void registerStaticCallbacks();
+
+    /* Bookkeeping */
+    std::string peerID_;
+    std::string accountID_;
+    std::vector<std::string> peers_;
+    std::vector<std::string> conversations_;
+    std::unique_ptr<BT::Node> root_;
+    std::vector<std::function<void(void)>> params_;
+
+    /* Helper */
+    void sendMessage(const std::string& to, const std::string& msg);
+
+    /* Behavior */
+    bool searchPeer();
+    bool wait();
+    bool echo();
+    bool makeCall();
+    bool False() { return false; }
+    bool True() { return true; }
+
+public:
+    static void run(const std::string& yaml_config);
+};
diff --git a/test/agent/bt.cpp b/test/agent/bt.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c03abc48b3745fd3e87684d005f6ea04bc91c61a
--- /dev/null
+++ b/test/agent/bt.cpp
@@ -0,0 +1,60 @@
+/*
+ *  Copyright (C) 2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Olivier Dion <olivier.dion@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#include "agent/bt.h"
+
+namespace BT {
+
+std::map<std::string, std::function<bool(void)>> registered_behaviors;
+
+std::unique_ptr<Node>
+from_yaml(YAML::Node behavior)
+{
+    std::unique_ptr<Node> node;
+
+    if (behavior.IsSequence()) {
+        auto tmp = new Sequence();
+
+        for (const auto& sub_behavior : behavior) {
+            tmp->add(from_yaml(sub_behavior));
+        }
+
+        node.reset(dynamic_cast<Node*>(tmp));
+
+    } else if (behavior.IsMap()) {
+        auto tmp = new Selector();
+
+        for (const auto& kv : behavior) {
+            assert(kv.second.IsSequence());
+            for (const auto& sub_behavior : kv.second) {
+                tmp->add(from_yaml(sub_behavior));
+            }
+        }
+
+        node.reset(dynamic_cast<Node*>(tmp));
+
+    } else {
+        node = std::make_unique<Execute>(behavior.as<std::string>());
+    }
+
+    return node;
+}
+
+}; // namespace BT
diff --git a/test/agent/bt.h b/test/agent/bt.h
new file mode 100644
index 0000000000000000000000000000000000000000..18537f88aad2e58db5269d6c17326558ab827345
--- /dev/null
+++ b/test/agent/bt.h
@@ -0,0 +1,105 @@
+/*
+ *  Copyright (C) 2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Olivier Dion <olivier.dion@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#pragma once
+
+#include <cassert>
+#include <functional>
+#include <map>
+
+#include <yaml-cpp/yaml.h>
+
+/* Jami */
+#include "logger.h"
+
+namespace BT {
+
+extern std::map<std::string, std::function<bool(void)>> registered_behaviors;
+
+static inline void
+register_behavior(const std::string& name, std::function<bool(void)>&& behavior)
+{
+    registered_behaviors[name] = std::move(behavior);
+}
+
+class Node
+{
+public:
+    virtual bool operator()(void) = 0;
+
+    virtual ~Node() = default;
+};
+
+class Execute : public Node
+{
+    std::function<bool(void)> todo_ {};
+
+public:
+    Execute(const std::string& behavior_name)
+    {
+        try {
+            todo_ = registered_behaviors.at(behavior_name);
+        } catch (const std::exception& E) {
+            JAMI_ERR("AGENT: Invalid behavior `%s`: %s", behavior_name.c_str(), E.what());
+        }
+    }
+
+    virtual bool operator()(void) override { return todo_(); }
+};
+
+struct Sequence : public Node
+{
+    std::vector<std::unique_ptr<Node>> nodes_;
+
+public:
+    virtual bool operator()(void) override
+    {
+        for (const auto& node : nodes_) {
+            if (not(*node)()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    void add(std::unique_ptr<Node> node) { nodes_.push_back(std::move(node)); }
+};
+
+struct Selector : public Node
+{
+    std::vector<std::unique_ptr<Node>> nodes_;
+
+public:
+    virtual bool operator()(void) override
+    {
+        for (const auto& node : nodes_) {
+            if ((*node)()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void add(std::unique_ptr<Node> node) { nodes_.push_back(std::move(node)); }
+};
+
+extern std::unique_ptr<Node> from_yaml(YAML::Node behavior);
+
+}; // namespace BT
diff --git a/test/agent/jami-agent-config.yml b/test/agent/jami-agent-config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..18388dcce0546a7f30a41f9870bfc5361b0917ae
--- /dev/null
+++ b/test/agent/jami-agent-config.yml
@@ -0,0 +1,16 @@
+behavior:
+  - search-peer
+#  - : [ echo, true ]
+#  - : [ make-call, true ]
+  - wait
+
+peers:
+  - "your-peer-id"
+
+account-id: "d65ebb0f1170ae44"
+
+params:
+  - [ upnpEnabled: "true", turnEnabled: "true" ]
+  # - [ upnpEnabled: "true", turnEnabled: "false" ]
+  # - [ upnpEnabled: "false", turnEnabled: "true" ]
+  # - [ upnpEnabled: "false", turnEnabled: "false" ]
diff --git a/test/agent/jami-config.yml b/test/agent/jami-config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1c5ff51e8eda53957dfcc6a7a9a109fd61f450ea
--- /dev/null
+++ b/test/agent/jami-config.yml
@@ -0,0 +1,63 @@
+accounts:
+  []
+preferences:
+  historyLimit: 0
+  ringingTimeout: 30
+  historyMaxCalls: 20
+  md5Hash: false
+  order: 1479408070c09163/
+  portNum: 5060
+  searchBarDisplay: true
+  zoneToneChoice: North America
+voipPreferences:
+  disableSecureDlgCheck: false
+  playDtmf: true
+  playTones: true
+  pulseLength: 250
+  symmetric: true
+  zidFile: ""
+audio:
+  alsa:
+    cardIn: 0
+    cardOut: 0
+    cardRing: 0
+    plugin: default
+    smplRate: 44100
+  alwaysRecording: false
+  audioApi: pulseaudio
+  automaticGainControl: false
+  captureMuted: false
+  noiseReduce: false
+  playbackMuted: false
+  pulse:
+    devicePlayback: ""
+    deviceRecord: ""
+    deviceRingtone: ""
+  portaudio:
+    devicePlayback: ""
+    deviceRecord: ""
+    deviceRingtone: ""
+  recordPath: ""
+  volumeMic: 1
+  volumeSpkr: 1
+  echoCanceller: system
+video:
+  recordPreview: true
+  recordQuality: 0
+  decodingAccelerated: true
+  encodingAccelerated: false
+  conferenceResolution: 1280x720
+  devices:
+    []
+plugins:
+  pluginsEnabled: false
+  installedPlugins:
+    []
+  loadedPlugins:
+    []
+shortcuts:
+  hangUp: ""
+  pickUp: ""
+  popupWindow: ""
+  toggleHold: ""
+  togglePickupHangup: ""
\ No newline at end of file
diff --git a/test/agent/main.cpp b/test/agent/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..849071f619e9bafd7b702a61e85e6e7a4c4d4ef8
--- /dev/null
+++ b/test/agent/main.cpp
@@ -0,0 +1,36 @@
+/*
+ *  Copyright (C) 2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Olivier Dion <olivier.dion@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#include "agent/agent.h"
+
+/* Jami */
+#include "dring.h"
+
+int
+main(void)
+{
+    DRing::init(DRing::InitFlag(DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_CONSOLE_LOG));
+
+    assert(DRing::start("jami-config.yml"));
+
+    Agent::run("jami-agent-config.yml");
+
+    DRing::fini();
+}
diff --git a/test/agent/utils.cpp b/test/agent/utils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f3bbcab5aa5a8d2fe885d6cff0fd6a6a07dc6880
--- /dev/null
+++ b/test/agent/utils.cpp
@@ -0,0 +1,89 @@
+/*
+ *  Copyright (C) 2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Olivier Dion <olivier.dion>@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+/* std */
+#include <atomic>
+#include <condition_variable>
+#include <mutex>
+#include <cassert>
+#include <vector>
+
+/* DRing */
+#include "dring.h"
+#include "dring/account_const.h"
+#include "dring/configurationmanager_interface.h"
+#include "dring/call_const.h"
+
+/* agent */
+#include "agent/utils.h"
+
+void
+wait_for_announcement_of(const std::vector<std::string> accountIDs, std::chrono::seconds timeout)
+{
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::vector<std::atomic_bool> accountsReady(accountIDs.size());
+
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>(
+            [&,
+             accountIDs = std::move(accountIDs)](const std::string& accountID,
+                                                 const std::map<std::string, std::string>& details) {
+                for (size_t i = 0; i < accountIDs.size(); ++i) {
+                    if (accountIDs[i] != accountID) {
+                        continue;
+                    }
+
+                    try {
+                        if ("true"
+                            != details.at(DRing::Account::VolatileProperties::DEVICE_ANNOUNCED)) {
+                            continue;
+                        }
+                    } catch (const std::out_of_range&) {
+                        continue;
+                    }
+
+                    accountsReady[i] = true;
+                    cv.notify_one();
+                }
+            }));
+
+    DRing::registerSignalHandlers(confHandlers);
+
+    assert(cv.wait_for(lk, timeout, [&] {
+        for (const auto& rdy : accountsReady) {
+            if (not rdy) {
+                return false;
+            }
+        }
+
+        return true;
+    }));
+
+    DRing::unregisterSignalHandlers();
+}
+
+void
+wait_for_announcement_of(const std::string& accountId, std::chrono::seconds timeout)
+{
+    wait_for_announcement_of(std::vector<std::string> {accountId}, timeout);
+}
diff --git a/test/agent/utils.h b/test/agent/utils.h
new file mode 100644
index 0000000000000000000000000000000000000000..148fb4aa8295c3f1ab4118ac47fb644c4cf33931
--- /dev/null
+++ b/test/agent/utils.h
@@ -0,0 +1,45 @@
+/*
+ *  Copyright (C) 2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Olivier Dion <olivier.dion>@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#pragma once
+
+#include <chrono>
+#include <cstdint>
+#include <map>
+#include <string>
+
+constexpr size_t WAIT_FOR_ANNOUNCEMENT_TIMEOUT = 30;
+constexpr size_t WAIT_FOR_REMOVAL_TIMEOUT = 30;
+
+extern void
+wait_for_announcement_of(const std::vector<std::string> accountIDs,
+                         std::chrono::seconds timeout = std::chrono::seconds(WAIT_FOR_ANNOUNCEMENT_TIMEOUT));
+
+extern void
+wait_for_announcement_of(const std::string& accountId,
+                         std::chrono::seconds timeout = std::chrono::seconds(WAIT_FOR_ANNOUNCEMENT_TIMEOUT));
+
+extern void
+wait_for_removal_of(const std::vector<std::string> accounts,
+                    std::chrono::seconds timeout = std::chrono::seconds(WAIT_FOR_REMOVAL_TIMEOUT));
+
+extern void
+wait_for_removal_of(const std::string& account,
+                    std::chrono::seconds timeout = std::chrono::seconds(WAIT_FOR_REMOVAL_TIMEOUT));