Skip to content

feat(a2a): add a route to continue a task with a follow-up message - #7374

Open
marekdano wants to merge 3 commits into
mainfrom
6703-a2a-continue-task
Open

marekdano wants to merge 3 commits into
mainfrom
6703-a2a-continue-task

Conversation

@marekdano

Copy link
Copy Markdown
Collaborator

Summary

A caller can now answer an agent that paused in an input-required or auth-required state. Before, the invoke routes had no task identifier, so interaction stopped at a single-shot request and response.

Closes #6703
Part of #5936

Depends on #7372, which adds the routes that read task state. Merge #7372 first. This branch does not include that change. After #7372 merges, rebase onto main.

Changes

New route: POST /v1/a2a/{agent_id}/tasks/{task_id}/messages, with permission a2a.invoke.

  • The body holds either message (an A2A message object) or text (one text part). The request needs exactly one of them. message.parts must not be empty.
  • A2AAgentService.continue_task() finds the task with the same visibility scoping as get_task. It adds taskId and the stored contextId to the message, and defaults messageId and role.
  • The message goes through invoke_agent. A2A 0.3 and 1.0 both carry taskId and contextId on the message, and the existing normalization converts the rest. A 1.0 agent gets SendMessage with ROLE_USER. A 0.3 agent gets message/send with kind markers.
  • The task row takes its state from the agent response, through the existing persistence in invoke_agent.
  • A task_continued event in a2a_task_events records the previous and new state, with a per-task sequence number. A failed event write is logged and does not fail the request, because the agent already received the message.
  • The route returns the agent response unchanged.
  • TokenScopingMiddleware maps the new path to a2a.invoke.
Condition Response
A2A service unavailable 503
Task does not exist or is not visible 404
Task is in a terminal state 409, names the state
message.taskId names a different task 400
Invalid body 422

Related fix

cancel_task and upsert_task only recognized the 0.3 terminal states. Cancelling a finished 1.0 task, stored as TASK_STATE_COMPLETED, overwrote its state. Both now use the new is_terminal_task_state() helper, which also drives the 409 check.

Limits

  • The route takes the agent UUID only, not a UAID, because a UAID can contain slashes.
  • If an agent answers a follow-up with a different task ID, the original row stays unchanged.
  • The route does not serialize two simultaneous follow-ups to one task.

Checklist

  • Tests cover task binding, both protocol versions, the 404, 409, 400, 422 and 503 paths, the event sequence, and the terminal-state fix
  • make pre-commit ruff interrogate pylint verify pass
  • CHANGELOG entry added

@Lang-Akshay Lang-Akshay left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking Changes

# Area File Line Blocking reason Required change
1 Security (subagent SECURITY_VERDICT=BLOCK) / Issue mcpgateway/services/a2a_service.py 3615-3617 {"messageId": ..., "role": "user", **message, "taskId": task_id} lets the client override the gateway-generated messageId and role. The contextId guard (if task.get("contextId") and not follow_up.get("contextId")) only fills a missing value. A client-supplied contextId is forwarded and stored in the task_continued event. taskId mismatch returns 400, but contextId mismatch passes silently. The PR claims the message is bound to the stored task and context. Set contextId from the stored task after the spread, or reject a mismatching value with 400 as for taskId. Set role and messageId after the spread, or reject client values. Add a regression test for each override.

marekdano added a commit that referenced this pull request Oct 29, 2026
Address review feedback on #7374 (issue #6703).

  Stop a client from overriding fields the gateway owns when it continues
  a task. The spread of the client message could replace messageId and
  role, and a client contextId was forwarded and logged.

  - Set taskId and contextId from the stored task. Reject a client value
    that differs, including a contextId on a task that has none.
  - Always send role as the user role. Accept the user and ROLE_USER
    spellings. Reject any other role.
  - Always generate messageId in the gateway.
  - Add a regression test for each override and for each accepted case.

Signed-off-by: Marek Dano <mk.dano@gmail.com>
@marekdano marekdano self-assigned this Oct 30, 2026
marekdano added a commit that referenced this pull request Oct 30, 2026
Address review feedback on #7374 (issue #6703).

  Stop a client from overriding fields the gateway owns when it continues
  a task. The spread of the client message could replace messageId and
  role, and a client contextId was forwarded and logged.

  - Set taskId and contextId from the stored task. Reject a client value
    that differs, including a contextId on a task that has none.
  - Always send role as the user role. Accept the user and ROLE_USER
    spellings. Reject any other role.
  - Always generate messageId in the gateway.
  - Add a regression test for each override and for each accepted case.

