diff --git a/test/agent/.gitignore b/test/agent/.gitignore
index 93d93c78cb8eeaea6050f5082cf303ee6a27a1dc..a39bc478f1648bc10c8420d593657451c84907bd 100644
--- a/test/agent/.gitignore
+++ b/test/agent/.gitignore
@@ -1,3 +1,6 @@
 agent
-
-
+*.yml
+*.txt
+*.log
+.gdbinit
+*.gdb
\ No newline at end of file
diff --git a/test/agent/Makefile.am b/test/agent/Makefile.am
index 349846a7cf01187f0a1e501625f98ef721f3696d..6dea50cbe4bab8bfea492d79cc281e3211a521b5 100644
--- a/test/agent/Makefile.am
+++ b/test/agent/Makefile.am
@@ -1,10 +1,15 @@
 include $(top_srcdir)/globals.mk
 
-AM_CXXFLAGS += -I$(top_srcdir)/src -I.. \
-                -DTOP_BUILDDIR=\"$$(cd "$(top_builddir)"; pwd)\"
+AM_CXXFLAGS += -I$(top_srcdir)/src -I.. $(GUILE_CFLAGS) -rdynamic
+AM_LDFLAGS  += $(GUILE_LIBS)
 
 check_PROGRAMS = agent
 
-agent_SOURCES = agent.cpp agent.h utils.cpp utils.h bt.cpp bt.h main.cpp log.h
+agent_SOURCES =      \
+	agent.cpp    \
+	agent.h      \
+	bindings.cpp \
+	bindings.h   \
+	main.cpp
 
-agent_LDADD = $(top_builddir)/src/libring.la -ldl
+agent_LDADD = $(top_builddir)/src/libring.la
diff --git a/test/agent/README.md b/test/agent/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..aa79a9eac62420717d10b39131269acb24c2ec8d
--- /dev/null
+++ b/test/agent/README.md
@@ -0,0 +1,55 @@
+# Compile
+To compile the agent, one has to enable it while configuring the daemon.
+
+```sh
+./configure --enable-agent
+```
+
+then you need to recompile the contrib.  This will compile Guile, which can take
+some time.
+
+# Running the agent
+The agent expects a Scheme file has its first parameter.  This scheme file will
+be interpreted by Guile.  In the script, you can control the agent.
+
+Usage:
+```sh
+./agent ./examples/passive-agent.scm
+```
+
+# Guile bindings
+In order for Guile to control the agent, bindings have to be added to the global
+environment where the configuration file is being interpreted.  This is done in
+`main.cpp` in the function `install_scheme_primitive()`.  All scheme bindings
+should have the prefix `agent:` to be clear that the procedure is one that
+control the agent.
+
+When a binding is called from Guile, the arguments passed are Scheme objects of
+type `SCM`.  This is an opaque type that is generic.  In order to be clear on
+what the underlying type needed by the primitive procedure is, one should add the
+suffix of the type at the end.
+
+For example, `my_primitive_procedure()` expects that `some_variable_str`
+will be of type `string`.  This is enforced by using an assertion:
+```c++
+static SCM my_primitive_procedure(SCM some_variable_str)
+{
+   AGENT_ASSERT(scm_is_string(some_variable_str), "`some_variable_str` must be of type string");
+   ...
+}
+```
+
+Here is another example where `my_second_primitive()` expects that
+`some_variable_vector_or_str` to be of type `string` or `vector`:
+```c++
+static SCM my_second_primitive(SCM some_variable_vector_or_str)
+{
+   AGENT_ASSERT(scm_is_string(some_variable_vector_or_str) ||
+                scm_is_simple_vector(some_variable_vector_or_str),
+                "`scm_some_variable_vector_or_str` must either be of type vector or string");
+  ...
+}
+```
+
+# Writing scenarios
+See `examples/`
diff --git a/test/agent/agent.cpp b/test/agent/agent.cpp
index a28f322513880b39d896bab9e2571f22f21227e4..c29e179e95fd9ab1f6417ecebc649404faaae77f 100644
--- a/test/agent/agent.cpp
+++ b/test/agent/agent.cpp
@@ -27,9 +27,6 @@
 #include <mutex>
 #include <thread>
 
-/* Third parties */
-#include <yaml-cpp/yaml.h>
-
 /* DRing */
 #include "account_const.h"
 #include "jami/presencemanager_interface.h"
