FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(acp): add ACP v2 draft support/features by mountaintopsolutions · Pull Request #44524 · anomalyco/opencode · GitHub

feat(acp): add ACP v2 draft support/features - #44524

Open
mountaintopsolutions wants to merge 6 commits into
anomalyco:devfrom
mountaintopsolutions:acp-v2-steering
Open

mountaintopsolutions wants to merge 6 commits into
anomalyco:devfrom
mountaintopsolutions:acp-v2-steering

Conversation

mountaintopsolutions commented Aug 23, 2026
edited
Loading

Copy link
Copy Markdown

Issue for this PR

Closes #44877

This is a WIP implementation of the ACP v2 draft spec per the migration guide.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Implements ACP v2 draft support behind OPENCODE_EXPERIMENTAL_ACP_V2, gated by protocolVersion negotiation. v1 peers are unaffected — the agent answers protocolVersion: 1 unless the flag is on AND the client requests v2.

Upgrades @agentclientprotocol/sdk from 0.21.0 → 1.3.0. Uses the SDK v2 experimental builder (@agentclientprotocol/sdk/experimental/v2) for v2 connections and the v1 builder for v1 connections, with AgentProtocolRouter selecting per-connection based on initialize protocolVersion.

Architecture: AgentProtocolRouter (agent.ts) selects v1 or v2 per-connection. v1 uses createV1App() (v1 AgentApp builder). v2 uses createV2App() (agent-v2.ts, v2 AgentApp builder). Both share ACPService.make() core logic. v2 ndJsonStream from the SDK accepts both individual and batch JSON-RPC messages. ACP over stdio is inherently single-connection-per-process — v2Active is connection-scoped in practice (see comment in service.ts).

All items from the v2 migration checklist are implemented:

  • Prompt lifecycle — session/prompt returns {} on acceptance (via prompt_async); foreground state flows as state_update notifications (running on busy, idle with stopReason on turn completion)
  • Initialize restructure — capabilities/info fields, object support markers, session-scoped capability groups; protocolVersion: 2 negotiation with v1 fallback
  • Cancel confirmation — session/cancel still a notification; confirmation is state_update: idle with stopReason: cancelled (derived from MessageAbortedError), not the prompt response
  • Mid-turn steering — a second session/prompt admitted while a turn is running steers by default at the next safe boundary
  • Message IDs — messageId on all chunks; whole-message upserts (user_message) with acknowledgment
  • Tool calls — stop emitting tool_call (create) in v2; use tool_call_update for create+patch
  • Agent-owned terminal display — terminal_update upserts with base64-encoded output.data and exitStatus (reading exit code from part.state.metadata.exit) on completion
  • Structured diffs — replace oldText/newText with changes array + git_patch format; guards oldText/newText are strings before patch generation
  • Permission requests — required title, optional description, extensible subject union (tool_call / command)
  • Plans — plan_update with planId keyed tagged union (type: "items")
  • Session lifecycle — implement session/resume with replayFrom; paginated message fetch (50/page via cursor) for replay
  • Session modes removed — express as config options with category: mode and configId naming
  • Auth renames — authenticate → auth/login, logout → auth/logout, type + methodId on descriptors
  • MCP config — type discriminator (http/sse/acp/stdio), advertise session.mcp.stdio/session.mcp.http; acp type filtered before registration
  • Slash commands — input: { type: "text", hint } discriminator
  • JSON-RPC batch — accept and process batch arrays on stdio (v2 ndJsonStream)
  • Consistent ID naming — id → configId/methodId
  • v1/v2 side-by-side — AgentProtocolRouter routes per-connection; v1 fallback verified
  • Extensibility — open enums, unknown content block types rendered as text fallback, _meta preserved on content chunks and resource_link blocks
  • Content blocks — resource_link icons/_meta passthrough, extensible type discriminator, unknown block types preserved
  • requires_action state_update — emits requires_action before permission requests, running after resolution; deduplicated running via v2Running set

