a4aa — Adapter For All Agents
One crate owns every agent-client adaptation path a host needs. Any Rust
application can embed this library in-process; hosts in other languages
(Go, TypeScript, …) spawn the a4aa serve sidecar and get the identical
adapter behavior over stdio JSON-RPC.
Design premise: the native agent is maximal — A4AA translates what agents natively offer into a common language, bottom-up. See docs/design-direction.md.
What it provides
| Layer | Module | Responsibility |
|---|---|---|
| Supervision | supervision |
Process-group spawn (command-group: Unix pgid, Windows Job Object), sanitized environment, bounded pipes, interrupt (SIGINT → grace → kill) and tree teardown |
| Transport | transport |
Piped stdio, Unix PTY (raw slave, streaming master, resize) and loopback WebSocket (RFC 6455 client for agent-owned daemons, e.g. droid daemon --port <n> announced on stdout) |
| Framing | framing |
Bounded NDJSON frames (1 MiB default cap) |
| RPC | rpc |
Bidirectional JSON-RPC 2.0 peer: correlated outbound calls, peer-initiated requests dispatched per-task and notifications delivered in wire order, half-close and fail-closed pending calls |
| Lanes | proto |
acp (official agent-client-protocol 3.x), rpc (host-declared methods/params over stdio or an announced WebSocket daemon), cli (argv template or stdin prompt, stdio or PTY streaming), serve (the agent's own first-party HTTP server, e.g. opencode serve --port 0 — discovered on stdout, driven over loopback REST) |
| Facade | adapter |
Adapter / AgentSession lane-erased API: open_session, prompt, cancel, close, before_turn hook, permission handler |
| Sidecar | src/bin/a4aa.rs |
a4aa serve — a4aa.v1 NDJSON-RPC on its own stdio; a4aa fixture-agent — deterministic test agent |
| Mechanisms | mechanisms + dialect |
Semantic declarations (MechanismSet) and the descriptor-driven adaptation engine producing an AdaptationPlan of artifacts + support report |
Mechanism adaptation and per-agent crates
Beyond driving agents, a4aa adapts configuration: a MechanismSet
(hooks, rules, skills, commands, subagents, plugins, permissions, MCP
servers, native-profile selection) is translated into an agent-native
AdaptationPlan — the concrete config fragments to write plus a per-item
native | adapted | unsupported report.
Agent knowledge does not live in this crate. Each agent owns one small
descriptor crate under crates/, which depends only on a4aa:
# A host that drives only Pi installs exactly one adapter.
= "0.2"
let plan = adapt; // or a4aa::adapt(a4aa_pi::dialect(), &set)
| Crate | Agent knowledge it owns |
|---|---|
a4aa (this crate) |
Shared vocabulary only: MechanismSet, Dialect/HookSurface/surface types, the adapt engine, Dialect::generic() |
crates/a4aa-<agent> × 16 |
That agent's Dialect const: hook-store shape, skills/commands/subagents paths, permission/MCP config surfaces |
crates/a4aa-dialects |
Optional name→Dialect registry for multi-agent hosts. Pulls in all adapters; single-agent hosts should not use it |
Every adapter crate is independently installable and independently
maintained — a change to a4aa-codex's descriptor never touches
a4aa-pi's dependency graph. A multi-agent host picks exactly the adapter
crates it needs — or a4aa-dialects for runtime name lookup; no mandatory
registry stands between a host and a single agent adapter.
Each adapter crate also publishes a dated native-capability table
(NATIVE in crates/a4aa-<agent>/src/native.rs) covering interaction
surfaces, MCP registration, config locations, and AwP primitives.
docs/native-capability.md is the aggregated
master table. last_verified is a release gate
(python3 .github/release.py freshness).
Invariants
- Zero-assumption output — natural language is a valid turn result; no lane forces a schema onto agent output.
- Fail closed — unknown inbound requests are rejected; unresolved
EnvValue::Secretand a missing permission handler never grant. - Explicit cancellation only —
cancel()is the sole cancel path; observer drops or transport errors are never rewritten as user cancels. - Bounded resources — output caps drain-and-discard; a chatty agent can never exhaust host memory or stall its own pipes. Event channels are intentionally unbounded queues between lane and consumer: the consumer must drain them promptly, since the lane cannot apply backpressure to a host that stops reading.
Rust usage
use ;
use mpsc;
let spec = AgentSpec ;
let adapter = new;
let = ;
let session = adapter
.open_session
.await?;
let outcome = session
.prompt
.await?;
session.close.await?;
Sidecar usage (Go / TypeScript)
Spawn a4aa serve; speak NDJSON JSON-RPC a4aa.v1 on its stdio:
// → request
{"jsonrpc":"2.0","id":1,"method":"session/open","params":{"spec":{...},"options":{...}}}
// → request (returns when the turn ends)
{"jsonrpc":"2.0","id":2,"method":"session/prompt","params":{"sessionId":"...","text":"..."}}
// → request — cancel must be id-bearing, and the response must be checked:
// a notification-form cancel is ignored (with a stderr warning), because
// its outcome (cancelled, or unknown session) cannot be reported back
{"jsonrpc":"2.0","id":3,"method":"session/cancel","params":{"sessionId":"..."}}
// ← events stream back as notifications
{"jsonrpc":"2.0","method":"session/event","params":{"sessionId":"...","event":{...}}}
Permission requests are forwarded as server→client requests
(permission/request); the host answers with the selected option or lets it
fail closed to cancelled. The full method table is
docs/sidecar-protocol.md.
Verify
Status
The crate is named a4aa (Apache-2.0); the sidecar protocol is versioned
as a4aa.v1.
RPC lane output limits apply before notification delivery is queued. Slow event
callbacks do not stop response or permission processing, including an RPC call
made from a callback. Successful turns wait for their admitted callbacks, and
new turns drain earlier admitted events before resetting the output budget.
max_output_bytes: None explicitly permits unbounded output retention. Generic
JsonRpcPeer delegates can use prepare_notification for synchronous ingress
admission; its default retains all notifications without a queue budget.
RPC turns belong to the session driver until a terminal result arrives. Dropping
the future waiting for a turn does not cancel it, discard its cancellation target,
or admit a second overlapping turn. If a dispatched turn loses its outcome,
agent_session_recovery_required prevents another turn on that session; close
the session and reconcile the uncertain outcome before submitting more work.
Generated Pi/OpenCode-style extension hooks are observers: only Notify intent
is adapted. Enforce, Inject and Rewrite are reported as Unsupported and
produce no observer artifact pretending to implement those semantics.