@@ -39,94 +36,251 @@
 
 /* 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); \
-    }
+
+#define LOG_AGENT_STATE() AGENT_DBG("In state %s", __FUNCTION__)
 
 void
-Agent::initBehavior()
+Agent::searchForPeers(std::vector<std::string>& peers)
 {
-    using std::bind;
+    LOG_AGENT_STATE();
 
-    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));
-    BT::register_behavior("start-log-recording", bind(&Agent::startLogRecording, this));
-    BT::register_behavior("stop-log-recording", bind(&Agent::stopLogRecording, this));
+    for (auto it = peers.begin(); it != peers.end(); ++it) {
+        DRing::sendTrustRequest(accountID_, it->c_str());
+        DRing::subscribeBuddy(accountID_, it->c_str(), true);
+    }
 }
 
-void
-Agent::configure(const std::string& yaml_config)
+bool
+Agent::ping(const std::string& conversation)
 {
-    std::ifstream file = std::ifstream(yaml_config);
+    LOG_AGENT_STATE();
+
+    auto cv = std::make_shared<std::condition_variable>();
+    auto pongReceived = std::make_shared<std::atomic_bool>(false);
+
+    std::string alphabet = "0123456789ABCDEF";
+    std::string messageSent;
+
+    for (usize i = 0; i < 16; ++i) {
+        messageSent.push_back(alphabet[rand() % alphabet.size()]);
+    }
+
+    onMessageReceived_.add([=](const std::string& /* accountID */,
+                               const std::string& conversationID,
+                               std::map<std::string, std::string> message) {
 
-    AGENT_ASSERT(file.is_open(), "Failed to open configuration file `%s`", yaml_config.c_str());
+        if ("text/plain" != message.at("type")) {
+            return true;
+        }
+
+        auto msg = message.at("body");
+
+        if (pongReceived->load()) {
+            return false;
+        }
+
+        if (conversationID == conversation and message.at("author") != peerID_
+            and msg == "PONG:" + messageSent) {
+            *pongReceived = true;
+            cv->notify_one();
+            return false;
+        }
+
+        return true;
+    });
 
-    YAML::Node node = YAML::Load(file);
+    AGENT_INFO("Sending ping `%s` to `%s`", messageSent.c_str(), conversation.c_str());
 
-    auto context = node["record-context"];
+    DRing::sendMessage(accountID_, conversation, messageSent, "");
 
-    if (context.IsScalar()) {
-        context_ = context.as<std::string>();
+    /* 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();
+
+    AGENT_INFO("Pong %s", ret ? "received" : "missing");
+
+    return ret;
+}
+
+std::string
+Agent::someContact() const
+{
+    auto members = DRing::getConversationMembers(accountID_, someConversation());
+
+    for (const auto& member : members) {
+        if (member.at("uri") != peerID_) {
+            return member.at("uri");
+
+        }
     }
 
-    auto to = node["record-to"];
+    return "";
+}
 
-    if (to.IsScalar()) {
-        recordTo_ = to.as<std::string>();
+std::string
+Agent::someConversation() const
+{
+    if (conversations_.empty()) {
+        return "";
     }
 
-    auto peers = node["peers"];
+    auto it = conversations_.begin();
+
+    std::advance(it, rand() % conversations_.size());
+
+    return *it;
+}
+
+bool
+Agent::placeCall(const std::string& contact)
+{
+    LOG_AGENT_STATE();
+
+    auto cv = std::make_shared<std::condition_variable>();
 
-    AGENT_ASSERT(peers.IsSequence(), "Configuration node `peers` must be a sequence");
+    auto callID = DRing::placeCall(accountID_, contact);
+    auto success = std::make_shared<std::atomic<bool>>(false);
+    auto over = std::make_shared<std::atomic<bool>>(false);
 
-    for (const auto& peer : peers) {
-        peers_.emplace_back(peer.as<std::string>());
+    if (callID.empty()) {
+        return false;
     }
 
-    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);
-            });
+    onCallStateChanged_.add([=](const std::string& call_id, const std::string& state, signed code) {
+
+        AGENT_INFO("[call:%s] In state %s : %d", callID.c_str(), state.c_str(), code);
+
+        if (call_id != callID) {
+            return true;
+        }
+
+        bool ret = true;
+
+        if ("CURRENT" == state) {
+            success->store(true);
+        } else if ("OVER" == state) {
+            over->store(true);
+            ret = false;
         }
+
+        cv->notify_one();
+
+        return ret;
+    });
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lck {mtx};
+
+    AGENT_INFO("Waiting for call %s", callID.c_str());
+
+    /* TODO - Parametize me */
+    cv->wait_for(lck, std::chrono::seconds(30), [=]{
+        return success->load() or over->load();
+    });
+
+    if (success->load()) {
+        AGENT_INFO("[call:%s] to %s: SUCCESS", callID.c_str(), contact.c_str());
+        DRing::hangUp(callID);
+    } else {
+        AGENT_INFO("[call:%s] to %s: FAIL", callID.c_str(), contact.c_str());
     }
+
+    if (not over->load()) {
+        cv->wait_for(lck, std::chrono::seconds(30), [=] { return over->load(); });
+    }
+
+    return success->load();
+}
+
+void
+Agent::wait(std::chrono::seconds period)
+{
+    LOG_AGENT_STATE();
+
+    std::this_thread::sleep_for(period);
+}
+
+void
+Agent::setDetails(const std::map<std::string, std::string>& details)
+{
+    LOG_AGENT_STATE();
+
+    auto cv = std::make_shared<std::condition_variable>();
+    auto cont = std::make_shared<std::atomic<bool>>(true);
+
+    std::string info("Setting details:\n");
+
+    for (const auto& [key, value] : details) {
+        info += key + " = " + value + "\n";
+    }
+
+    AGENT_INFO("%s", info.c_str());
+
+    DRing::setAccountDetails(accountID_, details);
+}
+
+void
+Agent::activate(bool state)
+{
+    LOG_AGENT_STATE();
+
+    DRing::setAccountActive(accountID_, state);
+
+    if (state) {
+        waitForAnnouncement();
+    }
+}
+
+void
+Agent::exportToArchive(const std::string& path)
+{
+    LOG_AGENT_STATE();
+
+    AGENT_ASSERT(DRing::exportToFile(accountID_,
+                                     path),
+                 "Failed to export account to `%s`", path.c_str());
+}
+
+void
+Agent::importFromArchive(const std::string& path)
+{
+    LOG_AGENT_STATE();
+
+    std::map<std::string, std::string> details;
+
+    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] = path;
+
+    AGENT_ASSERT(accountID_ == DRing::addAccount(details, accountID_), "Bad accountID");
+
+    details = DRing::getAccountDetails(accountID_);
+
+    waitForAnnouncement();
+
+    AGENT_ASSERT("AGENT" == details.at(DRing::Account::ConfProperties::DISPLAYNAME),
+                 "Bad display name");
+
+    peerID_ = details.at(DRing::Account::ConfProperties::USERNAME);
+    conversations_ = DRing::getConversations(accountID_);
+
+    AGENT_INFO("Using account %s - %s", accountID_.c_str(), peerID_.c_str());
 }
 
 void
 Agent::ensureAccount()
 {
+    LOG_AGENT_STATE();
+
     std::map<std::string, std::string> details;
 
     details = DRing::getAccountDetails(accountID_);
@@ -140,22 +294,28 @@ Agent::ensureAccount()
         details[DRing::Account::ConfProperties::ARCHIVE_PATH] = "";
 
         AGENT_ASSERT(accountID_ == DRing::addAccount(details, accountID_), "Bad accountID");
+    }
 
