Spens logoSpens

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 + map

ACP surface

spens-acp implements the official acp.Agent protocol via the agent-client-protocol SDK.

MethodBehaviour
initializeEchoes the client's protocolVersion, advertises agentInfo, validates spens binary and explicit env/agent config
session/newAgent generates sessionId (per official protocol). Records workspace cwd, resolves env/agent from params, workspace config, or error
session/resumeRestores a session in a fresh wrapper process from persisted state; validates cwd/env/agent, continues the conversation and spens-id sequence
session/promptFlattens prompt → string, prepends replayed session history, launches one spens yolo session, streams session/update notifications, returns PromptResponse
session/cancelNotification. 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:

  1. session/new params (env / agent, top-level or in the _meta extensibility field)
  2. Explicit argument — --env / --agent or SPENS_ENV / SPENS_AGENT
  3. Workspace config — <workspace>/.spens.config.json keys default_env / default_agent
  4. 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 / flagDefaultMeaning
SPENS_BINspensPath to spens binary
SPENS_ENV / --envExplicit environment
SPENS_AGENT / --agentExplicit agent
SPENS_CHANGESacceptaccept or reject → maps to --accept-changes / --reject-changes
SPENS_REBUILDautoauto (never pass flag) or always (pass --rebuild)
SPENS_DIR<workspace>/.spensOverride --spens-dir
SPENS_INCLUDE_AGENT_OUTPUTfalseStream agent_output events as thought chunks
SPENS_EMIT_SUMMARYtrueEmit final summary thought chunk (tokens / cost / files)
SPENS_TOOL_RESULT_MAX2000Truncate tool-result content in updates
SPENS_HISTORYtrueReplay earlier turns of the ACP session as transcript context in each spens prompt
SPENS_HISTORY_MAX_TURNS20Maximum earlier turns included in the replayed transcript
SPENS_HISTORY_MAX_CHARS24000Character budget for the replayed transcript; oldest turns are dropped whole until it fits
SPENS_RESUMEtruePersist session state to <spens-dir>/acp-sessions/ and support session/resume
SPENS_POLL_INTERVAL0.25File tailer poll interval in seconds
SPENS_LAUNCH_TIMEOUT60Seconds to wait for spens --output background to print the session id
SPENS_SESSION_DIR_TIMEOUT60Seconds to wait for the session directory to appear after a confirmed launch
SPENS_CANCEL_ON_EXITtrueSIGTERM/SIGINT/EOF → spens cancel in-flight sessions
SPENS_DEBUGSet to 1 for verbose stderr diagnostics (argv, events, state transitions)

Example ACP client wiring (Zed-style)

FieldValue
Commandpython3 -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>.json

The 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 cwd of the resume request must match). With SPENS_DIR pointing 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 answers invalid 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 patternDecoderUsed by
*/chat/completionsOpenAI Chat Completionsopencode, pi, Fireworks, OpenRouter, …
*/v1/messages (api.anthropic.com)Anthropic Messagesclaude
*/v1/responsesOpenAI Responsescodex

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 pi

It 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):

  1. spens-acp 0.1.x starting; spens binary: … — no line at all means an old build is installed or the agent never started.
  2. session '…' created (cwd=…, env=…, agent=…)session/new succeeded.
  3. prompt received for session '…' — the prompt request arrived.
  4. launching spens session '…' in <workspace> — right before spawn.
  5. spens session '…' confirmed via … — launch confirmed. Stuck before this line means the session dir never appeared where expected. With SPENS_DEBUG=1, the line expecting session dir: … shows the exact path.
  6. 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.

On this page