Agent SDK for Go
Implemented (#67), released from v0.1.0 (pkg.go.dev). packages/agent-go (module
github.com/AimTune/converger/packages/agent-go, package agent) is the Go agent SDK: a bot backend answers
the conversations of a Converger channel over one channel-scoped Protocol v1 connection
(Protocol v1, sections 9 and 10).
Users on WhatsApp, Telegram or the web widget write to their channel, a routing rule
targets the agent's websocket channel, and the agent's replies are stored and routed back to each user's
channel (WebSocket channel type).
The .NET and Elixir SDKs have the same model and wire behaviour: .NET, Elixir.
Install
go get github.com/AimTune/converger/packages/agent-go
Go 1.22+, one dependency (github.com/coder/websocket). Releases are tags packages/agent-go/vX.Y.Z (Releasing the SDKs).
Token
The agent authenticates with a channel-scoped token of the agent's websocket channel:
curl -X POST https://chat.example.com/api/v1/converger/tokens/generate \
-H "Authorization: Bearer $BOT_CHANNEL_SECRET" -H "content-type: application/json" \
-d '{"scope": "channel"}'
It travels in the Authorization: Bearer header of the upgrade. Tokens expire (default 30 minutes): pass a
TokenSource that returns a fresh one on every connect, and the agent survives the expiry close (4401).
Usage
import agent "github.com/AimTune/converger/packages/agent-go"
a := agent.New(agent.SocketURL("https://chat.example.com"), fetchChannelToken,
agent.WithGroup("faq-bot"),
agent.OnError(func(err error) { log.Printf("agent: %v", err) }))
a.OnTurn(func(ctx context.Context, turn *agent.Turn, reply *agent.Reply) error {
if turn.Text == "refund" {
answer, err := reply.Ask(ctx, map[string]any{"title": "Refund 249.90 TRY?"},
agent.Action{Label: "Yes", Value: "yes"}, agent.Action{Label: "No", Value: "no"})
if err != nil {
return err
}
_, err = reply.Text(ctx, fmt.Sprintf("You answered %v.", answer))
return err
}
_ = reply.Typing(ctx, true)
_ = reply.ToolCall(ctx, agent.ToolCall{ID: "t1", Name: "search_faq", Status: "running"})
_, err := reply.Text(ctx, "You said: "+turn.Text, agent.Action{Label: "Talk to a human", Value: "/human"})
return err
})
if err := a.Run(ctx); err != nil { // nil after ctx is cancelled
log.Fatal(err) // *agent.CloseError: the server refused for good (4401 twice, 4403, 4400, 1008)
}
Turn
| field | |
|---|---|
ConversationID, ID, Seq | the conversation, the activity id and its seq |
Text | data.text ("" for messages without text) |
Message | Type, Data, Raw (the whole frame): images, files, locations, ... arrive typed (messages) |
Participant | the sender (ID, Role, Name) |
Meta | the turn's per-turn context (mekik/1 meta, for example the locale), never stored |
Metadata, Action | the stored activity metadata; Action is the tapped button (metadata.action) |
ReplyTo | {id, seq} of the activity the turn answers |
Reply
| method | frame | |
|---|---|---|
Text(ctx, text, actions...) | text with frame-level actions chips | waits for the ack, returns *Ack{ID, Seq, Duplicate} |
Message(ctx, type, data) | any message type (buttons, card, carousel, quick-reply, image, file, ...) | waits for the ack |
Ask(ctx, payload, actions...), AskWith(ctx, payload, AskOptions{Actions, UI}) | interrupt, then interrupt_resolved | returns the answer: the tapped action's value, else its label or the user's text |
ToolCall(ctx, ToolCall{...}) | tool_call | upserted by ID on the client: running, then completed or error |
Typing(ctx, on) | typing | dropped while disconnected |
Send(ctx, frame) | any agent frame (genui, skill, ...) | raw, no ack |
End() | run {status: "finished"} | sent for you when the handler returns |
An Ask is answered by the user's resume frame or by a turn that answers the interrupt (a tapped WhatsApp
button, a chativa chip): the server turns both into resume {answers: {id: value}} for the agent
(section 9). Interrupt ids are
ask/<random>#<n>, which deliberately do not have the clientId syntax: the server does not key the
interrupt and its interrupt_resolved by them.
Behaviour
- Runs. Each turn runs between transient
run {status: "started"}andrun {status: "finished"}frames, relayed to the user's connections; a handler error or panic sendsrun {status: "error"}and goes toOnError. - Ordering. Turns of one conversation run one at a time, in order; conversations run concurrently. While
an
Askwaits, the conversation's next turn stays queued; the answer itself is dispatched immediately. - Idempotent sends. Every message send has a generated
clientIdand waits for theack(default 30 s,WithAckTimeout). A refused send returns an*agent.Errorwith the server'sCode,Message,Retryable,RetryAfterandDetails(errors). After a reconnect, unacked sends are re-sent with the sameclientId, so the server stores each once; frames written while disconnected are sent after the nextwelcome, in order (typing excepted). - Resume. The agent tracks the last
seqseen per conversation and sends it ashello.watermarkson every reconnect: turns committed while it was away are replayed, duplicates dropped.Watermarks()andWithWatermarkscarry the position across restarts. - Reconnect. Jittered exponential backoff (500 ms to 30 s,
WithBackoff), or the server'sretryAfterMsclose reason (1012 draining, 4503). Close 4401 is retried once with a fresh token; 1008, 4400, 4403 and a second 4401 endRunwith a*agent.CloseErrorcarrying the lasterrorframe. - Groups.
WithGroup("name")joins an agent group (hello.group): several processes behind one channel share its conversations, each served by one of them. - Not turns. Frames from the bot side (
from: "bot": this agent's replies, other bots, human agents), typing, receipts and lifecycle events are not passed to the handler.
Planned: token streaming (genui text chunks) and a turn history accessor, as in the TypeScript and Python
SDKs (#67).
Tests and conformance
cd packages/agent-go && go vet ./... && go test -race -count=1 ./...
TestConformance plays every scenario of the shared fixture packages/conformance/agent.json (a text turn,
duplicates and bot frames, an ask with a queued second turn, rich replies, a refused send, a reconnect
with watermarks and a re-sent unacked reply) against an in-process fake server, and validates every frame
the agent sends against the Protocol v1 schemas (frames/channel/client-envelope.schema.json). The rules
are in packages/conformance/README.md. The SDK was also run end to end against a local server: a widget
user's turn reached the agent, and both its reply and an Ask answered with resume came back to the user.
examples/echo is the agent used for that check.