spens-acp configuration
Complete configuration reference for the spens ACP shim — flags, environment variables, session behaviour, and troubleshooting
Warning: spens-acp is alpha software. It may experience bugs or challenges.
spens-acp is the Agent Client Protocol (ACP) shim for spens. It sits between an ACP-compatible editor and the spens CLI, translating JSON-RPC 2.0 requests into sandboxed spens sessions and streaming progress back as ACP notifications.
For a step-by-step guide to installing it and wiring it to Zed, see Using spens with an IDE.
How it works
The wrapper is intentionally loose — it couples only to spens' documented CLI surface and on-disk session files. It never imports spens as a library, and degrades gracefully when spens emits records it does not recognise.
ACP client (Zed / Devin / …)
│ JSON-RPC 2.0 over stdio
▼
spens-acp (Python; acp.Agent implementation)
│ subprocess (argv list, no shell)
├──────────────────────────► spens CLI
│ │
│ ▼
│ sessions/<id>/
│ events.jsonl
│ state.json
│ traces/captured.jsonl
▼
session/update notifications ◄── decode + mapACP surface
spens-acp implements the official acp.Agent protocol via the agent-client-protocol SDK.
| Method | Behaviour |
|---|---|
initialize | Echoes the client's protocolVersion, advertises agentInfo, validates spens binary and explicit env/agent config |
session/new | Agent generates sessionId (per official protocol). Records workspace cwd, resolves env/agent from params, workspace config, or error |
session/resume | Restores a session in a fresh wrapper process from persisted state; validates cwd/env/agent, continues the conversation and spens-id sequence |
session/prompt | Flattens prompt → string, prepends replayed session history, launches one spens yolo session, streams session/update notifications, returns PromptResponse |
session/cancel | Notification. Calls spens cancel <id>; in-flight session/prompt resolves with stopReason: "cancelled" |
Not implemented: session/set_mode, session/load, file-system methods, permission requests. These return JSON-RPC method not found (-32601).
Environment and agent resolution
When a session/new or session/prompt request arrives, spens-acp resolves the environment and agent in this order:
session/newparams (env/agent, top-level or in the_metaextensibility field)- Explicit argument —
--env/--agentorSPENS_ENV/SPENS_AGENT - Workspace config —
<workspace>/.spens.config.jsonkeysdefault_env/default_agent - Error —
"No environment/agent configured: pass --env/--agent or set default_env/default_agent in .spens.config.json"
Configuration reference
Flags and environment variables
| Env var / flag | Default | Meaning |
|---|---|---|
SPENS_BIN | spens | Path to spens binary |
SPENS_ENV / --env | — | Explicit environment |
SPENS_AGENT / --agent | — | Explicit agent |
SPENS_CHANGES | accept | accept or reject → maps to --accept-changes / --reject-changes |
SPENS_REBUILD | auto | auto (never pass flag) or always (pass --rebuild) |
SPENS_DIR | <workspace>/.spens | Override --spens-dir |
SPENS_INCLUDE_AGENT_OUTPUT | false | Stream agent_output events as thought chunks |
SPENS_EMIT_SUMMARY | true | Emit final summary thought chunk (tokens / cost / files) |
SPENS_TOOL_RESULT_MAX | 2000 | Truncate tool-result content in updates |
SPENS_HISTORY | true | Replay earlier turns of the ACP session as transcript context in each spens prompt |
SPENS_HISTORY_MAX_TURNS | 20 | Maximum earlier turns included in the replayed transcript |
SPENS_HISTORY_MAX_CHARS | 24000 | Character budget for the replayed transcript; oldest turns are dropped whole until it fits |
SPENS_RESUME | true | Persist session state to <spens-dir>/acp-sessions/ and support session/resume |
SPENS_POLL_INTERVAL | 0.25 | File tailer poll interval in seconds |
SPENS_LAUNCH_TIMEOUT | 60 | Seconds to wait for spens --output background to print the session id |
SPENS_SESSION_DIR_TIMEOUT | 60 | Seconds to wait for the session directory to appear after a confirmed launch |
SPENS_CANCEL_ON_EXIT | true | SIGTERM/SIGINT/EOF → spens cancel in-flight sessions |
SPENS_DEBUG | — | Set to 1 for verbose stderr diagnostics (argv, events, state transitions) |
Example ACP client wiring (Zed-style)
| Field | Value |
|---|---|
| Command | python3 -m spens_acp |
| Environment | { SPENS_ENV: node-22, SPENS_AGENT: claude } |
Continuous sessions (history replay)
spens itself is stateless — each prompt turn launches a fresh spens session. To make an ACP session behave like one continuous conversation, the wrapper records every completed exchange (user prompt + final assistant text from captured.jsonl) and replays them as a compact transcript prefix on the next prompt:
You are continuing an existing conversation. ...
User: <earlier prompt>
Assistant: <earlier reply>
User: <new prompt>Only successful (end_turn) turns are recorded. The transcript is bounded by SPENS_HISTORY_MAX_TURNS and SPENS_HISTORY_MAX_CHARS. Oldest turns are dropped whole, with an [... N earlier turn(s) omitted ...] marker. An oversized single exchange is mid-truncated with .... Disable the whole feature with SPENS_HISTORY=0 — every prompt then launches spens with the bare user message.
Resuming sessions (session/resume)
The wrapper process is itself stateless. When the editor restarts or reconnects, it spawns a fresh spens-acp with no memory. To let those sessions continue, every ACP session's state is mirrored to a small JSON file:
<spens-dir>/acp-sessions/<sessionId>.jsonThe file contains the workspace cwd, resolved env/agent, the replay-turn history, and the spens session ids already used. Writes are atomic and best-effort — a failed write never fails the prompt it serves, it only costs resumability.
session/resume reloads that file in a fresh process, re-validates the binary and env/agent, and restores the history so the next prompt continues the conversation. The spens-id sequence continues too, so a resumed turn never reuses a session directory already on disk.
Notes:
- A session can only be resumed in the workspace it was created in (the
cwdof the resume request must match). WithSPENS_DIRpointing at a shared directory the state is found there instead. - The spens id is persisted before each launch, so even a crash mid-turn cannot make a later resume reuse the id.
- Disable with
SPENS_RESUME=0: no state is written, the capability is not advertised, and cross-process resume answersinvalid params. Same-process resume of a live session still works.
Provider format support
The interceptor captures all three provider-native streaming formats raw. The wrapper dispatches decoders by request URL:
| URL pattern | Decoder | Used by |
|---|---|---|
*/chat/completions | OpenAI Chat Completions | opencode, pi, Fireworks, OpenRouter, … |
*/v1/messages (api.anthropic.com) | Anthropic Messages | claude |
*/v1/responses | OpenAI Responses | codex |
Unknown URLs degrade to the agent_output fallback rather than crashing.
stdout discipline
Only JSON-RPC ever goes to stdout (the SDK owns the stdio transport). All wrapper logging, spens subprocess stdout/stderr, and decoder warnings go to stderr. Key milestones (launch confirmed, session ended) are always logged to stderr. Set SPENS_DEBUG=1 for argv, event, and state-transition detail.
Troubleshooting
Smoke test
Split the problem in half by running one real prompt through the whole pipeline without any editor:
spens-acp --smoke-test "reply with hello" --env python-3.12 --agent piIt drives the agent through initialize / session/new / session/prompt against your real spens binary in the current directory and prints every session update to stderr. If the smoke test passes but the editor hangs, the problem is between the editor and the wrapper. If the smoke test hangs or fails, its last line shows exactly where the wrapper stopped, with the session dir it was waiting for.
Log decision tree
Every line below goes to stderr (SPENS_DEBUG=1 adds argv / event / state detail). In Zed, agent stderr lands in the Zed log (zed: open log):
spens-acp 0.1.x starting; spens binary: …— no line at all means an old build is installed or the agent never started.session '…' created (cwd=…, env=…, agent=…)—session/newsucceeded.prompt received for session '…'— the prompt request arrived.launching spens session '…' in <workspace>— right before spawn.spens session '…' confirmed via …— launch confirmed. Stuck before this line means the session dir never appeared where expected. WithSPENS_DEBUG=1, the lineexpecting session dir: …shows the exact path.spens session '…' ended: state='…' -> …— terminal state reached.
Prompt hangs forever
Launch is confirmed by the session directory appearing on disk, not by spens' stdout pipes closing. Both pipes are drained for the launcher's lifetime so a foreground spens can never freeze on a full pipe. If nothing confirms within SPENS_LAUNCH_TIMEOUT the launcher is killed with a clear error. If the session directory never appears where the wrapper expects it (<workspace>/.spens/sessions/<id>/, or SPENS_DIR), the prompt fails after SPENS_SESSION_DIR_TIMEOUT with the exact path it was waiting for.
Spens