?
Turn dispatch flow A message enters at the transport, is routed to the Agent, which calls the Provider, potentially issues tool calls, and returns a reply. Transport slack · whatsapp · … Router allowlist · session Agent loop Session · Turn Provider claudecli · anthropic Tool call read · edit · bash Session store SQLite + FTS5 Reply to transport
A user message travels through the transport, agent loop, provider, tool, and back — animated at 6 s / cycle.

Layered architecture

+---------------------------------------------------------------+
|                             CLI                              |
|  chat  whatsapp  slack  discord  ...  mcp  cron  skills      |
+-------------------------+-------------------------------------+
                          |
+-------------------------v-------------------------------------+
|                          Router                              |
|          (per-JID session, allowlist, dispatch)              |
+-------------+---------------------------+---------------------+
              |                           |
     Transport interface           agent.Agent
     Start / Stop / Deliver        Turn / TurnStream
              |                           |
   +----------+----------+       +--------+--------+
   | 9 concrete adapters |       | Provider iface  |
   +---------------------+       | 5 concrete impls|
                                 +--------+--------+
                                          |
                                 +--------v--------+
                                 | Tools Registry  |
                                 | read/write/edit |
                                 | grep/bash + ext |
                                 +--------+--------+
                                          |
                                 +--------v--------+
                                 |  State (SQLite) |
                                 | sessions, cron, |
                                 | jidmap, FTS5    |
                                 +-----------------+

The agent package depends only on interfaces exposed by tools, on its own Provider types, and on the standard library. Concrete providers, stores, and transports depend on agent — never the reverse.

The agent loop

Session → Turn → Provider → tool-use round trip. Every user message becomes a call to Agent.Turn:

  1. Compression check. The configured Compressor gets a chance to rewrite the session before the turn runs. When it does, Request.CacheableMessages is set so the summary block is cached on the very next turn.
  2. Skills appendix. If a SkillsProvider is configured, it inspects the last user message and returns text to splice into the system prompt.
  3. Recall appendix. If a RecallProvider is configured, it queries the FTS5 index across prior sessions and returns text to splice.
  4. Provider call. The Provider.Complete implementation returns a Response with a StopReason.
  5. Tool-use dispatch. If StopReason == StopToolUse, each requested tool call is sent to the Approver. Denials become tool_result errors so the model can adapt. Allowed calls are executed against the Registry and their outputs replayed on the next iteration.
  6. End of turn. Loop until StopReason == StopEndTurn or MaxIterations is reached (default 32).

internal/agent/agent.go is the canonical reference.

Transports

Every transport implements transport.Transport:

type Transport interface {
    Name() string
    Start(ctx context.Context, handler Handler) error
    Stop() error
}

Handler.Handle receives an IncomingMessage (From, Body, At) and returns the reply text. The Router sits above the transport and is responsible for per-sender session isolation, allowlist enforcement, and dispatching to the Agent.

None of the shipped transports expose a public HTTP surface by default. Slack uses Socket Mode (outbound WebSocket). Discord uses the Gateway (outbound WebSocket). Signal is a subprocess. WhatsApp is Meta's Web protocol over TCP. Matrix, Telegram, iMessage, and email use polling. SMS is send-only because the inbound side would require a webhook.

Tool registry

internal/tools defines the Tool interface and a concurrency-safe Registry. Built-in tools live in internal/tools/builtin/:

  • read — file read.
  • write — file write.
  • edit — string replace with unique-match enforcement to prevent accidental mass-replacement.
  • grep — text search.
  • bash — command execution. The load-bearing security boundary.

Every tool declares a strict JSON schema. Adding a tool is a single registry.MustRegister(myTool) call at wire-up; the agent core does not change.

Approval policies

Every tool call goes through Approver.Approve before execution. Three built-in policies live in internal/agent/approver.go:

Mode Behaviour
allow_all Every call runs. Sensible with the claudecli provider, which handles its own approvals.
deny_all Every call is blocked. Useful for smoke tests and read-only sessions.
pattern Regex allow / deny rules per tool. Deny wins over allow. Unmatched requests fall back to Default (allow or deny).

Deny reasons are surfaced back to the model as tool_result errors, so the model gets a chance to adapt instead of failing silently.

Session store

internal/state/sqlite/ implements the state.Store interface on modernc.org/sqlite — pure Go, no libc, no CGo. Features:

  • WAL journaling with busy_timeout=15s.
  • WAL checkpoint on Close so the primary database file stays consistent for backups.
  • FTS5 recall table indexes every message; the RecallProvider performs cross-session lookups.
  • JID map table normalises WhatsApp LID identities to phone JIDs.
  • Cron table persists scheduled jobs across restarts.

MCP server

internal/mcp/server.go is a JSON-RPC 2.0 server over stdio, spec revision 2024-11-05. rousseau mcp starts it. Register tools with server.Register(mcp.ToolSpec{...}) and let a client (Claude Desktop, an IDE extension, another agent) drive them.

Tool failures are surfaced through the content channel with isError=true, not the JSON-RPC error channel — this is what MCP hosts expect.

Cron scheduler

internal/cron/scheduler.go wraps robfig/cron/v3. Jobs are stored in SQLite so they survive restarts. Each fire calls Runner.RunOnce(ctx, prompt) (a one-shot agent turn against a fresh session), then hands the reply to Delivery — a transport-agnostic function that ships the message.

New jobs added via rousseau cron add become live within the next PollInterval (default 60s).

Skills loader

internal/skills/skills.go scans skills_dir for *.md files. Each file may carry YAML front-matter declaring name, description, and triggers. When any trigger appears in the current user message, the skill body is spliced into the system prompt for that turn. Format is deliberately close to the agentskills.io convention.

Compression

internal/agent/compressor.go runs LLM-backed summarisation once the session crosses TriggerMessages (default 60). The most recent KeepRecent messages (default 8) survive verbatim; everything older collapses into a single summary block. Disabled by default because a subscription-tier claudecli account rarely needs it; turn it on when running against pay-per-token providers.

Where to go next

Type to search 150+ pages. Ranking: BM25 with title/description boost.