Skip to content

fix: pass acp-tck for v1 and v2 agents - #150

Merged
EugeneTheDev merged 7 commits into
masterfrom
eugenethedev/tck-fixes
Oct 21, 2026
Merged

EugeneTheDev merged 7 commits into
masterfrom
eugenethedev/tck-fixes

Conversation

@EugeneTheDev

@EugeneTheDev EugeneTheDev commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes the SDK behavior that made acp-tck fail. With these changes, both the v1 and v2 agents pass acp-tck at the pinned revision (3d46e21). One fix per commit.

Behavior changes

  • session/resume without mcpServers is accepted (v1). The v1 schema makes the field optional, but the SDK rejected such requests.
  • session/load can replay history before its response (v1). The runtime calls the new AgentSession.replayHistory() after the session is registered and before the response is sent. If the hook throws, session/load fails and the session is unregistered. Fixes LOAD-002.
  • Requests and notifications without params are decoded as {} (v1 and v2). Types whose fields all have defaults no longer fail to decode; session/list is one example.
  • Params that don't decode get -32602 (Invalid params) (v1 and v2). They used to get -32700 (Parse error), or -32603 when a custom serializer rejected the value. A client now sees AcpExpectedError where it used to see SerializationException. Fixes RESUME-208 and the v1 advisory ERROR-002.
  • The v2 Agent negotiates initialize as the v2 spec requires (upstream 26008c5f). A request for version 2 or higher is answered with 2. A lower version is refused with -32022 (Unsupported protocol version) and {"requested": n, "supported": [2]} as data. A missing, non-integer, or out-of-range (outside uint16) version gets -32602. Fixes INIT-003/202/205/206.
  • The v2 Agent serves nothing but initialize until initialize succeeds. Other methods get -32600, as in the Rust SDK, so no session work starts on a connection whose handshake was refused. Fixes INIT-207.
  • The v2 session/close sends the cancelled turn's final updates before it answers. If a turn is running, the Agent cancels it through AgentSession.cancel(), as session/cancel does, and waits for it to end before it calls AgentSupport.closeSession. Fixes CLOSE-203.
  • v2 TCK agent: on session/resume it sends the current state_update snapshot and refuses unsupported replayFrom cursor types with -32602. Fixes RESUME-206/207.

Public API changes

  • New AgentSession.replayHistory() (acp/api/acp.api). Its default implementation does nothing. An agent that supports session/load must replay history here to comply with the spec. Updates sent from postInitialize arrive after the session/load response.
  • New JsonRpcErrorCode.UNSUPPORTED_PROTOCOL_VERSION (-32022), marked @UnstableApi (acp-model/api/acp-model.api).
  • ResumeSessionRequest.mcpServers (stable model) now defaults to emptyList(). The .api dump doesn't change, because the defaults overload already existed.
  • KDoc only: v2.AgentSupport.resumeSession now says the implementation must send the current-state state_update after any replay and must reject cursors it doesn't support with -32602. Only the implementation knows whether a session is idle, running, or waiting for the user, so the SDK doesn't send the snapshot itself.

Remaining

  • v1 SESSION-004 (advisory): the agent SHOULD connect to the MCP servers given in session/new. The TCK agent has no MCP client, so this is a gap in the TCK agent, not an SDK defect.

Verification

  • ./test-tck/run_tck.sh 1 → CONFORMANT (1 advisory failure, SESSION-004); ./test-tck/run_tck.sh 2 → CONFORMANT, no failures.
  • ./gradlew check passes.
  • Regression tests: ResumeSessionSerializationTest, SimpleAgentTest (replay order and replay failure), ProtocolTest (missing params, undecodable params → -32602), AgentTest (v2 negotiation, -32022 data, malformed versions, initialization gate), PromptReceiptTest (close mid-turn and close when idle), V2ClientTest (v1 client refused with -32022).

