Repository navigation
feat(acp): add ACP v2 draft support/features - #44524
mountaintopsolutions wants to merge 6 commits into
Conversation
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
9cd7ac9 to
26289f1
Compare
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:
Minor: |
|
Thanks for the review — all concerns addressed in the latest commits.
Minor items:
|
d491a49 to
e67bc25
Compare
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
e67bc25 to
4e6d64d
Compare
852dab5 to
c0b5879
Compare
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.
c0b5879 to
c0b8510
Compare
|
Automated PR Cleanup Thank you for contributing to opencode. Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions. This PR was closed because it matched the following cleanup criteria:
PRs created within the last month are not affected by this cleanup. If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate. Thanks again for taking the time to contribute. |
Issue for this PR
Closes #44877
This is a WIP implementation of the ACP v2 draft spec per the migration guide.
Type of change
What does this PR do?
Implements ACP v2 draft support behind
OPENCODE_EXPERIMENTAL_ACP_V2, gated byprotocolVersionnegotiation. v1 peers are unaffected — the agent answersprotocolVersion: 1unless the flag is on AND the client requests v2.Upgrades
@agentclientprotocol/sdkfrom 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, withAgentProtocolRouterselecting per-connection based oninitializeprotocolVersion.Architecture:
AgentProtocolRouter(agent.ts) selects v1 or v2 per-connection. v1 usescreateV1App()(v1AgentAppbuilder). v2 usescreateV2App()(agent-v2.ts, v2AgentAppbuilder). Both shareACPService.make()core logic. v2ndJsonStreamfrom the SDK accepts both individual and batch JSON-RPC messages. ACP over stdio is inherently single-connection-per-process —v2Activeis connection-scoped in practice (see comment inservice.ts).All items from the v2 migration checklist are implemented:
session/promptreturns{}on acceptance (viaprompt_async); foreground state flows asstate_updatenotifications (runningon busy,idlewithstopReasonon turn completion)capabilities/infofields, object support markers, session-scoped capability groups;protocolVersion: 2negotiation with v1 fallbacksession/cancelstill a notification; confirmation isstate_update: idlewithstopReason: cancelled(derived fromMessageAbortedError), not the prompt responsesession/promptadmitted while a turn is running steers by default at the next safe boundarymessageIdon all chunks; whole-message upserts (user_message) with acknowledgmenttool_call(create) in v2; usetool_call_updatefor create+patchterminal_updateupserts with base64-encodedoutput.dataandexitStatus(reading exit code frompart.state.metadata.exit) on completionoldText/newTextwithchangesarray +git_patchformat; guardsoldText/newTextare strings before patch generationtitle, optionaldescription, extensiblesubjectunion (tool_call/command)plan_updatewithplanIdkeyed tagged union (type: "items")session/resumewithreplayFrom; paginated message fetch (50/page via cursor) for replaycategory: modeandconfigIdnamingauthenticate→auth/login,logout→auth/logout,type+methodIdon descriptorstypediscriminator (http/sse/acp/stdio), advertisesession.mcp.stdio/session.mcp.http;acptype filtered before registrationinput: { type: "text", hint }discriminatorndJsonStream)id→configId/methodIdAgentProtocolRouterroutes per-connection; v1 fallback verified_metapreserved on content chunks andresource_linkblocksresource_linkicons/_metapassthrough, extensibletypediscriminator, unknown block types preservedrequires_actionbefore permission requests,runningafter resolution; deduplicatedrunningviav2RunningsetSDK 1.3.0 breaking changes addressed:
SetSessionModelRequesttype (which carriedmodelIdfor LLM model selection). This is distinct fromSetSessionModeRequest(which carriesmodeIdfor session mode selection). Defined a localSetSessionModelRequesttype to preserve the correct semantics for theunstable_setSessionModelextension method.PromptRequest.messageIdremoved from v1 typesPromptResponse.userMessageIdremoved from v1 typesMcpServeris now a discriminated union (http/sse/acp/stdio) — addedtypediscriminators to all MCP server configsmessageIDmust start with"msg"per the server validation — switched fromcrypto.randomUUID()toIdentifier.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 packagesNew v2 unit tests:
test/acp/event-v2.test.ts(11 tests):tool_call_updatefor creation, structured diffs withchanges+git_patch,terminal_updatefor running/completed bash tools withexitStatus,plan_updatefor todo events, noterminal_updatefor non-shell tools,_metapreserved onresource_link/text content chunks, unknown block type text fallback,_metapreservation from incomingresource_linktest/acp/permission.test.ts(3 tests):requires_actionbefore permission request,runningafter resolution,requires_actionon rejection, v2 permission structure withtitle/subjecttest/acp/service-session.test.ts(6 tests):configIdnaming,user_messageack withmessageId,auth/login+auth/logoutrouting,requires_actionbaseline, slash commandinputdiscriminator,replayFromreplayv1 backward compatibility is verified by the existing CLI test suite (
lifecycle,prompt-content,config-options,skills,initialize-auth) which all useprotocolVersion: 1and 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-clientbranch of the fork. It uses the SDK's built-inClientAppfrom@agentclientprotocol/sdk/experimental/v2to spawn an opencode ACP process, negotiate v2, create a session, send prompts, and stream allsession/updatenotifications.Scenario test results (using opencode/big-pickle):
steeringsession/promptmid-turn while the model is still workinguser_messageemitted with newmessageId,state_update: runningre-emitted, turn completed withend_turncancelsession/cancelnotification on firststate_update: runningstopReason: cancelledconfig-optionsconfigId(not v1id), andsession/set_config_optionwithtype: "id"updates the valuemodel,effort,modeall haveconfigId; mode switched frombuildtoplansuccessfullyresumesession/resumewithreplayFrom: { type: "start" }, verifyconfigOptionsin response and session appears insession/listfile-then-listsession/listsession/listreturned the session (model did not use tools in this run, but wire path verified)batchsession/listrequests) on stdinScreenshots / recordings
N/A — no UI changes.
Checklist