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:
- Compression check. The configured
Compressorgets a chance to rewrite the session before the turn runs. When it does,Request.CacheableMessagesis set so the summary block is cached on the very next turn. - Skills appendix. If a
SkillsProvideris configured, it inspects the last user message and returns text to splice into the system prompt. - Recall appendix. If a
RecallProvideris configured, it queries the FTS5 index across prior sessions and returns text to splice. - Provider call. The
Provider.Completeimplementation returns aResponsewith aStopReason. - Tool-use dispatch. If
StopReason == StopToolUse, each requested tool call is sent to theApprover. Denials becometool_resulterrors so the model can adapt. Allowed calls are executed against theRegistryand their outputs replayed on the next iteration. - End of turn. Loop until
StopReason == StopEndTurnorMaxIterationsis 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
RecallProviderperforms 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
- Configuration reference — every field.
- Agent-loop reference — library-embedding contract.
- MCP — client wire-up.