ACP Agent Transport
- Transport
- Client
- internal/acp/config.go
- ACPConfig
Relevant source files · 3
The internal/acp package lets astrolabe drive an external agent process — a program speaking the Agent Client Protocol (ACP), the JSON-RPC 2.0-over-stdio dialect used by agent frontends such as Zed — as if it were an LLM backend. Instead of (or alongside) a hosted model, astrolabe can spawn an agent declared in the target repository's astrolabe.yaml and push its plan/draft/review prompts through an initialize → session/new → session/prompt conversation. Only the client-side subset astrolabe needs is implemented; anything the agent asks that astrolabe doesn't understand is answered with a JSON-RPC error rather than silently ignored acp.go package doc.
How it works
Configuration and spawn. Before anything is sent, Load walks up from the directory being documented looking for astrolabe.yaml or .astrolabe.yaml (the names listed in ConfigNames), parses it, and defaults the agent's working directory to the config file's directory config.go Load. The ACP section of that file is an ACPConfig: the command to run, its args, extra environment variables, the CWD, a per-prompt timeout, and agent-specific session config entries ACPConfig struct. SpawnTransport then turns this into a live connection: it validates the config, builds an exec.Cmd with the merged environment and stderr wired to a log writer, and starts it, returning a stdioTransport that reads the child's stdout and writes its stdin SpawnTransport.
The Transport interface. Transport is deliberately tiny — just io.Reader, io.Writer, and io.Closer — carrying newline-delimited JSON-RPC messages between astrolabe and the agent Transport interface. This is what makes the package testable: a Client can run over any pipe, and the only production implementation is the spawned-process pipe above.
Client as an LLM backend. Client implements the chat interface defined in llm.Client interface over that transport. It is created with New(t Transport, opts ...Option), with options for the reported CWD, a per-chat timeout (default 10 minutes), a logger, and session/set_config_option entries applied after the session opens acp.go New and options. On the first prompt it lazily performs the handshake: initialize advertises protocol version 1 and the client capabilities (it will serve file reads, but no terminal or file writes) acp.go initialize, then session/new obtains a session ID and applies each configured option, logging failures rather than aborting since agents may not support every option acp.go newSession.
Each Chat call becomes one session/prompt round acp.go Chat. Prompts are serialized under a mutex — one Client is one agent process, one session — and the reply text is accumulated from agent_message_chunk notifications delivered during the round acp.go prompt. Timeouts are handled by a goroutine that closes the transport when the deadline fires; the transport is closed only on deadline expiry, never on caller cancellation or normal return, so the session survives across prompts acp.go timeout close. Errors caused by that deadline are rewritten by ctxErr into a clear "prompt timed out" message acp.go ctxErr.
The text tool-call shim. ACP agents have no function calling, but astrolabe's agent pipeline (see Agent Core) speaks native tool calls — the llm.ToolClient interface. ChatTools bridges the gap entirely in prompt text: it renders the tool catalogue into the prompt, instructs the agent to emit TOOL CALL: <name> {json} lines, and parses those lines back into structured llm.ToolCall values with synthetic IDs like acp-shim-3 acp.go ChatTools. When astrolabe executes a tool and sends the result back as a tool message, renderMessages turns it back into TOOL RESULT (<name>): ... text, resolving the synthetic ID to the real tool name via a map ChatTools maintains acp.go renderMessages. This shim is the single place the text protocol lives; the rest of the pipeline never sees it.
Server-initiated traffic. While a prompt is in flight, the agent may itself send requests. handleServerRequest answers fs/read_text_file from the local filesystem relative to the configured CWD, auto-approves session/request_permission by picking the first option (so unattended CI runs don't hang waiting for a human), and rejects everything else with method-not-found acp.go handleServerRequest. Notifications other than session/update are logged and ignored; session/update chunks feed the reply accumulator, and tool-call notifications are logged for observability acp.go handleNotification.
Relationships
flowchart LR
cfg["ACPConfig"] -->|"describes"| spawn["SpawnTransport"]
spawn -->|"produces"| tr["Transport"]
cfg -->|"parsed by"| load["Load"]
client["Client"] -->|"wraps"| tr
client -->|"implements"| llmclient["Client (internal/llm)"]
client -->|"shims for"| toolclient["ToolClient (internal/llm)"]
agent["agent.Agent"] -->|"uses as LLM backend"| clientThe ACPConfig struct and the loader in internal/acp/config.go are consumed by the pipeline entry point described in CLI & Pipeline Orchestration, which hands the resulting Client to the agent loop in Agent Core. The interfaces it satisfies come from the LLM Client Layer.
Reference
| Symbol | Kind | What it does | Location |
|---|---|---|---|
Transport |
interface | Carries newline-delimited JSON-RPC between astrolabe and the agent process (Reader/Writer/Closer) | internal/acp/acp.go#L38-L42 |
Client |
struct | Implements llm.Client/llm.ToolClient over ACP; serializes prompts, lazily initializes the session, shims tool calls in text |
internal/acp/acp.go#L55-L83 |
New |
function | Builds a Client over a transport with options (CWD, timeout, logger, config options) |
internal/acp/acp.go#L118-L133 |
ChatTools |
method | Text-protocol shim: renders tool catalogue, parses TOOL CALL: lines into llm.ToolCalls |
internal/acp/acp.go#L192-L250 |
handleServerRequest |
method | Serves fs/read_text_file, auto-approves permission requests, rejects unknown methods |
internal/acp/acp.go#L489-L521 |
handleNotification |
method | Routes session/update chunks to the reply accumulator; logs the rest |
internal/acp/acp.go#L537-L555 |
renderMessages |
function | Flattens llm.Messages into one tagged text block; renders TOOL CALL / TOOL RESULT lines |
internal/acp/acp.go#L555-L584 |
ACPConfig |
struct | astrolabe.yaml contract for the agent process: command, args, env, CWD, timeout, session config |
internal/acp/config.go#L24-L38 |
Load / Find |
functions | Locate and parse astrolabe.yaml walking up from the target directory |
internal/acp/config.go#L44-L81 |
SpawnTransport |
function | Validates config, spawns the agent process, returns its stdio as a Transport |
internal/acp/config.go#L97-L135 |
stdioTransport |
struct | Pipes newline-delimited JSON-RPC over the child process; Close closes pipes and waits |
internal/acp/config.go#L137-L150 |
Generated by astrolabe from commit ecc180b3 (tag 0.4.0).