Skip to content

fix(a2a): return GetTask and CancelTask tasks in result - #595

Merged
jpr5 merged 7 commits into
mainfrom
fix/a2a-task-result-shape
Oct 29, 2026
Merged

jpr5 merged 7 commits into
mainfrom
fix/a2a-task-result-shape

Conversation

@jpr5

@jpr5 jpr5 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Bug

The A2A spec defines GetTask and CancelTask as returning a Task, so the JSON-RPC result is the Task itself. aimock returned result: { task: {...} }. The official @a2a-js/sdk client (1.3.0) calls Task.fromJSON(rpcResponse.result), which reads top-level id, contextId and status. With the wrapper, every client.getTask() and client.cancelTask() decoded an empty Task: no id, no status, no history.

Fix

src/a2a-handler.ts: GetTask and CancelTask return the stored task directly in result.

This changes the wire shape (bug fix). Anyone reading result.task from a GetTask or CancelTask response must read result instead. SendMessage (result.task / result.message) and ListTasks (result.tasks) are unchanged. Those shapes already match the spec.

Local red/green proof

The probe starts a real A2AMock from the built dist, creates a task fixture with SendMessage, and calls getTask. It also opens a delayed streaming task and calls cancelTask while that task is WORKING. Both calls go through the @a2a-js/sdk 1.3.0 ClientFactory JSON-RPC client. Raw bodies are shown too. Timestamps are redacted.

RED (origin/main):

@a2a-js/sdk 1.3.0; aimock dist origin/main (912d75c9)
RAW GetTask body: {"jsonrpc":"2.0","id":99,"result":{"task":{"id":"task-Fw68pqtqyRZ4JH6t","contextId":"ctx-Ak6k3yS6OOfsB7jP","status":{"state":"TASK_STATE_COMPLETED","timestamp":"<ts>"},"artifacts":[{"name":"answer","parts":[{"text":"result: 42"}]}],"history":[{"messageId":"msg-yXlaSkXiu0H2nnKp","role":"ROLE_USER","parts":[{"text":"do-work"}]}]}}}
SDK getTask: {"id":"","contextId":"","artifacts":[],"history":[]}
SDK cancelTask: {"id":"","contextId":"","artifacts":[],"history":[]}
RAW CancelTask body: {"jsonrpc":"2.0","id":99,"result":{"task":{"id":"task-Q9FRutK-KNOgPt4-","contextId":"ctx-jx0PZVRBNZ1p7BxB","status":{"state":"TASK_STATE_CANCELED","timestamp":"<ts>"},"artifacts":[],"history":[{"messageId":"msg-itZ-zwVLnf1qvQBd","role":"ROLE_USER","parts":[{"text":"slow-work"}]}]}}}
VERDICT: RED (SDK decoded an empty Task)
EXIT=1

GREEN (this branch):

@a2a-js/sdk 1.3.0; aimock dist this branch
RAW GetTask body: {"jsonrpc":"2.0","id":99,"result":{"id":"task-Y_BDNRn6pv-NqewG","contextId":"ctx-ge8AKfjQLcjy9yz6","status":{"state":"TASK_STATE_COMPLETED","timestamp":"<ts>"},"artifacts":[{"name":"answer","parts":[{"text":"result: 42"}]}],"history":[{"messageId":"msg-PYNcxvDHnDQ7NcZ2","role":"ROLE_USER","parts":[{"text":"do-work"}]}]}}
SDK getTask: {"id":"task-Y_BDNRn6pv-NqewG","contextId":"ctx-ge8AKfjQLcjy9yz6","status":{"state":3,"timestamp":"<ts>"},"artifacts":[{"artifactId":"","name":"answer","description":"","parts":[{"content":{"$case":"text","value":"result: 42"},"filename":"","mediaType":""}],"extensions":[]}],"history":[{"messageId":"msg-PYNcxvDHnDQ7NcZ2","contextId":"","taskId":"","role":1,"parts":[{"content":{"$case":"text","value":"do-work"},"filename":"","mediaType":""}],"extensions":[],"referenceTaskIds":[]}]}
SDK cancelTask: {"id":"task-4rp2DIeeNUoIWdeR","contextId":"ctx-uYlrRtzOPuzseDfd","status":{"state":5,"timestamp":"<ts>"},"artifacts":[],"history":[{"messageId":"msg-ETHVEThYObFp3noQ","contextId":"","taskId":"","role":1,"parts":[{"content":{"$case":"text","value":"slow-work"},"filename":"","mediaType":""}],"extensions":[],"referenceTaskIds":[]}]}
RAW CancelTask body: {"jsonrpc":"2.0","id":99,"result":{"id":"task-F2eCDi4g3hnNejwL","contextId":"ctx-03wArh06vdBeIwVM","status":{"state":"TASK_STATE_CANCELED","timestamp":"<ts>"},"artifacts":[],"history":[{"messageId":"msg-jwNZBU2U_wS7awkz","role":"ROLE_USER","parts":[{"text":"slow-work"}]}]}}
VERDICT: GREEN (SDK decoded full tasks)
EXIT=0