-        wait_for_announcement_of(accountID_);
+    waitForAnnouncement();
 
-        details = DRing::getAccountDetails(accountID_);
-    }
+    details = DRing::getAccountDetails(accountID_);
 
     AGENT_ASSERT("AGENT" == details.at(DRing::Account::ConfProperties::DISPLAYNAME),
                  "Bad display name");
 
     peerID_ = details.at(DRing::Account::ConfProperties::USERNAME);
+    conversations_ = DRing::getConversations(accountID_);
+
+    AGENT_INFO("Using account %s - %s", accountID_.c_str(), peerID_.c_str());
 }
 
-void
-Agent::getConversations()
+
+Agent&
+Agent::instance()
 {
-    conversations_ = DRing::getConversations(accountID_);
+    static Agent agent;
+
+    return agent;
 }
 
 void
@@ -216,8 +376,19 @@ Agent::installSignalHandlers()
              _2,
              _3)));
 
-    handlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::MessageSend>(
-        bind(&Agent::onLogging, this, _1)));
+    handlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::RegistrationStateChanged>(
+        bind(&Agent::Handler<const std::string&, const std::string&, int, const std::string&>::execute,
+             &onRegistrationStateChanged_,
+             _1,
+             _2,
+             _3,
+             _4)));
+
+    handlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>(
+                        bind(&Agent::Handler<const std::string&, const std::map<std::string, std::string>&>::execute,
+                             &onVolatileDetailsChanged_,
+                             _1,
+                             _2)));
 
     DRing::registerSignalHandlers(handlers);
 }
@@ -232,7 +403,18 @@ Agent::registerStaticCallbacks()
         (void) accountID;
         (void) peerDisplayName;
 
-        AGENT_INFO("Incoming call from `%s`", peerDisplayName.c_str());
+        std::string medias("");
+
+        for (const auto& media : mediaList) {
+            for (const auto& [key, value] : media) {
+                medias += key + "=" + value + "\n";
+            }
+        }
+
+        AGENT_INFO("Incoming call `%s` from `%s` with medias:\n`%s`",
+                   callID.c_str(),
+                   peerDisplayName.c_str(),
+                   medias.c_str());
 
         AGENT_ASSERT(DRing::acceptWithMedia(callID, mediaList),
                      "Failed to accept call `%s`",
@@ -263,7 +445,9 @@ Agent::registerStaticCallbacks()
         AGENT_INFO("Incomming message `%s` from %s", msg.c_str(), author.c_str());
 
         /* Echo back */
-        DRing::sendMessage(accountID_, conversationID, msg, "");
+        if (0 != msg.rfind("PONG:", 0)) {
+            DRing::sendMessage(accountID_, conversationID, "PONG:" + msg, "");
+        }
 
         return true;
     });
