Skip to main content

Agent SDK for Elixir

Implemented (#67), published on Hex from 0.1.0. packages/agent-elixir is the converger_agent package (Mint and Mint.WebSocket, Jason). It has the same model and wire behaviour as the Go agent SDK: one channel-scoped Protocol v1 connection (section 10) serves every conversation routed to the agent's websocket channel.

Install​

{:converger_agent, "~> 0.1"}

Usage​

defmodule MyBot do
use ConvergerAgent
alias ConvergerAgent.Reply

@impl true
def handle_turn(%{text: "refund"}, reply) do
with {:ok, answer} <-
Reply.ask(reply, %{"title" => "Refund 249.90 TRY?"},
actions: [%{label: "Yes", value: "yes"}, %{label: "No", value: "no"}]
) do
Reply.text(reply, "You answered #{answer}.")
end
end

def handle_turn(turn, reply) do
Reply.typing(reply, true)
Reply.text(reply, "You said: " <> turn.text, actions: [%{label: "Talk to a human", value: "/human"}])
end

@impl true
def handle_error(error), do: Logger.warning(Exception.message(error))
end

children = [
{MyBot,
url: ConvergerAgent.socket_url("https://chat.example.com"),
token: fn -> MyApp.channel_token() end,
group: "faq-bot"}
]

The token is a channel-scoped token of the agent's websocket channel (POST /api/v1/converger/tokens/generate with {"scope": "channel"}); a function or {m, f, a} is called on every connect. The functional form is ConvergerAgent.start_link(handler: fn turn, reply -> ... end, url: ..., token: ...).

API
options:url, :token, :handler, :on_error, :group, :user_id, :watermarks, :backoff_initial / :backoff_max (ms, 500 / 30 000), :ack_timeout (30 000), :headers, :name
ConvergerAgent.watermarks(agent), ConvergerAgent.stop(agent)last seq seen per conversation; graceful stop
%ConvergerAgent.Turn{}conversation_id, id, seq, text, message (type, data, raw), participant, meta, metadata, action, reply_to, timestamp
Reply.text(reply, text, actions: [...]), Reply.message(reply, type, data)wait for the ack: {:ok, %Ack{}}, {:error, %ConvergerAgent.Error{}} or {:error, :timeout}
Reply.ask(reply, payload, actions: ..., ui: ..., timeout: ...)interrupt, the user's answer, interrupt_resolved; {:ok, answer}
Reply.tool_call/2, Reply.typing/2, Reply.send_frame/2, Reply.end_run/1tool trace, typing, raw frame, run finished now

handle_turn/2 returning {:error, reason}, raising, throwing or exiting ends the run with run {status: "error"} and calls handle_error/1. Each turn runs in a task under the agent's Task.Supervisor; the connection process keeps the per-conversation queues.

Behaviour​

Identical to the Go SDK: runs framed by run started / finished / error, turns of one conversation serialised (an ask answer is never queued), acked message sends re-sent with their clientId after a reconnect, jittered backoff honouring retryAfterMs, hello.watermarks resume with duplicates dropped, agent groups, and frames from the bot side never treated as turns. On 1008, 4400, 4403 or a second consecutive 4401 the connection process calls handle_error(%ConvergerAgent.CloseError{}) and stops with {:shutdown, {:closed, code, last_error}}; its child spec is restart: :transient, so a supervisor does not restart it.

Tests and conformance​

cd packages/agent-elixir && mix deps.get && mix test --warnings-as-errors

One ExUnit test per scenario of packages/conformance/agent.json runs against a Bandit + WebSock fake server, and every agent frame is validated against the Protocol v1 schemas with JSV. The SDK was also run against a local server: a widget user's turn and an ask answered with resume round-tripped.