Updated assertions

These existing tests in src/__tests__/a2a-mock.test.ts read GetTask/CancelTask responses. They move from body.result.task.* to body.result.*:

  • SendStreamingMessage: "preserves TASK_STATE_FAILED terminal state after streaming" (GetTask)
  • SendStreamingMessage: "preserves TASK_STATE_CANCELED terminal state after streaming" (GetTask)
  • GetTask: "returns stored task"
  • CancelTask: "cancels a non-terminal task". The CancelTask result.task toBeDefined check is now result.id === taskId, and the follow-up GetTask check is updated the same way.
  • streaming task stored in tasks map: "task created by streaming is retrievable via GetTask"

New regression test: "returns the Task itself as result for GetTask and CancelTask (A2A spec shape)". It asserts that there is no task key and that id, contextId, status, artifacts and history are at the top level of result. With the new test file against the old handler, 6 tests fail: the new test plus the 5 updated ones. All pass with the fix.

Other-method audit (reported, not fixed here)

These were checked against the @a2a-js/sdk 1.3.0 decoders (SendMessageResponse, StreamResponse, ListTasksResponse, Task, Artifact):

  • SendMessage: OK. {task} and {message} match SendMessageResponse. The task result leaves out history, which is optional.
  • ListTasks: OK. {tasks:[...]} matches ListTasksResponse. nextPageToken, pageSize and totalSize are absent, and the SDK defaults them.
  • SendStreamingMessage, artifact events: MISMATCH. Each artifact event is {task:{...}, artifact:{...}}. StreamResponse.fromJSON picks the task case first, so the SDK drops the artifact on every artifact event. The spec shape is {artifactUpdate:{taskId, contextId, artifact, append, lastChunk}}.
  • SendStreamingMessage, status events: they are sent as {task:{id,contextId,status}} snapshots instead of {statusUpdate:{taskId,contextId,status}}. The SDK decodes them as Task events, so this works, but it does not follow the spec.
  • Artifacts: they have no artifactId, which A2A requires. The SDK decodes it as "".
  • Stream after CancelTask: the stream keeps running. A later scripted status event overwrites TASK_STATE_CANCELED, and the task ends TASK_STATE_COMPLETED (seen in the probe).

Checks

Full suite: 484 files, 19704 passed and 0 failed. Typecheck, lint, prettier and build are clean. The api report has no change, because no public types changed. Docs: no page documents the GetTask/CancelTask result shape, so none needed an update.

jpr5 added 3 commits October 29, 2026 15:25
The A2A spec defines GetTask and CancelTask as returning a Task, so the
JSON-RPC result is the Task itself. aimock wrapped it as { task }, and
the @a2a-js/sdk client decoded an empty Task (no id, status or history).
Move GetTask/CancelTask assertions off the result.task wrapper and add a
regression test that pins the spec shape for both methods.
@pkg-pr-new

pkg-pr-new Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@copilotkit/aimock@595

commit: 5cfe76b

@jpr5
jpr5 merged commit 905fb0c into main Oct 29, 2026
30 checks passed
@jpr5
jpr5 deleted the fix/a2a-task-result-shape branch October 29, 2026 23:10
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.

1 participant