Skip to content

feat(runtime): production ACP adapter for opencode and Claude Code - #69

Open
FreshlyBrewedCode wants to merge 4 commits into
t3code/investigate-canvas-acp-adapterfrom
acp/adapter
Open

FreshlyBrewedCode wants to merge 4 commits into
t3code/investigate-canvas-acp-adapterfrom
acp/adapter

Conversation

@FreshlyBrewedCode

@FreshlyBrewedCode FreshlyBrewedCode commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

What

This PR turns the finding-13 spike adapter into the production ACP adapter (ADR 0013 §1, §3–§6). It runs beside the opencode adapter, and #64 makes it the default.

  • src/runtime/acp-agents.ts: the agent definitions and the availability check.
    • claudeAgent() launches the bundled claude-agent-acp on the daemon's own bun. opencodeAgent() launches opencode acp. acpAgent(kind, { hostSettings }) picks one of the two.
    • Host settings default to "ignore":
      • Claude: _meta.claudeCode.options.settingSources: ["project","local"].
      • opencode: XDG_CONFIG_HOME and OPENCODE_TEST_HOME point at empty directories that factory owns. A factory plugin (acp-opencode-plugin.ts, opencode's shell.env hook) hands the host's XDG_CONFIG_HOME back to every shell command, so gh and git keep their config. The host's OPENCODE_CONFIG* variables are dropped.
    • Environment scrub: CLAUDECODE and CLAUDE_* are removed from the agent's environment. The credential variables stay: CLAUDE_CONFIG_DIR, CLAUDE_CODE_OAUTH_TOKEN and CLAUDE_CODE_USE_BEDROCK/_VERTEX.
    • checkAgents(kinds) returns {agent, problem}[]. It reports opencode missing from PATH, and Claude not logged in (read from the bundled CLI's auth status --json).
  • src/runtime/acp-adapter.ts: acpAdapter(definition, { onDiagnostic?, cancelGraceMs? }).
    • Model: required. If the agent doesn't offer it, the step fails before the prompt, and the error names the agent and gives a sample of the ids it does offer.
    • Permission: answered with allow_always, falling back to allow_once.
    • Usage: each usage_update becomes a CUSTOM acp.usage chunk carrying a new usage signal (context: {used,size}, cost?: {amount,currency}), in arrival order.
    • Cancel: session/cancel, then a kill after a grace period, and always a kill in finally (SIGKILL if the process lingers).
    • Exit mid-turn: the step fails with the exit code and the tail of stderr.
  • AgentSignal gains usage. The runtime ignores it for now; feat(events): context and cost per agent step, ACP-aware run detail #65 records it.
  • AgentStepChunkError now starts with the adapter's message, so AgentStepFinished.error reads AgentStepChunkError: claude has no model "…" instead of a bare stack.
  • spikes/13-acp-agents.ts is adapted to the production API and has a new isolation mode.
  • Finding 13, addendum 2 records the isolation results.

Tests

bun run check is green (377 tests). The new unit tests run against a fake ACP agent over stdio, built on the SDK's AgentSideConnection (test/fixtures/fake-acp-agent.ts). They cover:

  • model selection and rejection, a missing model, and an agent with no model option;
  • the allow_always answer;
  • usage chunks and signals, and their order;
  • structured output;
  • refusal surfacing as RUN_ERROR;
  • the environment scrub;
  • cancel that settles as cancelled, an agent that ignores cancel and is killed after the grace period, and a crash mid-turn that carries the exit code and stderr;
  • a start failure;
  • a run cancelled through startRun that leaves no process behind.

Separate tests cover the definitions, the plugin hook and the auth parsing.

Live validation (this host)

run result
run claude sonnet completed. implement 7.8 s, report 5.7 s. Model switched from haiku to sonnet. One ask (Edit math.ts), answered allow_always. Structured output on tier 1. Per-step cost 0.039 / 0.027 USD in acp.usage
run opencode opencode/big-pickle completed. implement 8.3 s, report 6.6 s. Structured output on tier 1. (One earlier run took 244 s on slow provider calls; a rerun was 10.8 s.)
cancel claude sonnet RunCancelled 2 ms, settled 22 ms, 0 of 2 processes alive
cancel opencode … RunCancelled 2 ms, settled 83 ms, 0 of 1 processes alive
bad-model claude / opencode failed at 0.8 s / 1.5 s, before the prompt, with the offered ids
env scrub (run from inside Claude Code) 12 CLAUDE* vars in the parent, 0 passed to the agent. Parent sentinel values appeared 0 times in either agent's shell
checkAgents [] on this host (0.25 s). With HOME pointed at an empty dir and opencode off PATH, both problems are reported

Isolation (finding 13, addendum 2):

  • Claude: settingSources: ["project","local"] hides the user's skills, the anthropic-skills plugin and the user CLAUDE.md. 47 skills are listed with include and 14 with ignore. The project's CLAUDE.md and skills stay.
  • opencode: OPENCODE_CONFIG_DIR only adds a directory; with it set, 715 models were still offered. What hides the host config is XDG_CONFIG_HOME, the plugin and OPENCODE_TEST_HOME:
    • skills drop from 51 to 3 (the built-in one and the two project skills);
    • models drop from 715 to 39 (the host's omniroute provider is gone);
    • in the shell, XDG_CONFIG_HOME=/home/karl/.config is restored, gh auth status is logged in, and git's global config is read;
    • the step's cached input fell from ~13.9k to ~7.7k tokens.

Deviations / notes for later layers

Closes #63
Part of #62

🤖 Generated with Claude Code

FreshlyBrewedCode and others added 2 commits October 2, 2026 19:28
Turn the finding-13 spike adapter into the production ACP adapter (ADR 0013
§1, §3–§6). It sits beside the opencode adapter; #64 makes it the default.

- acp-agents.ts: agent definitions for `claude` (bundled claude-agent-acp,
  run on the daemon's bun) and `opencode` (`opencode acp`), each with
  `hostSettings: "ignore" | "include"` (default ignore):
  - claude: `_meta.claudeCode.options.settingSources: ["project","local"]`
  - opencode: `XDG_CONFIG_HOME` and `OPENCODE_TEST_HOME` point at empty
    factory-owned dirs; a factory plugin (`shell.env` hook) hands the
    host's `XDG_CONFIG_HOME` back to shell commands so gh and git keep
    their config; the host's `OPENCODE_CONFIG*` variables are dropped
- environment scrub of the parent's `CLAUDECODE` / `CLAUDE_*` variables,
  keeping the credential ones (`CLAUDE_CONFIG_DIR`, OAuth token, provider)
- `checkAgents()`: opencode on PATH, Claude login via the bundled CLI's
  `auth status --json`
- acp-adapter.ts: required model (fails before the prompt, naming the
  agent and a sample of offered ids), permission answered allow_always >
  allow_once, each `usage_update` forwarded in order as a CUSTOM
  `acp.usage` chunk with a new `usage` signal, cancel = session/cancel then
  kill after a grace, kill (and SIGKILL fallback) in finally, agent exit
  mid-turn fails the step with the exit code and stderr tail
- AgentSignal gains `usage` (recorded by #65; the runtime reads past it)
- AgentStepChunkError leads with the adapter's message
- tests against a fake ACP agent over stdio (AgentSideConnection)
- spike runner adapted to the production API, with an `isolation` mode

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Finding 13, addendum 2: Claude's settingSources keeps user skills,
plugins and CLAUDE.md out; opencode's OPENCODE_CONFIG_DIR adds rather than
hides, so XDG_CONFIG_HOME + a shell.env plugin + OPENCODE_TEST_HOME hide
the host's opencode config and home-level skills while gh and git keep
theirs. Also the env scrub and the availability check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@FreshlyBrewedCode
FreshlyBrewedCode added this pull request to stack #70 October 2, 2026 19:28
@FreshlyBrewedCode FreshlyBrewedCode changed the title acp/adapter feat(runtime): production ACP adapter for opencode and Claude Code Oct 2, 2026
FreshlyBrewedCode and others added 2 commits October 2, 2026 19:44
ACP's session/new requires an absolute `cwd`, and claude-agent-acp rejects
anything else. The default workspace root (`.factory/workspaces`) is
relative, so every daemon-allocated run failed its first Claude step with
"`cwd` must be an absolute path". The adapter now resolves the step's dir
against the daemon's working directory, for the spawn and for session/new.
The fake ACP agent validates `cwd` the same way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With host settings ignored, claude-agent-acp still reports the user's
`model` setting (here `haiku`) as the session's current model, while the
Agent SDK, no longer reading user settings, runs its own default. The
adapter skipped `session/set_config_option` when the requested model equalled
that reported value, so a step asking for `haiku` ran on another model: a
live run reported a 1M context window and 0.08 USD for 330 output tokens.
The adapter now always sets the model; a reported current value that is not
in the offered list is still accepted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@FreshlyBrewedCode

Copy link
Copy Markdown
Owner Author

Two fixes landed on this branch from the layer above (#71), both found in live runs through the daemon:

  • 7f9e998 fix(runtime): send ACP an absolute cwd for a relative workspace. The default .factory/workspaces root is relative, and claude-agent-acp rejects a relative session/new cwd.
  • 61856f2 fix(runtime): always pin the session's model over ACP. With host settings ignored, claude-agent-acp still reports the user's model setting as current, so skipping set_config_option on a match ran haiku steps on a 1M-context model.

🤖 Generated with Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(runtime): production ACP adapter for opencode and Claude Code

1 participant