SDK 1.3.0 breaking changes addressed:

  • SDK 1.3.0 removed the unstable SetSessionModelRequest type (which carried modelId for LLM model selection). This is distinct from SetSessionModeRequest (which carries modeId for session mode selection). Defined a local SetSessionModelRequest type to preserve the correct semantics for the unstable_setSessionModel extension method.
  • PromptRequest.messageId removed from v1 types
  • PromptResponse.userMessageId removed from v1 types
  • McpServer is now a discriminated union (http/sse/acp/stdio) — added type discriminators to all MCP server configs
  • messageID must start with "msg" per the server validation — switched from crypto.randomUUID() to Identifier.ascending("message")

How did you verify your code works?

Automated tests:

  • bun test test/acp/ — 155 pass (20 new v2 unit tests)
  • bun test test/cli/acp/ — 17 pass (12 v1 wire path + 5 v2 wire path CLI subprocess tests)
  • bun typecheck — clean across all 30 packages

New v2 unit tests:

  • test/acp/event-v2.test.ts (11 tests): tool_call_update for creation, structured diffs with changes + git_patch, terminal_update for running/completed bash tools with exitStatus, plan_update for todo events, no terminal_update for non-shell tools, _meta preserved on resource_link/text content chunks, unknown block type text fallback, _meta preservation from incoming resource_link
  • test/acp/permission.test.ts (3 tests): requires_action before permission request, running after resolution, requires_action on rejection, v2 permission structure with title/subject
  • test/acp/service-session.test.ts (6 tests): configId naming, user_message ack with messageId, auth/login + auth/logout routing, requires_action baseline, slash command input discriminator, replayFrom replay

v1 backward compatibility is verified by the existing CLI test suite (lifecycle, prompt-content, config-options, skills, initialize-auth) which all use protocolVersion: 1 and exercise the full v1 wire path against SDK 1.3.0.

Manual end-to-end testing:

A standalone test client is available on the acp-v2-test-client branch of the fork. It uses the SDK's built-in ClientApp from @agentclientprotocol/sdk/experimental/v2 to spawn an opencode ACP process, negotiate v2, create a session, send prompts, and stream all session/update notifications.

# Basic prompt
bun run test/cli/acp/v2-client.ts --binary ./dist/opencode-darwin-arm64/bin/opencode "say hello"

# With a specific model (writes opencode.json in cwd)
bun run test/cli/acp/v2-client.ts --binary ./dist/opencode-darwin-arm64/bin/opencode \
  --model opencode/big-pickle "say hello"

# Mid-turn steering
bun run test/cli/acp/v2-client.ts --binary ./dist/opencode-darwin-arm64/bin/opencode \
  --model opencode/big-pickle \
  --steer "STOP. Just say STEERED." --steer-delay 500 \
  "write a detailed Python script that prints 1-100 with comments"

# Session resume with replay
bun run test/cli/acp/v2-client.ts --binary ./dist/opencode-darwin-arm64/bin/opencode \
  --resume <sessionId> "continue"

# Predefined scenarios
bun run test/cli/acp/v2-client.ts --binary ./dist/opencode-darwin-arm64/bin/opencode \
  --model opencode/big-pickle --scenario steering
bun run test/cli/acp/v2-client.ts --binary ./dist/opencode-darwin-arm64/bin/opencode \
  --model opencode/big-pickle --scenario cancel
bun run test/cli/acp/v2-client.ts --binary ./dist/opencode-darwin-arm64/bin/opencode \
  --model opencode/big-pickle --scenario config-options
bun run test/cli/acp/v2-client.ts --binary ./dist/opencode-darwin-arm64/bin/opencode \
  --model opencode/big-pickle --scenario resume
bun run test/cli/acp/v2-client.ts --binary ./dist/opencode-darwin-arm64/bin/opencode \
  --model opencode/big-pickle --sequence file-then-list
bun run test/cli/acp/v2-client.ts --binary ./dist/opencode-darwin-arm64/bin/opencode \
  --model opencode/big-pickle --scenario batch

Scenario test results (using opencode/big-pickle):

