How the ACP Agent Harness Connects Relay Events to AI Subprocesses in Buzz
The ACP harness in the buzz-acp crate spawns AI agents as isolated subprocesses, negotiates JSON-RPC capabilities, and bridges Nostr relay events to agent stdin/stdout using an NDJSON-framed protocol with observer hooks for real-time steering and usage tracking.
The ACP (AI Connector Protocol) agent harness is the critical infrastructure component in the block/buzz repository that transforms Nostr relay events into structured prompts for AI subprocesses. Implemented in Rust within the crates/buzz-acp directory, this harness manages the entire lifecycle of AI agent execution—from process spawning and protocol handshake to real-time message relay and graceful termination.
Architecture Overview: Three-Stage Integration
The harness operates through a strict three-stage pipeline defined in crates/buzz-acp/src/acp.rs. First, AcpClient::spawn launches the agent binary as a child process with captured stdin/stdout. Second, the initialize method negotiates protocol version and capability extensions. Third, the pool component (src/pool.rs) creates sessions and forwards relay events while the observer pattern handles asynchronous notifications.
This architecture cleanly separates network I/O (relay communication), protocol handling (JSON-RPC framing), and process management (subprocess lifecycle), allowing any AI binary that speaks the ACP schema to integrate without modification.
Spawning the AI Subprocess with Process Isolation
The harness launches AI agents using AcpClient::spawn, defined in src/acp.rs#L54-L78. This method executes the agent binary (e.g., "claude-agent-acp") as a child process in its own process group, enabling the harness to terminate the entire process tree during cleanup.
let client = AcpClient::spawn(
command, // e.g. "claude-agent-acp"
&args,
&extra_env,
has_generated_codex_config,
).await?;
The harness wraps the child’s stdout in a FramedRead using LinesCodec::new_with_max_length(MAX_LINE_SIZE), where MAX_LINE_SIZE is set to 10 MiB. This hard limit prevents rogue agents from exhausting memory via infinite line output. The stdin/stdout streams are captured immediately, establishing the NDJSON communication channel.
Protocol Initialization and Capability Discovery
After spawning, the harness negotiates the ACP protocol via the initialize method (src/acp.rs#L112-L122). The harness sends a JSON-RPC initialize request specifying protocol version 2, and the agent responds with capability metadata.
let init_resp = client.initialize().await?;
// The response may contain:
// "_meta.steering.supported": true/false
// "clientCapabilities": { … }
The harness specifically inspects _meta.steering.supported to determine if the agent accepts mid-turn steering commands. This capability flag is stored internally and used later when injecting guidance via the _session/steering extension method (ACP_STEER_METHOD).
Bridging Relay Events to Agent Sessions
The relay-to-agent message loop represents the core connection logic. The pool component in src/pool.rs receives Nostr events from the relay and creates ACP sessions via session_new, which returns a unique sessionId identifying the logical conversation.
let sess = client.session_new(
cwd, // absolute working directory
mcp_servers, // optional MCP servers
Some(SystemPromptTransport::Field("You are a helpful assistant")),
Some("Chat Session"), // optional session title
).await?;
When a relay event contains prompt text, the pool invokes session_prompt_with_idle_timeout ([src/acp.rs](https://github.com/block/buzz/blob/main/crates/buzz-acp/src/acp.rs)), writing a JSON-RPC session/prompt request to the child’s stdin:
let stop = client.session_prompt_with_idle_timeout(
&sess,
"Explain the difference between Nostr and HTTP.",
Duration::from_secs(30), // idle timeout
Duration::from_secs(300), // hard max turn duration
).await?;
The harness blocks on read_until_response_with_idle_timeout, which simultaneously drains stdout lines, respects both idle and hard deadlines, and intercepts asynchronous notifications like session/update.
Notifications, Steering, and Usage Tracking
The harness installs an observer via set_observer (src/acp.rs#L90-L103) to capture semantic events. Every read from the agent emits an "acp_read" event; every write emits "acp_write". These events feed into UsageTracker implementations defined in src/usage.rs, supporting both goose_usage (legacy) and standard_usage adapters.
Permission handling occurs when the agent emits session/request_permission notifications. The harness stores these as pending_permission_id values and can automatically respond with "allow_once" or "cancelled" before the turn completes.
Steering leverages the _meta.steering.supported capability discovered during initialization. When enabled, the harness can inject mid-turn guidance messages to redirect the agent’s reasoning without terminating the session.
Graceful Shutdown and Process Cleanup
Termination is handled by the shutdown method, which kills the entire process group and waits up to 5 seconds for the child to exit. This prevents zombie processes and ensures that transient subprocesses (spawned by the AI agent) are properly reaped.
client.shutdown().await;
Summary
- The ACP harness lives in
crates/buzz-acpand manages AI agent lifecycles in the Buzz ecosystem. - Process isolation uses process groups and a 10 MiB line limit via
LinesCodecto prevent memory exhaustion. - Protocol negotiation discovers capabilities like steering support through JSON-RPC initialization.
- Relay integration occurs via the pool component, which creates sessions and forwards prompts using
session_prompt_with_idle_timeout. - Observer hooks emit
acp_readandacp_writeevents for usage tracking and UI updates. - Graceful shutdown terminates the process tree with a 5-second timeout to prevent resource leaks.
Frequently Asked Questions
What is the ACP harness in Buzz?
The ACP (AI Connector Protocol) harness is the Rust-based infrastructure in the buzz-acp crate that connects Nostr relay events to AI agent subprocesses. It handles process spawning, JSON-RPC communication, session management, and real-time event streaming between the decentralized relay network and local AI binaries.
How does the harness prevent memory overflow from malicious agents?
The harness enforces a 10 MiB line size limit using LinesCodec::new_with_max_length(MAX_LINE_SIZE) when framing stdout from the agent subprocess. If an agent attempts to write a line exceeding this limit, the codec fails and the harness can terminate the connection, preventing memory exhaustion attacks via infinite output streams.
What is the steering extension in the ACP protocol?
The steering extension is an optional capability advertised by agents during initialization via _meta.steering.supported. When enabled, the harness can send _session/steering JSON-RPC messages mid-turn to guide the agent’s reasoning process without canceling the current operation, enabling real-time interaction adjustments based on relay-side logic.
How does the harness handle AI agent timeouts?
The harness implements dual-timeout protection in session_prompt_with_idle_timeout. An idle timeout (e.g., 30 seconds) triggers if the agent produces no output, while a hard maximum duration (e.g., 300 seconds) enforces an absolute ceiling on turn execution. Both timeouts are monitored during the read_until_response_with_idle_timeout loop, ensuring stuck or slow agents cannot block the relay message pipeline indefinitely.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →