Proxy
proxy
¶
ACP proxy — one connection's worth of typed JSON-RPC mediation.
ACPProxy is the bridge behind
ACPRoster.attach. It
implements both sides of the ACP protocol on the same object:
acp.Agent— facing the connected ACP client (Zed, Toad, …). Anacp.agent.connection.AgentSideConnectionreads the client's frames, deserialises them into typed pydantic models, and dispatches toself.initialize/self.new_session/self.prompt/ etc.acp.Client— facing the bound in-container backend wrapper. Once a model has been picked, aacp.client.connection.ClientSideConnectiontoterok-{agent}-acpreads the wrapper's frames and dispatches backend → client traffic (session/update,request_permission,fs/*,terminal/*) onto the same proxy object so it can forward to the connected client.
Two phases drive the lifecycle:
- Pre-bind:
initializeandsession/newanswer locally, advertising the aggregatedagent:modellist inacp.schema.SessionModelStateplus a mirroringconfigOptions[category=model]. No backend process exists yet. - Bound: on the first model-picking client request — modern ACP's
session/set_modelor older Zed'ssession/set_config_option(category=model), or lazily on the first backend-needing method likesession/prompt— the proxy spawns the in-container wrapper throughacp.spawn_agent_process, replaysinitialize+session/new+session/set_modelagainst it, and from then on forwards typed calls in both directions. Backend responses and notifications carrying model ids are re-namespaced on the way out so the client always seesagent:modelids.
V1 takes shortcuts where the design is still settling: one session per connection (Zed reconnects on every chat — fix on the roadmap), one bound agent per session (no cross-agent switches without reconnect), no push notifications when the authed-agent set changes mid-connection.
CLIENT_SESSION_ID = 'proxy-1'
module-attribute
¶
Synthetic session id the proxy advertises to the connected client.
Backend session ids never reach the client — every backend-originated
frame has its sessionId rewritten to this constant on the way out.
CONTAINER_WORKSPACE = '/workspace'
module-attribute
¶
Path the backend session/new runs in.
ACP clients send their host filesystem path in new_session.cwd
(Zed: /var/home/user/prog/X) which doesn't exist inside the
container. claude-agent-acp chdirs into cwd before exec; an
ENOENT there surfaces as the famously misleading "Claude Code native
binary not found …". Pinning to the container's workspace mount is a
stopgap until the host↔sandbox path strategy lands.
PROXY_AGENT_NAME = 'terok-acp'
module-attribute
¶
PROXY_AGENT_TITLE = 'Terok ACP host-proxy'
module-attribute
¶
PROXY_AGENT_VERSION = '1'
module-attribute
¶
SessionUpdatePayload = UserMessageChunk | AgentMessageChunk | AgentThoughtChunk | ToolCallStart | ToolCallProgress | AgentPlanUpdate | AvailableCommandsUpdate | CurrentModeUpdate | ConfigOptionUpdate | SessionInfoUpdate | UsageUpdate
module-attribute
¶
ACPProxy(*, roster)
¶
One client connection's worth of proxy state.
Constructed by attach;
lives for the duration of a single client connection. Not reusable
— discard after run returns.
Source code in src/terok_executor/acp/proxy.py
run(reader, writer)
async
¶
Run the typed proxy loop until the client disconnects.
Hands the client side to
acp.agent.connection.AgentSideConnection
which dispatches typed methods on this object. Always tears the
bound backend down on exit, even on cancellation.
Source code in src/terok_executor/acp/proxy.py
on_connect(_conn)
¶
initialize(protocol_version, client_capabilities=None, client_info=None, **_kw)
async
¶
Answer initialize locally and capture the client's caps.
client_capabilities is replayed verbatim on the backend's
initialize during bind — whatever the client said it can do,
the backend believes. client_info is accepted to satisfy
the protocol but discarded (the proxy doesn't relay it).
Source code in src/terok_executor/acp/proxy.py
new_session(cwd, additional_directories=None, mcp_servers=None, **_kw)
async
¶
Answer session/new with the aggregated model list.
Synthesises CLIENT_SESSION_ID
so the client can proceed to model selection before any backend
exists. The real backend session id is captured on bind and
translated on every forwarded frame.
Source code in src/terok_executor/acp/proxy.py
set_session_model(model_id, session_id, **_kw)
async
¶
Bind on first call; same-agent re-pick forwards through.
Source code in src/terok_executor/acp/proxy.py
set_config_option(config_id, session_id, value, **_kw)
async
¶
Bind on category=model; otherwise forward to the bound backend.
Older ACP clients (Zed v1.0.x at the time of writing) pick the
model through session/set_config_option(category="model");
modern clients use the dedicated session/set_model. Accept
both. Non-model categories pass through to the bound backend;
a non-model option pre-bind is rejected.
Source code in src/terok_executor/acp/proxy.py
prompt(prompt, session_id, message_id=None, **_kw)
async
¶
Lazy-bind to the default model if needed, then forward.
Source code in src/terok_executor/acp/proxy.py
cancel(session_id, **_kw)
async
¶
Fire-and-forget cancel to the backend (no-op if not bound).
Source code in src/terok_executor/acp/proxy.py
authenticate(method_id, **_kw)
async
¶
Forward to bound backend; pre-bind authenticate is rejected.
Source code in src/terok_executor/acp/proxy.py
set_session_mode(mode_id, session_id, **_kw)
async
¶
Forward to bound backend.
Source code in src/terok_executor/acp/proxy.py
load_session(cwd, session_id, additional_directories=None, mcp_servers=None, **_kw)
async
¶
Forward to bound backend (v1 advertises no session-load capability).
Source code in src/terok_executor/acp/proxy.py
list_sessions(additional_directories=None, cursor=None, cwd=None, **_kw)
async
¶
Forward to bound backend.
Source code in src/terok_executor/acp/proxy.py
fork_session(cwd, session_id, additional_directories=None, mcp_servers=None, **_kw)
async
¶
Forward to bound backend.
Source code in src/terok_executor/acp/proxy.py
resume_session(cwd, session_id, additional_directories=None, mcp_servers=None, **_kw)
async
¶
Forward to bound backend.
Source code in src/terok_executor/acp/proxy.py
close_session(session_id, **_kw)
async
¶
Forward to bound backend and tear down the wrapper.
v1 keeps one backend per connection; after a successful close the
wrapper has nothing more to do, so reap it eagerly instead of
leaving it around to be killed by _teardown_backend on
disconnect. Returns None when no backend was ever bound.
Source code in src/terok_executor/acp/proxy.py
ext_method(method, params)
async
¶
Forward extension methods to the bound backend.
ext_notification(method, params)
async
¶
Forward extension notifications to the bound backend (silent if not bound).
Source code in src/terok_executor/acp/proxy.py
session_update(session_id, update, **_kw)
async
¶
Rewrite session id and any model ids, then forward to the client.
Source code in src/terok_executor/acp/proxy.py
request_permission(options, session_id, tool_call, **_kw)
async
¶
Forward permission request to the connected client.
Source code in src/terok_executor/acp/proxy.py
read_text_file(path, session_id, limit=None, line=None, **_kw)
async
¶
Forward fs read to the connected client.
Source code in src/terok_executor/acp/proxy.py
write_text_file(content, path, session_id, **_kw)
async
¶
Forward fs write to the connected client.
Source code in src/terok_executor/acp/proxy.py
create_terminal(command, session_id, args=None, cwd=None, env=None, output_byte_limit=None, **_kw)
async
¶
Forward terminal create to the connected client.
Source code in src/terok_executor/acp/proxy.py
terminal_output(session_id, terminal_id, **_kw)
async
¶
Forward terminal output read to the connected client.
Source code in src/terok_executor/acp/proxy.py
release_terminal(session_id, terminal_id, **_kw)
async
¶
Forward terminal release to the connected client.
Source code in src/terok_executor/acp/proxy.py
wait_for_terminal_exit(session_id, terminal_id, **_kw)
async
¶
Forward wait-for-exit to the connected client.
Source code in src/terok_executor/acp/proxy.py
kill_terminal(session_id, terminal_id, **_kw)
async
¶
Forward kill to the connected client.
Source code in src/terok_executor/acp/proxy.py
AgentBindError
¶
Bases: RuntimeError
Surface error raised when the proxy fails to bind a backend agent.
Always converted to a JSON-RPC error response on the wire — never
bubbles to the caller of run.