a4aa 0.4.2

Adapter-For-All-Agents — unified agent client adapter: process supervision, stdio/PTY transports, JSON-RPC and ACP lanes, CLI drivers, and a stdio sidecar for hosts in other languages.
Documentation

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.

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:

# A host that drives only Pi installs exactly one adapter.
a4aa-pi = "0.2"
let plan = a4aa_pi::adapt(&mechanisms); // or a4aa::adapt(a4aa_pi::dialect(), &set)
Crate Agent knowledge it owns
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 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::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

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,
};
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:

// → 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

scripts/verify.sh   # fmt + clippy + cargo test --all-targets

The workspace builds against two sibling checkouts — Cargo path dependencies resolve to ../Hook-Engine (the Unka-Malloc/Agent-Hook-Engine repository, providing ahe) 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.