zeph-acp
ACP (Agent Client Protocol) server adapter for embedding Zeph in IDE environments.
Overview
Implements the Agent Client Protocol server side, allowing IDEs and editors to drive the Zeph agent loop over stdio, HTTP+SSE, or WebSocket transports. The crate wires IDE-proxied capabilities — file system access, terminal execution, and permission gates — into the agent loop via AcpContext, exposes AgentSpawner as the integration point for the host application, and supports runtime model switching via ProviderFactory and MCP server management via ext_method. Built on the agent-client-protocol SDK v0.10.
Installation
[]
= "0.15.2"
# With HTTP+SSE transport
= { = "0.15.2", = ["acp-http"] }
[!IMPORTANT] Requires Rust 1.88 or later.
Features
| Feature | Description | Default |
|---|---|---|
acp-http |
HTTP+SSE transport via axum (AcpHttpState, acp_router, post_handler, get_handler) |
No |
[!TIP] Enable
acp-httponly when deploying Zeph as a network-accessible ACP endpoint. The default stdio transport is sufficient for local IDE integrations.
Key modules
| Module | Description |
|---|---|
agent |
AcpContext — IDE-proxied capabilities (file executor, shell executor, permission gate, cancel signal) wired into the agent loop per session; SessionContext — per-session identity carrying session_id, conversation_id, and working_dir so each ACP session maps to exactly one Zeph conversation in SQLite; AgentSpawner factory type; ZephAcpAgent ACP protocol handler with multi-session isolation (each session gets its own ConversationId), LRU eviction, idle reaper, SQLite persistence, rich content support (images, embedded resources, tool locations), runtime model switching via ProviderFactory, and MCP server management via ext_method |
transport |
serve_stdio / serve_connection (stdio), HTTP+SSE handlers (post_handler, get_handler), WebSocket handler (ws_upgrade_handler), duplex bridge, axum router; AcpServerConfig |
fs |
AcpFileExecutor — file system executor backed by IDE-proxied ACP file operations |
terminal |
AcpShellExecutor — shell executor backed by IDE-proxied ACP terminal; configurable command timeout with kill_terminal on expiry; deferred terminal/release ensures terminal remains alive until IDE receives ToolCallContent::Terminal |
permission |
AcpPermissionGate — forwards tool permission requests to the IDE for user approval; persists "always allow/deny" decisions to TOML file |
mcp_bridge |
acp_mcp_servers_to_entries — converts ACP-advertised MCP servers (Stdio, Http, Sse) into McpServerEntry configs |
error |
AcpError typed error enum (includes ResourceLink variant for resolution failures) |
Re-exports: AcpContext, AgentSpawner, ProviderFactory, SessionContext, AcpError, AcpFileExecutor, AcpPermissionGate, AcpShellExecutor, AcpServerConfig, serve_connection, serve_stdio, acp_mcp_servers_to_entries
Re-exports (feature acp-http): SendAgentSpawner, AcpHttpState, acp_router
AcpContext
AcpContext carries per-session IDE capabilities into the agent loop. Each field is None when the IDE did not advertise the corresponding capability:
The cancel_signal is shared with the agent's LoopbackHandle so that an IDE cancel request immediately interrupts the running inference loop.
SessionContext
SessionContext carries per-session identity into the agent spawner, ensuring each ACP session maps to exactly one Zeph conversation:
The conversation_id is pre-created in SQLite before the agent loop starts. This guarantees that the session-to-conversation mapping is persisted atomically, enabling:
- Session isolation — concurrent sessions maintain independent conversation histories.
- Session fork —
fork_sessioncopies the source conversation's messages into a new conversation viacopy_conversation. - Session resume —
load_sessionandresume_sessionlook up the existingconversation_idfrom theacp_sessionstable; legacy sessions (pre-migration 026) get a new conversation created on demand.
[!NOTE] When the SQLite store is unavailable,
ConversationId(0)is used as a sentinel. The agent'sSemanticMemorywill create its own conversation if needed.
ResourceLink resolution
ZephAcpAgent resolves ResourceLink content blocks in prompts, as required by the ACP spec. Two URI schemes are supported:
| Scheme | Behavior | Security |
|---|---|---|
file:// |
Read local file via tokio::fs |
Canonicalized path must be inside session cwd; blocked path components (/proc, .ssh, .gnupg, etc.); size cap (1 MiB); binary rejection; 10 s timeout |
http:// / https:// |
Fetch via reqwest with Accept: text/* |
No-redirect policy; post-fetch SSRF check (private IP + CGNAT rejection, fail-closed on missing remote_addr); response size cap (1 MiB); 10 s timeout |
Unsupported schemes produce a warning and the block is skipped. Resolution failures are non-fatal.
StopReason mapping
The agent loop emits StopHint values that ZephAcpAgent maps to protocol-level StopReason variants:
| Agent condition | StopHint | ACP StopReason |
|---|---|---|
| Normal completion | (none) | EndTurn |
| LLM response truncated by token limit | MaxTokens |
MaxTokens |
Turn count >= max_turns |
MaxTurnRequests |
MaxTurnRequests |
| IDE cancel request | (cancel signal) | Cancelled |
[!NOTE]
StopHintis defined inzeph-core::channeland carried viaLoopbackEvent::Stop. The ACP layer consumes it inprompt()to produce the finalStopReason.
Tool call lifecycle
ZephAcpAgent emits ACP session notifications following the protocol-specified two-step lifecycle:
- Before execution —
SessionUpdate::ToolCallwithstatus: InProgressis sent as soon as tool invocation begins, enabling the IDE to display a running indicator. - After execution —
SessionUpdate::ToolCallUpdatewithstatus: Completed(orFailedon error) carries the output content and optional file locations.
Each tool call is identified by a UUID generated per invocation. The UUID is threaded through LoopbackEvent::ToolStart / LoopbackEvent::ToolOutput so the update correctly references the original announcement. Both the fenced-block execution path (handle_tool_result) and the structured parallel tool-call path emit this full two-step sequence unconditionally — output content always appears inside a tool call block in the IDE regardless of which path handled the tool.
Terminal release ordering — when a shell tool call embeds a terminal via ToolCallContent::Terminal, the ACP spec requires the terminal to remain alive until the IDE has processed the tool_call_update notification. ZephAcpAgent defers terminal/release until after all notifications for that event are dispatched. The deferred release is triggered from the prompt() event loop via AcpShellExecutor::release_terminal(), which is retained in SessionEntry for exactly this purpose.
[!NOTE] Prior to #1003 the fenced-block path did not generate a UUID or emit
ToolStart. Prior to #1013 the terminal was released insideexecute_in_terminalbeforetool_call_updatewas sent, preventing IDEs from displaying terminal output. Both issues are now resolved.
Subagent IDE visibility
Three _meta extensions give IDEs (Zed, VS Code ACP) structured visibility into sub-agent execution:
Parent tool use ID propagation (#1008)
Every session_update emitted by a sub-agent carries _meta.claudeCode.parentToolUseId set to the tool call ID that spawned it. IDEs use this to nest sub-agent output under the originating tool card, so multi-agent runs render as collapsible trees rather than interleaved flat messages.
Terminal streaming (#1009)
AcpShellExecutor streams shell output in real time instead of buffering until completion:
- Each chunk of stdout/stderr is emitted as a
session_updatewith_meta.terminal_output. - When the command exits, a final
session_updatewith_meta.terminal_exitcarries the exit code. - IDEs display the output inside the tool card as it arrives.
Tool call location (#1010)
ToolCall.location carries a filePath for file read/write tool calls. IDEs move the editor cursor to the referenced file as the agent works, so the user always sees which file is being modified without manually switching tabs.
[!TIP] All three extensions are additive
_metafields — they are ignored by clients that do not recognize them and require no feature flag.
Terminal command timeout
AcpShellExecutor enforces a configurable wall-clock timeout on every IDE-proxied shell command (default: 120 seconds, controlled via acp.terminal_timeout_secs). When the timeout expires:
kill_terminalis called to terminate the running process.- Partial output collected so far is returned as an error result.
- The terminal is released and
AcpError::TerminalTimeoutis propagated to the agent loop.
[]
= 120 # set to 0 to disable (wait indefinitely)
Protocol methods
AgentCapabilities (G3)
The initialize response advertises enriched capabilities:
new
.load_session
.mcp_capabilities // when McpManager is available
.meta
This signals to the IDE that the agent supports session config options (session/configure), custom ext_method extensions, and MCP server management. McpCapabilities is only advertised when a McpManager is configured.
set_session_mode (G2)
ZephAcpAgent implements set_session_mode to handle IDE-driven mode switches per session:
- Validates that the target session exists; returns
invalid_requesterror if not found. - Logs the
session_idandmode_idat debug level. - Currently a no-op acknowledgement — mode semantics are handled by the IDE.
ext_notification (G4)
ZephAcpAgent implements ext_notification to accept IDE-originated fire-and-forget notifications:
- Logs the notification method name at debug level.
- Returns
Ok(())for all known and unknown methods — unrecognized notifications are silently accepted.
MCP transport support (G8)
acp_mcp_servers_to_entries converts ACP-advertised MCP servers into zeph-mcp ServerEntry configs. Three transport types are supported:
| ACP variant | Mapped transport | Notes |
|---|---|---|
McpServer::Stdio |
McpTransport::Stdio |
Env vars forwarded as-is to child process |
McpServer::Http |
McpTransport::Http |
Streamable HTTP via rmcp |
McpServer::Sse |
McpTransport::Http |
Legacy SSE mapped to streamable HTTP (backward-compatible) |
[!NOTE] SSE is a legacy MCP transport. rmcp's
StreamableHttpClientTransporthandles both SSE and streamable HTTP endpoints, so both variants map toMcpTransport::Http.
use acp_mcp_servers_to_entries;
let entries = acp_mcp_servers_to_entries;
// entries: Vec<ServerEntry> ready for McpManager::start_all
HTTP+SSE transport (feature acp-http)
Enable the acp-http feature to expose Zeph over HTTP with Server-Sent Events:
use ;
let state = new;
state.start_reaper; // prune idle connections every 60 s
let app = acp_router;
// mount app into your axum Router
Endpoints:
| Method | Path | Description |
|---|---|---|
POST |
/acp |
Send a JSON-RPC request; stream responses as SSE. Creates a new connection when Acp-Session-Id header is absent. |
GET |
/acp |
Reconnect to an existing connection's SSE stream. Requires Acp-Session-Id header. |
GET |
/acp/ws |
WebSocket upgrade for bidirectional streaming. |
Session IDs are UUIDs returned in the Acp-Session-Id response header. Idle connections (beyond session_idle_timeout_secs) are reaped by a background task.
[!TIP] Use
SendAgentSpawner(theSend-safe variant ofAgentSpawner) when constructingAcpHttpState. This satisfies axum'sStaterequirement forSend + Sync.
Rich content
ACP prompts can carry multi-modal content blocks beyond plain text:
- Images — base64-encoded image blocks (
image/jpeg,image/png,image/gif,image/webp) are decoded and forwarded to the LLM provider as inline attachments. Oversized payloads and unsupported MIME types are skipped with a warning. - Embedded resources —
TextResourceContentsblocks are injected into the prompt text wrapped in<resource>markers. - Tool locations — tool call results can include file path locations (
ToolCallLocation) that the IDE uses for source navigation. - Thinking chunks — intermediate reasoning status events are streamed back to the IDE as
session/updateevents.
Model switching
The IDE can switch the active LLM model at runtime via session/configure with config_id = "model". ZephAcpAgent uses a ProviderFactory closure that resolves a "provider:model" key to an AnyProvider, and an available_models allowlist that populates the IDE dropdown. The resolved provider is stored in a shared Arc<RwLock<Option<AnyProvider>>> (provider_override) that the agent loop checks on each turn.
ConfigOptionUpdate notifications
When a config option changes via set_session_config_option, ZephAcpAgent emits a SessionUpdate::ConfigOptionUpdate notification containing only the changed option. This keeps the IDE config UI in sync without requiring a full config poll.
Config option categories
Config options include SessionConfigOptionCategory for IDE grouping:
| Config option | Category |
|---|---|
model |
Model |
thinking |
Model |
auto_approve |
Other |
MCP server management
ext_method handles custom JSON-RPC extensions for managing MCP servers at runtime:
| Method | Description |
|---|---|
_agent/mcp/list |
List active MCP servers |
_agent/mcp/add |
Register a new MCP server |
_agent/mcp/remove |
Remove a running MCP server |
Requires a shared McpManager reference set via AcpServerConfig::mcp_manager.
Session lifecycle
ZephAcpAgent manages multiple concurrent sessions with the following capabilities:
- Per-session conversation isolation — each session is assigned its own
ConversationIdat creation time (migration 026). Concurrent sessions maintain fully independent conversation histories in SQLite, preventing cross-session message leakage. - LRU eviction — when the number of active sessions reaches
max_sessions, the least-recently-used session is evicted to free resources. - SQLite persistence — session events are persisted to
acp_sessionsandacp_session_eventstables (migrations 013, 026) viazeph-memory. Theacp_sessionstable carries aconversation_idforeign key linking each session to its conversation. - Session resume —
load_sessionreplays persisted history assession/updateevents, restoring the conversation state. The existingconversation_idis looked up from the store; legacy sessions (pre-026) get a new conversation created on demand. - Session fork —
fork_sessioncreates a new conversation and copies the source session's messages viacopy_conversation, preserving history while isolating future turns. - Idle reaper — a background task periodically removes sessions that have been idle longer than
session_idle_timeout_secs.
Configuration
| Config field | Type | Default | Env override |
|---|---|---|---|
acp.max_sessions |
usize | 16 |
ZEPH_ACP_MAX_SESSIONS |
acp.session_idle_timeout_secs |
u64 | 1800 |
ZEPH_ACP_SESSION_IDLE_TIMEOUT_SECS |
acp.permission_file |
PathBuf | ~/.config/zeph/acp-permissions.toml |
ZEPH_ACP_PERMISSION_FILE |
acp.terminal_timeout_secs |
u64 | 120 |
ZEPH_ACP_TERMINAL_TIMEOUT_SECS |
acp.available_models |
Vec<String> |
[] |
— |
Permission persistence
When the IDE user selects "always allow" or "always deny" for a tool, AcpPermissionGate persists the decision to a TOML file (~/.config/zeph/acp-permissions.toml by default). On next session startup the gate pre-populates its cache from this file, skipping redundant IDE prompts.
- Atomic write via temp file + rename to prevent corruption.
- File permissions set to
0o600(owner-only). - Graceful fallback: if the file is missing or malformed, the gate starts with an empty cache.
AgentSpawner
AgentSpawner is the integration contract between zeph-acp and the host application:
pub type AgentSpawner = ;
The host constructs an AgentSpawner closure that wires AcpContext capabilities and SessionContext (session identity + pre-created ConversationId) into Agent via the builder, then passes the closure to serve_stdio or serve_connection.
For HTTP transport, use SendAgentSpawner which requires Send + Sync:
pub type SendAgentSpawner = ;
Custom methods
ZephAcpAgent exposes vendor-specific extensions via ExtRequest dispatch. The custom module matches on req.method and routes to the appropriate handler. Unrecognized methods return None, allowing the ACP runtime to respond with "method not found".
| Method | Description |
|---|---|
_session/list |
List all sessions (in-memory + persisted via SqliteStore::list_acp_sessions) |
_session/get |
Get session details and event history |
_session/delete |
Remove session from memory and SQLite |
_session/export |
Export session events as a portable JSON payload |
_session/import |
Import events into a new session (UUID assigned server-side) |
_agent/tools |
Return the list of tools available to the agent |
_agent/working_dir/update |
Change the working directory for a session |
Security guards
- Session ID validation — IDs must be at most 128 characters, restricted to
[a-zA-Z0-9_-]. Rejects control characters, slashes, and whitespace. - Path traversal protection —
_agent/working_dir/updaterejects any path containing..(Component::ParentDir). - Import size cap —
_session/importrejects payloads exceeding 10,000 events.
Auth hints in initialize
The initialize response includes an auth_hint key in its metadata map. For stdio transport (trusted local client) this is a generic "authentication required" string. IDEs can use this hint to prompt the user for credentials before issuing further requests.
Feature flags
| Feature | Status | Description |
|---|---|---|
acp-http |
stable | Enables the HTTP+SSE and WebSocket transports (axum-based). Required for post_handler, get_handler, ws_upgrade_handler, and router. |
unstable-session-list |
unstable | Enables the list_sessions ACP method. See below. |
unstable-session-fork |
unstable | Enables the fork_session ACP method. See below. |
unstable-session-resume |
unstable | Enables the resume_session ACP method. See below. |
unstable-session-usage |
unstable | Enables UsageUpdate events — token counts (input, output, cache) sent to the IDE after each turn. See below. |
unstable-session-model |
unstable | Enables SetSessionModel — IDE-driven model switching via a native picker without session/configure. See below. |
unstable-session-info-update |
unstable | Enables SessionInfoUpdate — agent-generated session title emitted to the IDE after the first turn. See below. |
[!WARNING] All
unstable-*features have wire protocol that is not yet finalized. Expect breaking changes before these features graduate to stable.
To opt in, add the desired features in your Cargo.toml:
[]
= { = "*", = [
"unstable-session-list",
"unstable-session-fork",
"unstable-session-resume",
"unstable-session-usage",
"unstable-session-model",
"unstable-session-info-update",
] }
All flags are independent and can be combined freely.
unstable-session-list
Enables the list_sessions method on ZephAcpAgent. Returns a snapshot of all active in-memory sessions as SessionInfo records (session ID, working directory, last-updated timestamp). Supports an optional cwd filter — when provided, only sessions whose working directory matches the given path are returned.
When this feature is active, initialize advertises SessionListCapabilities in the session capabilities block, signalling to the IDE that the server supports session enumeration.
unstable-session-fork
Enables the fork_session method. Branches an existing conversation into a new session by:
- Verifying the source session exists in memory or SQLite.
- Assigning a fresh UUID as the new session ID.
- Asynchronously copying all persisted events from the source into the new session via
import_acp_events. - Spawning a new agent loop for the forked session with the supplied
cwd.
The forked session is immediately available for new turns. The event copy is fire-and-forget — if the store write fails, a warning is logged but the session is still created. Model config options are forwarded to the fork response when available_models is non-empty.
unstable-session-resume
Enables the resume_session method. Restores a persisted session to an active in-memory state without replaying history as session/update events:
- If the session is already active in memory, returns success immediately (no-op).
- Otherwise, verifies existence in SQLite and hydrates a new
SessionEntry, making the session available for new turns with lower latency than the defaultload_sessionreplay path. - Requires a configured
SqliteStore; returns an error if no store is present.
unstable-session-usage
Enables UsageUpdate session events. After each agent turn ZephAcpAgent emits a SessionUpdate::UsageUpdate carrying token counts for the turn:
input_tokens— tokens consumed from the prompt.output_tokens— tokens produced by the model.cache_read_tokens/cache_write_tokens— cache activity when the provider supports prompt caching.
The IDE can use this data to display running cost estimates or token budgets without polling a separate endpoint.
unstable-session-model
Enables SetSessionModel handling. When the IDE sends a set_session_model request (e.g., from a native model-picker dropdown), ZephAcpAgent:
- Resolves the requested
"provider:model"key viaProviderFactory. - Stores the resolved provider in the session-scoped
provider_override. - Returns a confirmation to the IDE so the picker reflects the active selection.
This avoids the need to wrap model selection in a session/configure call and maps directly to the Zed AI model picker interaction.
unstable-session-info-update
Enables SessionInfoUpdate events. After the first completed turn in a new session, ZephAcpAgent emits a SessionUpdate::SessionInfoUpdate containing a short, LLM-generated title derived from the opening message. The IDE can use this title to label the session in its sidebar or tab bar.
Plan updates
During orchestrator runs ZephAcpAgent emits SessionUpdate::Plan events as the agent formulates its execution plan. The IDE receives these events in real time and can render a collapsible plan view alongside the conversation, giving users visibility into multi-step reasoning before tool calls begin.
Slash command dispatch
ZephAcpAgent sends an AvailableCommandsUpdate to the IDE during session initialization listing the built-in slash commands:
| Command | Description |
|---|---|
/help |
Show available slash commands |
/model |
Switch the active model |
/mode |
Change the session mode (ask / code / architect) |
/clear |
Clear the current conversation history |
/compact |
Trigger a manual context compaction |
User input that begins with / is matched against this list and dispatched to the corresponding handler before the message reaches the agent loop.
Documentation
Full documentation: https://bug-ops.github.io/zeph/
License
MIT