@@ -296,272 +480,51 @@ Agent::registerStaticCallbacks()
 }
 
 void
-Agent::onLogging(const std::string& message)
+Agent::waitForAnnouncement(std::chrono::seconds timeout)
 {
-    for (const auto& [context, logger] : loggers_) {
-        logger->pushMessage(message);
-    }
-}
-
-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);
+    std::unique_lock<std::mutex> lk {mtx};
 
-    cv->wait(lck);
+    onVolatileDetailsChanged_.add([=](const std::string& accountID,
+                                      const std::map<std::string, std::string>& details) {
 
-    return true;
-}
+                    if (accountID_ != accountID) {
+                        return true;
+                    }
 
-void
-Agent::run(const std::string& yaml_config)
-{
-    static Agent agent;
+                    try {
+                        if ("true"
+                            != details.at(DRing::Account::VolatileProperties::DEVICE_ANNOUNCED)) {
+                            return true;
+                        }
+                    } catch (const std::out_of_range&) {
+                        return true;
+                    }
 
-    agent.initBehavior();
-    agent.ensureAccount();
-    agent.configure(yaml_config);
-    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 */
-        }
-    }
-}
+                    cv->notify_one();
 
-/* 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::startLogRecording()
-{
-    class FileHandler : public LogHandler
-    {
-        std::ofstream out;
-
-    public:
-        FileHandler(const std::string& context, const std::string& to)
-            : LogHandler(context)
-            , out(to)
-        {}
-
-        virtual void pushMessage(const std::string& message) override
-        {
-            out << context_ << message << std::endl;
-        }
-
-        virtual void flush() override { out.flush(); }
-    };
-
-    loggers_[context_] = std::make_unique<FileHandler>(context_, recordTo_);
-
-    setMonitorLog(true);
-
-    return true;
-}
-
-bool
-Agent::stopLogRecording()
-{
-    LOG_AGENT_STATE();
-
-    if (not loggers_.empty()) {
-        loggers_.erase(context_);
-    }
-
-    if (loggers_.empty()) {
-        setMonitorLog(false);
-    }
-
-    return true;
-}
-
-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;
+                    return false;
     });
 
-    /* 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;
+    AGENT_ASSERT(std::cv_status::no_timeout == cv->wait_for(lk, timeout),
+                 "Timeout while waiting for account announcement on DHT");
 }
 
-bool
-Agent::makeCall()
+void
+Agent::init()
 {
     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;
+    installSignalHandlers();
+    registerStaticCallbacks();
 }
 
-bool
-Agent::wait()
+void
+Agent::fini()
 {
     LOG_AGENT_STATE();
 
-    std::this_thread::sleep_for(std::chrono::seconds(30));
-
-    return true;
+    DRing::unregisterSignalHandlers();
 }
diff --git a/test/agent/agent.h b/test/agent/agent.h
index 5cbb348eb43100c4756d5936342ee19d03f5f5f5..21c446a3c47e57c94e2154578b04b0e349fd2092 100644
--- a/test/agent/agent.h
+++ b/test/agent/agent.h
@@ -20,19 +20,27 @@
 
 #pragma once
 
-/* agent */
-#include "agent/bt.h"
-#include "agent/log.h"
-
 /* Dring */
+#include "logger.h"
 #include "jami/jami.h"
 
 /* std */
+#include <chrono>
+#include <map>
 #include <memory>
 #include <mutex>
 #include <string>
 #include <vector>
 
