# 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](docs/design-direction.md).
## What it provides
| 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.
Hook installation is owned by the Agent Hook Engine crate
(`ahe::install`, re-exported as `a4aa::hooks`): the `HookDecl`/`HookEvent`
declaration contract, the `HookSurface` store descriptors, the lowering
engine and the `a4aa::hooks::harnesses` per-agent constants all live
there. `Dialect::adapt` projects the returned `InstallationPlan` into the
`AdaptationPlan` unchanged — artifacts stay user-home relative.
Agent token counters and vendor-shaped usage normalization are provided by token-usage-detector from Agent-Statistics. A4AA acquires protocol records and forwards the reported counters; it does not aggregate charges or own a usage ledger.
Produced-artifact operators are owned by the Agent Context Operators
crate (re-exported as `a4aa::context`): the canonical conversation model,
the `Codec` contract and capability matrix, and the search / export /
transform / import / edit / snapshot engines over session stores. The
per-agent `acop-<agent>` codec crates live in that repository and project
each `a4aa-<agent>` card's `history` roots into ACO's `HistoryRoot`
vocabulary — the card stays the single authority for store locations.
Agent knowledge does not live in this crate. Each agent owns one small
descriptor crate under `crates/`, which depends only on `a4aa`:
```toml
# A host that drives only Pi installs exactly one adapter.
a4aa-pi = "0.5"
```
```rust
let plan = a4aa_pi::adapt(&mechanisms); // or a4aa::adapt(a4aa_pi::dialect(), &set)
```
| `a4aa` (this crate) | Shared vocabulary only: `MechanismSet`, `Dialect`/surface types, the `adapt` engine, `Dialect::generic()`; hook surfaces and the hook-installation engine come from `a4aa::hooks` (`ahe::install`) |
| `crates/a4aa-<agent>` | That agent's `Dialect` const: skills/commands/subagents paths, permission/MCP config surfaces, and a reference to its AHE-owned hook descriptor |
| `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`](docs/native-capability.md) is the aggregated
master table. `last_verified` is a freshness gate enforced by release
tooling before a release ships.
## 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::Secret` and 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
```rust
use a4aa::{Adapter, AgentEvent, AgentSpec, CliLaneSpec, CommandSpec,
LaneSpec, SessionRequest, TurnRequest};
use tokio::sync::mpsc;
let spec = AgentSpec {
id: "my-agent".into(),
client_name: None,
lane: LaneSpec::Cli(Box::new(CliLaneSpec {
args: vec!["{prompt}".into()],
..Default::default()
})),
command: CommandSpec {
program: "my-agent-cli".into(),
..Default::default()
},
max_output_bytes: None,
seatbelted_permission: None,
};
let adapter = Adapter::new(spec);
let (events, mut rx) = mpsc::unbounded_channel::<AgentEvent>();
let session = adapter
.open_session(SessionRequest::default(), events)
.await?;
let outcome = session
.prompt(TurnRequest {
text: "hello".into(),
..Default::default()
})
.await?;
session.close().await?;
```
## Sidecar usage (Go / TypeScript)
Spawn `a4aa serve`; speak NDJSON JSON-RPC `a4aa.v1` on its stdio:
```jsonc
// → 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](docs/sidecar-protocol.md).
## Verify
```sh
scripts/verify.sh # fmt + clippy + cargo test --all-targets
```
The workspace builds against three sibling checkouts — Cargo path
dependencies resolve to `../Hook-Engine` (the
`Unka-Malloc/Agent-Hook-Engine` repository, providing `ahe`), `../ACOP`
(the `Unka-Malloc/Agent-Context-Operators` repository, providing `acop`)
and `../Metera` (the `Unka-Malloc/Metera` repository, providing
`token-usage-detector`)
at the versions `Cargo.toml` declares. Clone them beside this checkout
before building.
## 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.