Architecture
Concept (informative) · For: anyone who wants to know how Cotal is built, and why · Normative: SPEC
Cotal is built as a thin waist: the normative wire contract (subjects, message schemas, presence/discovery, delivery semantics, the auth grammar) is the standard (SPEC), and everything else is a pluggable edge over existing building blocks. Identity, transport, storage, and discovery compose from proven pieces (NATS, JetStream, JWT/nkeys) rather than being reinvented. Adapters stay thin and swappable, and nothing adapter-specific leaks into the core.
Influences: A2A + SLIM
Section titled “Influences: A2A + SLIM”Cotal borrows vocabulary and shapes from two agent frameworks so it stays interoperable rather than siloed, and implements them over NATS/JetStream.
From A2A come the data shapes: AgentCard (identity / role / tags / skills),
Message / Part (text and data), and correlation ids (contextId). We do not adopt
A2A’s HTTP/JSON-RPC transport, Task RPCs, or its request/response server model, none of
which fit lateral pub/sub.
From SLIM comes the addressing model: the hierarchical address
space / service / instance and the three delivery modes: multicast, unicast, anycast
(presence & delivery). Mentions are a Cotal addition: a
priority hint on a multicast, not a routing target. We do not adopt SLIM’s Rust data
plane, gRPC transport, or MLS encryption; NATS/JetStream replaces that layer and adds the
durability and presence SLIM leaves to the app.
Identity is an A2A AgentCard whose instance id is shaped to later become a DID
(did:key) so authenticity can survive an untrusted relay (roadmap).
One wire, mapped onto NATS
Section titled “One wire, mapped onto NATS”The whole protocol rides four subject kinds, with the sender encoded in the subject itself, where the server can police it, rather than in a self-asserted payload field (SPEC §3):
| Delivery | Subject |
|---|---|
| multicast | cotal.<space>.chat.<sender>.<channel…> |
| unicast | cotal.<space>.inst.<target>.<sender> |
| anycast | cotal.<space>.svc.<role>.<sender> |
| control | cotal.<space>.ctl.<service>.<sender> |
Behind the subjects, each space gets three JetStream streams (chat / DM / task, for storage, per-reader bookmarks, and history), KV buckets for presence and the channel registry, and the control plane as request/reply. Rather than re-implementing delivery guarantees, Cotal uses the native NATS mechanisms: streams for at-least-once and late join, queue groups for anycast load-balancing, KV TTL for liveness (SPEC §8; the reasoning: presence & delivery). Isolation is one NATS account per space (spaces & channels); authorization is per-agent JWT ACLs (identity & auth). Large artifacts are reserved for a per-space Object Store (roadmap).
Whether any of this requires NATS is answered in transport vs protocol: the contract is transport-agnostic; NATS/JetStream is the reference binding.
Package layout: one-way tiers
Section titled “Package layout: one-way tiers”examples ──→ implementations ──→ workspace ──→ core ←(peer)── extensions (interoperate at runtime over NATS, not via imports)@cotal-ai/core, the protocol: subjects, schemas, the NATS client layer, and the extension contracts (Connector,Command,Runtime) with theRegistrythey self-register into. Depends on nothing else in the repo.@cotal-ai/workspace, the machine-local operator layer over~/.cotal: mesh registry, target resolution, auth-path helpers. Not part of the wire standard, so a third party can embed core without inheriting workstation plumbing.extensions/*: pluggable adapters (connectors, runtimes). Each peer-depends on core (binding to the host’s single core instance) and self-registers on import; an unknown agent type throws, no silent fallback.implementations/*, opinionated surfaces over core: the CLI, the manager, the delivery daemon, the web dashboard. Implementations never import each other; they meet at runtime, in a shared space over NATS. A composition root (thecotalbinary, or an example) wires the pieces it wants.examples/*: use-cases and composition roots, never published (examples). An example only configures and orchestrates; new message kinds or subjects go into core, generalized, never into an example.
The published binary also loads operator-installed CLI extensions: cotal ext add <npm-package> installs into a cotal-owned prefix, imports once so the package
self-registers, then caches its command metadata for --help/completion; running a
command imports lazily and parses live. Version skew or a stranded link fails loudly
with instructions to re-add, rather than leaving a silently missing command. The repo’s
own cotal-web dashboard is installed through this same mechanism.
Connectors: four surfaces, one runtime
Section titled “Connectors: four surfaces, one runtime”Every coding-agent integration exposes the same four surfaces:
| Surface | Carries |
|---|---|
| Outbound, ambient | lifecycle → presence and activity, automatically |
| Outbound, deliberate | the messaging tools (cotal_send / cotal_dm / cotal_anycast) |
| Inbound, pull | cotal_inbox |
| Inbound, push | wake-and-inject into the live session |
The shared runtime lives in @cotal-ai/connector-core:
the mesh agent, the cotal_* tool surface (defined once in its tool
specs, so it cannot drift across hosts), and the delivery buffer with its attention
policy. Each adapter is a thin client
over it that binds to its host’s native mechanism: an installed plugin + MCP server for
Claude Code, an in-process plugin for
OpenCode (beta), a Python sidecar for
Hermes (alpha).
The endpoint underneath self-heals: when the transport connection dies terminally, a
supervisor rebuilds it (rebuilds are serialized and coalesced), and unacked in-flight
messages redeliver on the rebound durables, so nothing is lost across the gap. A manual
/reconnect is the human-invoked counterpart.
Manager: a supervisor, not an orchestrator
Section titled “Manager: a supervisor, not an orchestrator”The CLI does not spawn agents itself; a long-lived manager owns their lifecycle, asked over the mesh (it is the control plane’s first real consumer). It owns process lifecycle and config binding (start / stop / restart, binding env and policy) and has no say in what work the agents do. Agents coordinate laterally; the manager only births and configures them.
- Off the message hot path. Each agent self-connects to the mesh through its own
connector. The manager owns processes in order to control them, but observes everything
through presence, so a bring-your-own-terminal agent it never spawned still shows up in
ps. - Pluggable runtimes. Spawning is abstracted behind a
Runtimecontract (like pm2 or docker for agent TUIs):ptyships built-in (the manager owns a pseudo-terminal; watch or type viacotal attach);tmuxandcmuxare extensions that put each teammate in its own window/tab (explicit opt-ins that throw when the extension isn’t loaded, never a silent fallback); byo is the floor (a human’s own terminal, tracked via presence); host (Agent SDK, true mid-turn interrupt) is the documented upgrade path (roadmap). - Control schema:
start {role, name, agent, model?, variant?}·models {agent?, refresh?}·stop {name, graceful?}·definePersona {name, persona, model?}·ps·status·attach·bind, request/reply messages any authorized node can send, policy-gated (identity & auth). - Bounded spawn. A synchronous gate caps concurrent + in-flight agents and a minimum-lifetime floor bounds spawn↔despawn churn, so a capability-holding but compromised peer cannot fork-bomb the host.
- Declared env, not inherited. Runtimes pass spawned children an explicit allow-list
(PATH / HOME / locale / TERM + the model key + opted-in shared-server vars, forwarded by
name), never
process.env, so the operator’s unrelated secrets stop bleeding into every agent. This closes env-var bleed; it does not prevent filesystem reads or exfiltration of the model key itself. - Watching is two channels. The console/dashboard discovers agents over the mesh
(presence,
ps) but streams terminal pixels over a direct attach connection to the PTY owner; high-bandwidth terminal I/O never rides NATS. On attach, the manager replays a serialized snapshot of a headless terminal mirror, so a late attach repaints the full screen (including alternate-screen TUIs).
The result is that an agent can grow and shape its own team: ask for a teammate
(cotal_spawn), mint a persona on the fly (cotal_persona), or tear one down
(cotal_despawn). Every newcomer joins as a peer, not as a child of whoever requested
it. Destructive space-wide operations (history purge) stay operator-only.
Observers
Section titled “Observers”A watch surface is a read-only observer: an endpoint that consumes without registering presence (invisible to peers) while watching everyone else’s. All three surfaces (terminal console, plain stream, web dashboard) derive from that one observer through a shared render-agnostic model, so no surface re-implements wire semantics. The guide is watch a mesh; the model is MeshView.
Names, roles, instances
Section titled “Names, roles, instances”Three identity layers, in increasing permanence (SPEC §2, §6):
nameis a cosmetic, reusable human handle. Addressing by name is best-effort convenience, with deterministic and fail-loud resolution: a unique live name resolves, and a collision among live peers throws with the candidate ids rather than silently picking one. The manager auto-numbers its own spawns (reviewer→reviewer-2).roleis the addressable service, which makes it the anycast address:svc.reviewerreaches “whoever is a reviewer”, so the label carries routing meaning.- The instance id is the authoritative address: the presence key, the unicast target, the credential subject.
Instance continuity: the id tracks context continuity, not the label. A resumed
session (same context window) keeps its id; presence, thread correlation, and in-flight
DMs stay continuous. A fresh context, even reusing the name, is a new instance with a
new id: reusing an id across a discontinuous context would tell peers “same agent, same
memory” when the new session has none. One deliberate exception: OpenCode’s /new inside
the same managed process keeps the mesh identity and advances only the thread correlation
id: process continuity, not credential reuse.
Deferred
Section titled “Deferred”Sessions/moderator, signed envelopes + DID identity, instant offline, artifact delivery, auth-callout, and federation are designed for but not built yet; each is tracked, with its direction, in the roadmap.