@slapoguzov
slapoguzov self-requested a review October 20, 2026 15:13
The v1 schema (agent-client-protocol 6d08f412) requires only `sessionId`
and `cwd` in `ResumeSessionRequest`, but the model required `mcpServers`,
so a conforming request failed to decode. Default it to an empty list,
as the v2 model already does.
ACP requires the agent to replay the conversation through session/update
before it answers session/load. The only session-level hook,
postInitialize, runs after the response, so an agent on AgentSession
could not meet that order without sending raw notifications from
AgentSupport.loadSession.

Add AgentSession.replayHistory(). The runtime calls it during
session/load after registering the session and before sending the
response, with client operations available in the coroutine context. If
it throws, session/load fails and the session is unregistered. The v1
TCK agent now replays history there.

Fixes acp-tck ACP-LOAD-002.
JSON-RPC lets a request or notification omit `params`, but the typed
handlers decoded a missing `params` as `null`. Even a type whose fields
all have defaults, such as `session/list`'s ListSessionsRequest, was
rejected with -32700. Decode a missing `params` as an empty object, as
sendRequest already does for a missing result.
A request whose `params` did not decode into the method's type was
answered with -32700 (Parse error), and a value a custom serializer
refused, such as a non-object `replayFrom`, with -32603. The message had
already been parsed, so JSON-RPC calls this Invalid params (-32602).

Decode params in one place for the typed request and notification
handlers, and map a decoding failure there to -32602. A SerializationException
raised anywhere else keeps its mapping.

Fixes acp-tck v2 ACP-RESUME-208 and the v1 advisory ACP-ERROR-002.
agent-client-protocol 26008c5f (unstable v2) changed version
negotiation: the agent selects the highest version it supports at or
below the requested one. If there is none, it refuses `initialize` with
-32022 (Unsupported protocol version), and the error data holds
`requested` and `supported`. A missing, non-integer, or out-of-range
`protocolVersion` gets -32602, and a refused `initialize` does not
initialize the connection.

The v2 Agent now answers a request for version 2 or later with version
2. It refuses a lower version, v1-shaped or not, with -32022 and
`{"requested": n, "supported": [2]}`, and a version outside the uint16
range with -32602. Every method but `initialize` is refused with -32600
until `initialize` succeeds, as the Rust SDK does, so no session work
starts on a connection whose handshake failed. Add
JsonRpcErrorCode.UNSUPPORTED_PROTOCOL_VERSION, marked unstable.

Fixes acp-tck v2 ACP-INIT-003, ACP-INIT-202, ACP-INIT-205, ACP-INIT-206
and ACP-INIT-207.
…on/close

ACP requires an agent that cancels active work on `session/close` to send
that work's final updates and the idle `state_update` with the
`cancelled` stop reason before it answers. The v2 Agent handed
`session/close` straight to AgentSupport.closeSession, so the answer
could overtake the turn's last updates.

If a turn is running, the Agent now cancels it through
AgentSession.cancel, as `session/cancel` does, and waits for it to end
before it calls closeSession. Closing an idle session cancels nothing.

Fixes acp-tck v2 ACP-CLOSE-203.
… cursors

ACP requires every successful `session/resume` to send the session's
current foreground state as a `state_update` after any replay and before
the response, also when `replayFrom` is omitted. An agent must refuse a
`replayFrom` cursor type it does not support with -32602 instead of
resuming without replay.

Only the implementation knows whether a session is idle, running, or
waiting for the user, so AgentSupport.resumeSession now documents both
duties instead of the SDK guessing. The v2 TCK agent keeps the state it
last reported, sends it when it resumes a session, and refuses
ReplayFrom.Unknown.

Fixes acp-tck v2 ACP-RESUME-206 and ACP-RESUME-207.
@EugeneTheDev
EugeneTheDev force-pushed the eugenethedev/tck-fixes branch from 7537519 to aecc730 Compare October 20, 2026 17:23
@EugeneTheDev
EugeneTheDev merged commit e5f8c31 into master Oct 21, 2026
4 checks passed
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.

2 participants