+#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); \
+    }
+
 class Agent
 {
     template<typename... Args>
@@ -82,41 +90,40 @@ class Agent
 
     Handler<const std::string&, const std::string&, bool> onContactAdded_;
 
+    Handler<const std::string&, const std::string&, int, const std::string&>
+        onRegistrationStateChanged_;
+
+    Handler<const std::string&, const std::map<std::string, std::string>&>
+    onVolatileDetailsChanged_;
+
     /*  Initialize agent */
-    void configure(const std::string& yaml_config);
-    void getConversations();
-    void ensureAccount();
-    void initBehavior();
     void installSignalHandlers();
     void registerStaticCallbacks();
 
     /* Bookkeeping */
-    std::string context_;
-    std::string recordTo_;
     std::string peerID_;
-    const std::string accountID_{"afafafafafafafaf"};
-    std::vector<std::string> peers_;
+    const std::string accountID_ {"afafafafafafafaf"};
     std::vector<std::string> conversations_;
-    std::unique_ptr<BT::Node> root_;
-    std::vector<std::function<void(void)>> params_;
-    std::map<std::string, std::unique_ptr<LogHandler>> loggers_;
-
-    /* Event */
-    void onLogging(const std::string& message);
-
-    /* Helper */
-    void sendMessage(const std::string& to, const std::string& msg);
 
+public:
     /* Behavior */
-    bool startLogRecording();
-    bool stopLogRecording();
-    bool searchPeer();
-    bool wait();
-    bool echo();
-    bool makeCall();
-    bool False() { return false; }
-    bool True() { return true; }
+    bool ping(const std::string& conversation);
+    bool placeCall(const std::string& contact);
+    std::string someContact() const;
+    std::string someConversation() const;
+    void setDetails(const std::map<std::string, std::string>& details);
+    void stopRecording(const std::string& context);
+    void startRecording(const std::string& context, const std::string& to);
+    void searchForPeers(std::vector<std::string>& peers);
+    void wait(std::chrono::seconds period);
+    void exportToArchive(const std::string& path);
+    void importFromArchive(const std::string& path);
+    void ensureAccount();
+    void waitForAnnouncement(std::chrono::seconds timeout=std::chrono::seconds(30));
+    void activate(bool state);
 
-public:
-    static void run(const std::string& yaml_config);
+    void init();
+    void fini();
+
+    static Agent& instance();
 };