Scenario Description Result
steering Send a second session/prompt mid-turn while the model is still working PASS — steer accepted, second user_message emitted with new messageId, state_update: running re-emitted, turn completed with end_turn
cancel Send session/cancel notification on first state_update: running PASS — turn completed with stopReason: cancelled
config-options Verify all config options use configId (not v1 id), and session/set_config_option with type: "id" updates the value PASS — model, effort, mode all have configId; mode switched from build to plan successfully
resume Create session + prompt, then session/resume with replayFrom: { type: "start" }, verify configOptions in response and session appears in session/list PASS — resume returned 3 config options, session found in list
file-then-list Prompt that triggers file write + bash tool, then session/list PASS — turn completed, session/list returned the session (model did not use tools in this run, but wire path verified)
batch Send a raw JSON-RPC batch array (two session/list requests) on stdin PASS — server returned a batch array with 2 results, each containing the session list

Screenshots / recordings

N/A — no UI changes.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

github-actions Bot added the needs:compliance This means the issue will auto-close after 2 hours. label Aug 23, 2026
mountaintopsolutions marked this pull request as draft August 23, 2026 20:25
github-actions Bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Aug 23, 2026

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

mountaintopsolutions changed the title feat(acp): add v2 prompt lifecycle with mid-turn steering feat(acp): add ACP v2 draft support/features Aug 23, 2026

Copy link
Copy Markdown

AI code review — automated review for reference, author can ignore or act on any point.

Overall: impressive scope for a draft-protocol integration — the boundary-cast strategy is documented at every cast site, the v2 prompt lifecycle (async admit + state_update driving) is a sensible mapping onto opencode's model, and the test coverage (event-v2, service-session, e2e v2-prompt) is substantial. Concerns, roughly in priority order:

  1. Connection-scoped state is process-global — packages/opencode/src/acp/service.ts:~97 flips a single v2Active for the whole service once any client negotiates v2, and both agent.ts:createV1App/agent-v2.ts:~25 capture the first connection's client wrapper into a singleton service. A second, concurrently connected client (e.g. two editors against one opencode acp process) would receive the other dialect's notifications or have its updates routed to the first client. If one-connection-per-process is guaranteed today, say so in a comment; otherwise key these by connection/session ID.

  2. packages/opencode/src/acp/event.ts:~455-459 — const toolCall = pendingToolCall(...) is computed then never used; both branches rebuild it. Drop the variable and hoist the single call into both update payloads.

  3. packages/opencode/src/acp/event.ts:~480-508 — exitStatus reads input.exitCode, which the bash tool doesn't populate, so every command reports exit 0 even on failure. part.state.metadata is where opencode records exit codes — prefer that before defaulting.

  4. packages/opencode/src/acp/service.ts:mcpConfig — the new acp case returns undefined; please confirm registerMcpServers filters undefined results rather than passing them into the SDK request (a type: undefined entry would fail server-side validation).

Minor: resumeSession drops the limit: 20 whenever replayFrom is present, making an unbounded messages fetch per resume — fine for now, worth a cap later; v2 prompts emit state_update running immediately and via onBusy on the first busy event (duplicate notifications); v2DiffContent guards only path but assumes oldText/newText are strings; and the "======" line filter is safe only because unified-diff body lines carry /+/- prefixes — a comment would prevent future breakage. Nice work overall.

Copy link
Copy Markdown
Author

Thanks for the review — all concerns addressed in the latest commits.

  1. Connection-scoped state — Documented the single-connection constraint. ACP over stdio is inherently 1:1 (one stdin/stdout pair per router.connect() call), so v2Active is connection-scoped in practice. Added a comment noting that multi-connection transport (e.g. WebSocket) would require keying by connection ID.

  2. Unused toolCall variable — Removed. Single pendingToolCall call shared across both branches via a ternary on the sessionUpdate discriminator.

  3. exitStatus reading input.exitCode — Fixed. Now reads part.state.metadata.exit (where opencode records exit codes in shell.ts). Falls back to 0 only when metadata is unavailable.

  4. mcpConfig acp case returning undefined — Fixed. Added .filter((entry) => entry.config !== undefined) before the registration pipeline so undefined configs never reach sdk.mcp.add.