Signed-off-by: Marek Dano <mk.dano@gmail.com>
@marekdano
marekdano force-pushed the 6703-a2a-continue-task branch from d3c8630 to f08c76f Compare October 30, 2026 12:12
marekdano added a commit that referenced this pull request Oct 30, 2026
Address review feedback on #7374 (issue #6703).

  Stop a client from overriding fields the gateway owns when it continues
  a task. The spread of the client message could replace messageId and
  role, and a client contextId was forwarded and logged.

  - Set taskId and contextId from the stored task. Reject a client value
    that differs, including a contextId on a task that has none.
  - Always send role as the user role. Accept the user and ROLE_USER
    spellings. Reject any other role.
  - Always generate messageId in the gateway.
  - Add a regression test for each override and for each accepted case.

Signed-off-by: Marek Dano <mk.dano@gmail.com>
@marekdano
marekdano force-pushed the 6703-a2a-continue-task branch from 727a397 to f07080f Compare October 30, 2026 15:13
@marekdano
marekdano requested a review from Lang-Akshay October 30, 2026 15:13
marekdano added a commit that referenced this pull request Nov 3, 2026
Address review feedback on #7374 (issue #6703).

  Stop a client from overriding fields the gateway owns when it continues
  a task. The spread of the client message could replace messageId and
  role, and a client contextId was forwarded and logged.

  - Set taskId and contextId from the stored task. Reject a client value
    that differs, including a contextId on a task that has none.
  - Always send role as the user role. Accept the user and ROLE_USER
    spellings. Reject any other role.
  - Always generate messageId in the gateway.
  - Add a regression test for each override and for each accepted case.

Signed-off-by: Marek Dano <mk.dano@gmail.com>
@marekdano
marekdano force-pushed the 6703-a2a-continue-task branch from f07080f to 27991cd Compare November 3, 2026 14:32
marekdano added a commit that referenced this pull request Nov 9, 2026
Address review feedback on #7374 (issue #6703).

  Stop a client from overriding fields the gateway owns when it continues
  a task. The spread of the client message could replace messageId and
  role, and a client contextId was forwarded and logged.

  - Set taskId and contextId from the stored task. Reject a client value
    that differs, including a contextId on a task that has none.
  - Always send role as the user role. Accept the user and ROLE_USER
    spellings. Reject any other role.
  - Always generate messageId in the gateway.
  - Add a regression test for each override and for each accepted case.

Signed-off-by: Marek Dano <mk.dano@gmail.com>
@marekdano
marekdano force-pushed the 6703-a2a-continue-task branch from 27991cd to 9586778 Compare November 9, 2026 10:46
  A caller could not answer an agent that paused in an input-required
  or auth-required state. The invoke routes had no task identifier.

  Add POST /v1/a2a/{agent_id}/tasks/{task_id}/messages. The body holds
  either an A2A message object or a text string. The gateway adds taskId
  and the stored contextId, converts the message for 0.3 and 1.0 agents,
  and sends it through invoke_agent. The task row takes its state from
  the agent response. A task_continued event records the previous and
  new state.

  An unknown or invisible task returns 404. A task in a terminal state
  returns 409. A message that names a different taskId returns 400. The
  route requires a2a.invoke, and TokenScopingMiddleware maps the path.

  Fix cancel_task and upsert_task to treat the 1.0 terminal states
  (TASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_CANCELED) as
  terminal. Before, cancelling a finished 1.0 task overwrote its state.

Signed-off-by: Marek Dano <mk.dano@gmail.com>
Address review feedback on #7374 (issue #6703).

  Stop a client from overriding fields the gateway owns when it continues
  a task. The spread of the client message could replace messageId and
  role, and a client contextId was forwarded and logged.

  - Set taskId and contextId from the stored task. Reject a client value
    that differs, including a contextId on a task that has none.
  - Always send role as the user role. Accept the user and ROLE_USER
    spellings. Reject any other role.
  - Always generate messageId in the gateway.
  - Add a regression test for each override and for each accepted case.

Signed-off-by: Marek Dano <mk.dano@gmail.com>
Signed-off-by: Marek Dano <mk.dano@gmail.com>
@marekdano
marekdano force-pushed the 6703-a2a-continue-task branch from 9586778 to af46258 Compare November 11, 2026 09:40

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.

[API]: No way to continue an A2A task that is waiting for input or authentication

2 participants