diff --git a/test/agent/bindings.cpp b/test/agent/bindings.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4ab15351ca5b278cb8fd04f3ff1ae5d138144c26
--- /dev/null
+++ b/test/agent/bindings.cpp
@@ -0,0 +1,215 @@
+/*
+ *  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"
+#include "agent/bindings.h"
+
+static std::string(scm_to_cxx_string)(SCM value_str, const char* value_name)
+{
+    AGENT_ASSERT(scm_is_string(value_str), "`%s` must be of type string", value_name);
+
+    char* str_raw = scm_to_locale_string(value_str);
+    std::string ret(str_raw);
+    free(str_raw);
+
+    return ret;
+}
+
+static SCM
+wait_binding(SCM period_long_optional)
+{
+    std::chrono::seconds period;
+
+    if (SCM_UNBNDP(period_long_optional)) {
+        period = std::chrono::seconds::max();
+    } else {
+        AGENT_ASSERT(scm_is_number(period_long_optional), "Invalid period");
+        period = std::chrono::seconds(scm_to_uint(period_long_optional));
+    }
+
+    Agent::instance().wait(period);
+
+    return SCM_UNDEFINED;
+}
+
+static SCM
+place_call_binding(SCM contact_str)
+{
+    AGENT_ASSERT(scm_is_string(contact_str), "Wrong type for contact");
+
+    return scm_from_bool(Agent::instance().placeCall(scm_to_cxx_string(contact_str)));
+}
+
+static SCM
+some_conversation_binding()
+{
+    auto contact = Agent::instance().someConversation();
+
+    return scm_from_utf8_string(contact.c_str());
+}
+
+static SCM
+some_contact_binding()
+{
+    auto contact = Agent::instance().someContact();
+
+    return scm_from_utf8_string(contact.c_str());
+}
+
+static SCM
+search_peer_binding(SCM peers_vector_or_str)
+{
+    std::vector<std::string> peers;
+
+    if (scm_is_string(peers_vector_or_str)) {
+        peers.emplace_back(scm_to_cxx_string(peers_vector_or_str));
+    } else {
+        AGENT_ASSERT(scm_is_simple_vector(peers_vector_or_str),
+                     "peers_vector_or_str must be a simple vector or a string");
+    }
+
+    for (size_t i = 0; i < SCM_SIMPLE_VECTOR_LENGTH(peers_vector_or_str); ++i) {
+        SCM peer_str = SCM_SIMPLE_VECTOR_REF(peers_vector_or_str, i);
+
+        peers.emplace_back(scm_to_cxx_string(peer_str));
+    }
+
+    Agent::instance().searchForPeers(peers);
+
+    return SCM_UNDEFINED;
+}
+
+static SCM
+ping_binding(SCM contact_str)
+{
+    return scm_from_bool(Agent::instance().ping(scm_to_cxx_string(contact_str)));
+}
+
+static SCM
+set_details_binding(SCM details_alist)
+{
+    std::map<std::string, std::string> details;
+
+    AGENT_ASSERT(scm_is_true(scm_list_p(details_alist)), "Bad format of details");
+
+    while (not scm_is_null(details_alist)) {
+        SCM detail_pair = scm_car(details_alist);
+
+        AGENT_ASSERT(scm_is_pair(detail_pair), "Detail must be a pair");
+
+        auto car = scm_to_cxx_string(scm_car(detail_pair));
+        auto cdr = scm_to_cxx_string(scm_cdr(detail_pair));
+
+        details[car] = cdr;
+
+        details_alist = scm_cdr(details_alist);
+    }
+
+    Agent::instance().setDetails(details);
+
+    return SCM_UNDEFINED;
+}
+
+static SCM
+ensure_account_binding()
+{
+    Agent::instance().ensureAccount();
+
+    return SCM_UNDEFINED;
+}
+
+static SCM
+export_to_archive_binding(SCM path)
+{
+    Agent::instance().exportToArchive(scm_to_cxx_string(path));
+
+    return SCM_UNDEFINED;
+}
+
+static SCM
+import_from_archive_binding(SCM path)
+{
+    Agent::instance().importFromArchive(scm_to_cxx_string(path));
+
+    return SCM_UNDEFINED;
+}
+
+static SCM
+enable_binding()
+{
+    Agent::instance().activate(true);
+
+    return SCM_UNDEFINED;
+}
+
+static SCM
+disable_binding()
+{
+    Agent::instance().activate(false);
+
+    return SCM_UNDEFINED;
+}
+
+/*
+ * Register Guile bindings here.
+ *
+ * 1. Name of the binding
+ * 2. Number of required argument to binding
+ * 3. Number of optional argument to binding
+ * 4. Number of rest argument to binding
+ * 5. Pointer to C function to call
+ *
+ * See info guile:
+ *
+ * Function: SCM scm_c_define_gsubr(const char *name, int req, int opt, int rst, fcn):
+ *
+ * Register a C procedure FCN as a “subr” — a primitive subroutine that can be
+ * called from Scheme.  It will be associated with the given NAME and bind it in
+ * the "current environment".  The arguments REQ, OPT and RST specify the number
+ * of required, optional and “rest” arguments respectively.  The total number of
+ * these arguments should match the actual number of arguments to FCN, but may
+ * not exceed 10.  The number of rest arguments should be 0 or 1.
+ * ‘scm_c_make_gsubr’ returns a value of type ‘SCM’ which is a “handle” for the
+ * procedure.
+ */
+void
+install_scheme_primitives()
+{
+    auto define_primitive = [](const char* name, int req, int opt, int rst, void* func) {
+        AGENT_ASSERT(req + opt + rst <= 10, "Primitive binding `%s` has too many argument", name);
+
+        AGENT_ASSERT(0 == rst or 1 == rst, "Rest argument for binding `%s` must be 0 or 1", name);
+
+        scm_c_define_gsubr(name, req, opt, rst, func);
+    };
+
+    define_primitive("agent:search-for-peers", 1, 0, 0, (void*) search_peer_binding);
+    define_primitive("agent:place-call", 1, 0, 0, (void*) place_call_binding);
+    define_primitive("agent:some-contact", 0, 0, 0, (void*) some_contact_binding);
+    define_primitive("agent:some-conversation", 0, 0, 0, (void*) some_conversation_binding);
+    define_primitive("agent:wait", 0, 1, 0, (void*) wait_binding);
+    define_primitive("agent:ping", 1, 0, 0, (void*) ping_binding);
+    define_primitive("agent:set-details", 1, 0, 0, (void*) set_details_binding);
+    define_primitive("agent:ensure-account", 0, 0, 0, (void*) ensure_account_binding);
+    define_primitive("agent->archive", 1, 0, 0, (void*) export_to_archive_binding);
+    define_primitive("archive->agent", 1, 0, 0, (void*) import_from_archive_binding);
+    define_primitive("agent:enable", 0, 0, 0, (void*)enable_binding);
+    define_primitive("agent:disable", 0, 0, 0, (void*)disable_binding);
+}
diff --git a/test/agent/log.h b/test/agent/bindings.h
similarity index 74%
rename from test/agent/log.h
rename to test/agent/bindings.h
index 75ffed23784728ddad4e4b8d392818baa98a87d0..85ac943267cda372e6b0f0e56cbd1fe79c7ffb48 100644
--- a/test/agent/log.h
+++ b/test/agent/bindings.h
@@ -20,19 +20,8 @@
 
 #pragma once
 
-#include <string>
+#include <libguile.h>
 