Minor items:

  • Duplicate state_update running — Added a v2Running set that tracks sessions already in the running state. The eager emission on prompt acceptance marks the session; onBusy skips if already running. Cleared on idle.
  • v2DiffContent guards — Added type checks for oldText/newText being strings before passing to createTwoFilesPatch.
  • ======= filter comment — Added comment explaining that body lines always start with , +, or -, so the filter is safe.
  • Unbounded messages fetch on replay — Replaced with paginated retrieval using the session.messages cursor (50 per page, looping on cursor.next until exhausted). Non-replay resumes keep the existing limit: 20.

github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. labels Aug 25, 2026

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

mountaintopsolutions marked this pull request as ready for review August 25, 2026 04:27
mountaintopsolutions force-pushed the acp-v2-steering branch 3 times, most recently from 852dab5 to c0b5879 Compare September 6, 2026 02:17
Implements the ACP v2 draft prompt lifecycle behind
OPENCODE_EXPERIMENTAL_ACP_V2, gated by protocolVersion negotiation.
v1 peers are unaffected.

- session/prompt returns {} on acceptance (via prompt_async) instead of
  blocking for the whole turn; a second prompt admitted mid-turn steers
  by default at the next safe boundary
- emit v2 session/update notifications: user_message ack on acceptance,
  state_update running on busy, state_update idle with stopReason on
  turn completion (derived from the latest assistant message)
- session/cancel still routes through the existing abort path; the
  cancelled stopReason is derived from MessageAbortedError at idle
- initialize negotiates protocolVersion 2 when the flag is on and the
  client requests v2; otherwise falls back to v1
- v2 state_update/user_message variants are cast at the wire boundary
  since SDK 0.21 ships only v1 SessionUpdate types

https://agentclientprotocol.com/announcements/acp-v2-draft
…update, and plan_update

- Add AgentProtocolRouter for per-connection v1/v2 selection
- Implement v2 app builder (agent-v2.ts) with wrapClient
- Implement structured diffs (changes array + git_patch format)
- Implement terminal_update for bash/shell tools with exitStatus
- Implement plan_update for todo.updated events in v2 mode
- Implement replayFrom on session/resume
- Implement v2 permission request structure (title, subject union)
- Implement configId naming in v2 config options
- Implement auth/login + auth/logout routing
- Implement tool_call_update for creation in v2 mode
- Implement user_message acknowledgment emission
- Implement slash command input type:text discriminator
- Add v2 ndJsonStream with JSON-RPC batch support
- Upgrade @agentclientprotocol/sdk from 0.21.0 to 1.3.0
- Add 12 new v2 tests covering all implemented features
…ity, and _meta passthrough

- Emit requires_action state_update before permission requests, running after resolution
- Preserve _meta on resource_link and text content chunks output
- Render unknown content block types as text fallback when _meta is present
- Pass through _meta from incoming resource_link blocks as metadata on text parts
- Add _meta field to ReplayPart types for metadata propagation
- Add 8 new tests: requires_action before/after permission, v2 permission structure,
  _meta on resource_link/text chunks, unknown block fallback, _meta preservation
… SetSessionModeRequest

SDK 1.3.0 removed the unstable SetSessionModelRequest type (which carried
modelId for LLM model selection). The previous workaround retyped
setSessionModel to use SetSessionModeRequest, but that type carries modeId
(session mode like 'build'), not modelId (LLM model like
'anthropic/claude-3.5-sonnet'). Define a local type to preserve the correct
semantics.
- Fix exitStatus reading input.exitCode (never populated) instead of
  part.state.metadata.exit (where opencode records exit codes)
- Remove unused toolCall variable in toolStart, deduplicate pendingToolCall call
- Filter undefined mcpConfig results (acp type) before passing to sdk.mcp.add
- Deduplicate state_update running: track v2Running set so onBusy doesn't
  emit a second running after the eager emission on prompt acceptance
- Guard v2DiffContent against non-string oldText/newText
- Add comment explaining the ======= line filter safety in v2DiffContent
- Document single-connection constraint on v2Active (ACP stdio is 1:1)
- Add TODO for unbounded messages fetch in resumeSession replay
Replace unbounded messages fetch with paginated retrieval using the
session.messages cursor (50 per page). Non-replay resumes keep the
existing limit:20 behavior.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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.

[FEATURE]: ACP v2 draft spec support

2 participants


Back | FazBrowse Home | New Git URL