-class LogHandler
-{
-protected:
-    std::string context_;
+#define scm_to_cxx_string(VAR) (scm_to_cxx_string)(VAR, #VAR)
 
-public:
-    LogHandler(const std::string& context)
-        : context_(context)
-    {}
-    virtual ~LogHandler() = default;
-
-    virtual void pushMessage(const std::string& message) = 0;
-    virtual void flush() = 0;
-};
+extern void install_scheme_primitives();
diff --git a/test/agent/bt.cpp b/test/agent/bt.cpp
deleted file mode 100644
index c03abc48b3745fd3e87684d005f6ea04bc91c61a..0000000000000000000000000000000000000000
--- a/test/agent/bt.cpp
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- *  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
deleted file mode 100644
index 18537f88aad2e58db5269d6c17326558ab827345..0000000000000000000000000000000000000000
--- a/test/agent/bt.h
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- *  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/examples/active-agent.scm b/test/agent/examples/active-agent.scm
new file mode 100644
index 0000000000000000000000000000000000000000..84078dd3fc7f1511a988017348bbfb55bde8e193
--- /dev/null
+++ b/test/agent/examples/active-agent.scm
@@ -0,0 +1,98 @@
+;;; This is an example of an active agent.
+
+;;; Here we define a simple variable named `peer` that references a string.
+;;;
+;;; You should change this to the Jami ID of an other agent or one of your
+;;; account.
+(define my-peer "358d385fc78edde27981caac4ec40fb963c8a066")
+
+;;; Here we define a variable named `details-matrix` that references a list of
+;;; association lists.
+;;;
+;;; An association list is a list that has pairs of (key . value).
+(define details-matrix
+  '((("Account.upnpEnabled" . "true")
+     ("TURN.enable" . "true"))
+
+    (("Account.upnpEnabled" . "false")
+     ("TURN.enable" . "true"))
+
+    (("Account.upnpEnabled" . "false")
+     ("TURN.enable" . "false"))))
+
+(define (scenario/call:details someone)
+  "
+  pre-conditions: SOMEONE is a string that references a valid contact in the agent account.
+
+  Here we define a variable named `scenario/call` that references a procedure
+  that takes a single argument called `someone`.
+
+  This procedure iterates over the details matrix defined above and changes the
+  details of the account, using `agent:set-details` for every association list in
+  the matrix.  If it fails to place a call, then the `bad-call` exception is
+  thrown.  Finally, the agent sleeps for 10 seconds using `agent:wait`.
+  "
+  (for-each (lambda (details)
+              (agent:set-details details)
+              (or (agent:place-call someone)
+                  (throw 'bad-call)))
+            (agent:wait 10)
+            details-matrix))
+
+(define (scenario/call:periodic someone)
+  "
+  pre-conditions: SOMEONE is a string that references a valid contact in the agent account.
+
+  Place a call to SOMEONE periodically until it fails.
+
+  NOTE!  This is an example of a recursive procedure.  In Scheme, tail
+  recursions are free."
+  (when (agent:place-call someone)
+    (scenario/call:periodic someone)))
+
+(define (scenario/call:nth someone cnt)
+  "
+  pre-conditions: SOMEONE is a string that references a valid contact in the agent account.
+                   LEFT is a positive number.
+
+  Place a call to SOMEONE CNT time.
+  "
+  (let ((fail 0))
+    (do ((i 0 (1+ i)))
+        ((> i cnt))
+      (unless (agent:place-call someone)
+        (set! fail (1+ fail))))
+    fail)
+)
+
+(define (scenario/ping conversation)
+  "
+  pre-conditions: CONVERSATION is a string that references a valid conversation in the agent account.
+
+  Here we define a variable named `scenario/ping` that references a procedure
+  that takes a single argument called `conversation`.
+
+  This procedure work just like `scenario/call`, but will send a random
+  message to CONVERSATION and expect to receive a pong of it, instead of
+  placing a call.
+  "
+  (for-each (lambda (details)
+              (agent:set-details details)
+              (or (agent:ping conversation) (throw 'bad-ping))
+              (agent:wait 5))
+            details-matrix))
+
+;;; Set default account's details.
+;(agent:set-details '(("Account.upnpEnabled" . "true") ("TURN.enable" . "true")))
+
+;;; Search for our peer.
+;(agent:search-for-peers my-peer)
+
+;;; Let's call our peer 50 times.
+;(scenario/call:nth my-peer 50)
+
+;;; Wait for 100000 seconds.
+(agent:ensure-account)
+(agent:search-for-peers my-peer)
+
+(format #t "Failed ~a calls out of 1000" (scenario/call:nth my-peer 100))
diff --git a/test/agent/examples/passive-agent.scm b/test/agent/examples/passive-agent.scm
new file mode 100644
index 0000000000000000000000000000000000000000..e78246648927bd3ae1215378e4b356e9c0e6b271
--- /dev/null
+++ b/test/agent/examples/passive-agent.scm
@@ -0,0 +1,29 @@
+;;; This is an example of a passive agent
+;;;
+;;; It will accept all trust request and answer every call
+
+(define details-matrix
+  '((("Account.upnpEnabled" . "true")
+     ("TURN.enable" . "true"))
+
+    (("Account.upnpEnabled" . "false")
+     ("TURN.enable" . "true"))
+
+    (("Account.upnpEnabled" . "false")
+     ("TURN.enable" . "false"))))
+
+(define (scenario/passive)
+  "Agent does nothing"
+  (agent:wait))
+
+(define (scenario/passive:change-details)
+  "Agent changes its account details once in a while"
+  (for-each (lambda (details)
+              (agent:disable)
+              (agent:set-details details)
+              (agent:enable)
+              (agent:wait 15))
+            details-matrix))
+
+(agent:ensure-account)
+(scenario/passive)
diff --git a/test/agent/jami-agent-config.yml b/test/agent/jami-agent-config.yml
deleted file mode 100644
index 4dae9ae59e622f3f6d5d8a5b2c45d40e5ead2b2c..0000000000000000000000000000000000000000
--- a/test/agent/jami-agent-config.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-behavior:
-  - search-peer
-  - start-log-recording
-#  - : [ echo, true ]
-#  - : [ make-call, true ]
-  - stop-log-recording
-  - wait
-
-record-context: "My context: "
-record-to: "out.txt"
-
-peers:
-  - "your-peer-id"
-
-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
deleted file mode 100644
index 1c5ff51e8eda53957dfcc6a7a9a109fd61f450ea..0000000000000000000000000000000000000000
--- a/test/agent/jami-config.yml
+++ /dev/null
@@ -1,63 +0,0 @@
-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
index 665208729a1427e480e310a282ceaeb7cb32046d..b6844bbdd9904ae474648e33da6b20cd103143b3 100644
--- a/test/agent/main.cpp
+++ b/test/agent/main.cpp
@@ -18,19 +18,85 @@
  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
  */
 
+/* agent */
 #include "agent/agent.h"
+#include "agent/bindings.h"
 
 /* Jami */
 #include "jami.h"
 
+/* Third parties */
+#include <gnutls/gnutls.h>
+#include <libguile.h>
+
+/* std */
+#include <fstream>
+
+/* Guile helpers */
+
+static SCM
+main_body(void* path_raw)
+{
+    const char* path = (const char*)path_raw;
+
+    if (0 == strcmp("-", path)) {
+        scm_c_primitive_load("/dev/stdin");
+    } else {
+        scm_c_primitive_load(path);
+    }
+
+    return SCM_UNDEFINED;
+}
+
+static SCM
+main_catch(void* nil, SCM key_sym, SCM rest_lst)
+{
+    (void) nil;
+
+    SCM fmt_str = scm_from_utf8_string("Guile exception `~a`: ~a");
+    SCM args_lst = scm_list_2(key_sym, rest_lst);
+    SCM to_print_str = scm_simple_format(SCM_BOOL_F, fmt_str, args_lst);
+
+    char* to_print_raw = scm_to_locale_string(to_print_str);
+
+    AGENT_ERR("%s\n", to_print_raw);
+
+    free(to_print_raw);
+
+    return SCM_UNDEFINED;
+}
+
+void*
+main_inner(void* agent_config_raw) /* In Guile context */
+{
+    install_scheme_primitives();
+
+    Agent::instance().init();
+
+    scm_internal_catch(SCM_BOOL_T, main_body, agent_config_raw, main_catch, nullptr);
+
+    Agent::instance().fini();
+
+    return nullptr;
+}
+
 int
-main(void)
+main(int argc, char* argv[])
 {
+    if (argc < 2) {
+        printf("Usage: agent CONFIG\n");
+        exit(EXIT_FAILURE);
+    }
+
+    setenv("GUILE_LOAD_PATH", ".", 1);
+
+    /* NOTE!  It's very important to initialize the daemon before entering Guile!!! */
     DRing::init(DRing::InitFlag(DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_CONSOLE_LOG));
 
-    assert(DRing::start("jami-config.yml"));
+    AGENT_ASSERT(DRing::start(""), "Failed to start daemon");
 
-    Agent::run("jami-agent-config.yml");
+    /* Entering guile context */
+    scm_with_guile(main_inner, argv[1]);
 
     DRing::fini();
 }
diff --git a/test/agent/tools/make-account-archive b/test/agent/tools/make-account-archive
new file mode 100755
index 0000000000000000000000000000000000000000..178200d10d228865912fc501609ee42fb61479cb
--- /dev/null
+++ b/test/agent/tools/make-account-archive
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+tmp=$(mktemp)
+
+export XDG_CONFIG_HOME=$tmp
+export XDG_CACHE_HOME=$tmp
+export XDG_DATA_HOME=$tmp
+
+archive=$1
+
+./agent - <<EOF
+(agent:ensure-account)
+(agent->archive "${archive}")
+EOF
diff --git a/test/agent/utils.cpp b/test/agent/utils.cpp
deleted file mode 100644
index c656ca7d68202019c49f3c06af7b64ba5c44398b..0000000000000000000000000000000000000000
--- a/test/agent/utils.cpp
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- *  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 "jami.h"
-#include "jami/account_const.h"
-#include "jami/configurationmanager_interface.h"
-#include "jami/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
deleted file mode 100644
index 148fb4aa8295c3f1ab4118ac47fb644c4cf33931..0000000000000000000000000000000000000000
--- a/test/agent/utils.h
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  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));