Skip to content

feat(a2a): implement full A2A task lifecycle state support#1025

Open
agent-of-mkmeral wants to merge 1 commit intostrands-agents:mainfrom
agent-of-mkmeral:feat/a2a-task-lifecycle
Open

feat(a2a): implement full A2A task lifecycle state support#1025
agent-of-mkmeral wants to merge 1 commit intostrands-agents:mainfrom
agent-of-mkmeral:feat/a2a-task-lifecycle

Conversation

@agent-of-mkmeral
Copy link
Copy Markdown
Contributor

Summary

Implements the complete A2A task lifecycle on both server and client sides, matching the Python SDK implementation (strands-agents/sdk-python#2245).

Server-side (executor.ts)

State Trigger Behavior
completed Agent finishes with stopReason: 'endTurn' Same as before
failed Agent throws an error Graceful transition (was: re-throw). Generic message in A2A response, details only in server logs
input-required Agent returns stopReason: 'interrupt' New — publishes interrupt details in status message
canceled cancelTask() called or agent returns stopReason: 'cancelled' New — cooperative cancellation via AbortController

Key changes:

  • Error handling: Errors no longer bubble up as unhandled exceptions. The executor catches them and transitions the task to failed state with a generic message (no internal details leaked).
  • Cancellation: cancelTask() now works — uses AbortController to signal the agent. The agent stops at the next cancellation checkpoint and the task transitions to canceled.
  • Interrupts: When the agent needs human input (stopReason: 'interrupt'), the task transitions to input-required with interrupt names/reasons in the status message.
  • CancelledError: Specifically caught and mapped to canceled (not failed).

Client-side (a2a-agent.ts)

A2A State StopReason Rationale
completed endTurn Normal completion
failed endTurn Terminal (error info in message)
canceled cancelled Maps to SDK's cancelled concept
rejected endTurn Terminal
input-required interrupt Agent needs human input
auth-required interrupt Agent needs authentication
unknown endTurn Safe default

Key changes:

  • _isCompleteEvent: Now recognizes ALL terminal and input states (was: only completed). This prevents infinite polling when a remote agent fails or requires input.
  • STATE_TO_STOP_REASON: Single source of truth mapping (derived sets prevent drift).
  • a2aTaskState metadata: The task state is included in invocationState.a2aTaskState for downstream consumers to inspect.

Python part of TS SDK

The strands-py/ directory has no A2A code — A2A is handled entirely in the TypeScript layer. No changes needed there.

Tests

  • Executor: 15 tests (was 7) — +8 for failed, canceled, input-required, multi-interrupt, error leak prevention, CancelledError, and cancelTask
  • A2A Agent: 39 tests (was 28) — +11 for parametrized state mapping across all 7 states, a2aTaskState metadata, terminal state recognition, and input-required

All 54 tests pass ✅

Related

cc @mkmeral

Implements the complete A2A task lifecycle on both server and client sides,
matching the Python SDK implementation (strands-agents/sdk-python#2245).

## Server-side (executor.ts)

- **failed**: Agent errors now gracefully transition task to 'failed' state
  instead of re-throwing. Error details stay in server logs (no leaking).
- **input-required**: Agent returning stopReason='interrupt' transitions
  task to 'input-required' with interrupt details in the status message.
- **canceled**: Cooperative cancellation via AbortController. cancelTask()
  signals the agent to stop; task transitions to 'canceled'.
- **CancelledError handling**: CancelledError from the agent is caught and
  transitions to 'canceled' (not 'failed').

## Client-side (a2a-agent.ts)

- **STATE_TO_STOP_REASON mapping**: Single source of truth for mapping A2A
  task states to Strands StopReason values:
  - completed/failed/rejected/unknown → 'endTurn'
  - canceled → 'cancelled'
  - input-required/auth-required → 'interrupt'
- **_isCompleteEvent**: Now recognizes all terminal and input states as
  complete events (prevents infinite polling on failed/canceled tasks).
- **a2aTaskState metadata**: Task state included in invocationState for
  downstream consumers.

## Tests

- 15 executor tests (was 7): +8 tests for failed, canceled, input-required,
  multi-interrupt, error leak prevention, CancelledError, and cancelTask
- 39 a2a-agent tests (was 28): +11 tests for parametrized state mapping,
  a2aTaskState metadata, terminal state recognition, and input-required

Closes: strands-agents/sdk-python#2245 (TS counterpart)
@agent-of-mkmeral agent-of-mkmeral deployed to manual-approval May 7, 2026 20:16 — with GitHub Actions Active
@github-actions github-actions Bot added the strands-running <strands-managed> Whether or not an agent is currently running label May 7, 2026
* Terminal task states in the A2A protocol.
* These states indicate the task has finished processing.
*/
const TERMINAL_STATES = new Set(['completed', 'failed', 'canceled', 'rejected'])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Issue: TERMINAL_STATES (line 24) and INPUT_STATES (line 30) are defined but never referenced in this file. The executor uses stopReason-based branching directly (result.stopReason === 'interrupt', result.stopReason === 'cancelled').

Suggestion: Remove these unused constants, or use them in the execution logic (e.g., for a future getTaskState() helper).

* Terminal states that indicate the task is finished.
* Derived from STATE_TO_STOP_REASON for single source of truth.
*/
const TERMINAL_STATES = new Set(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Issue: Both a2a-agent.ts and executor.ts independently define TERMINAL_STATES and INPUT_STATES with the same values. This creates a drift risk — if a new state is added, both files must be updated in sync.

Suggestion: Extract the shared state definitions into a common module (e.g., a2a/states.ts) that both files import from. The STATE_TO_STOP_REASON map and derived sets in a2a-agent.ts could serve as the single source of truth for both files.

* The executor maps agent execution outcomes to A2A task states:
* - **completed** — Agent finished successfully with `stopReason: 'endTurn'`
* - **failed** — Agent threw an error during execution
* - **input_required** — Agent returned with `stopReason: 'interrupt'`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Issue: Doc says input_required (underscore) but the actual A2A state is input-required (hyphen). The code on line 203 correctly uses 'input-required'.

Suggestion: Change to input-required for consistency with the A2A protocol and the implementation.

*/
async cancelTask(_taskId: string, _eventBus: ExecutionEventBus): Promise<void> {
throw A2AError.unsupportedOperation('Task cancellation is not supported')
async cancelTask(taskId: string, eventBus: ExecutionEventBus): Promise<void> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Issue: The eventBus parameter is accepted but never used in this method. The actual canceled status is published by execute() when it catches the CancelledError. If this parameter is required by the AgentExecutor interface, consider adding a comment (e.g., // eventBus unused — execute() publishes the status) to clarify intent for future readers. If not required by the interface, consider removing it.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

Assessment: Comment

Solid implementation of the A2A task lifecycle. The code is well-structured, tests are comprehensive with good edge case coverage, and the approach aligns well with the Python SDK implementation.

Review Categories
  • Dead code: TERMINAL_STATES and INPUT_STATES in executor.ts are defined but never referenced in that file.
  • DRY: State classification constants are duplicated across a2a-agent.ts and executor.ts — consolidating into a shared module would prevent drift.
  • Documentation: Minor typo (input_required vs input-required) in two doc comments.

The test coverage is thorough with clear parametrized tests and good separation of concerns between the server and client sides.

* Implements the AgentExecutor interface from `@a2a-js/sdk/server` to allow
* a Strands Agent to handle A2A JSON-RPC requests.
* a Strands Agent to handle A2A JSON-RPC requests. Supports the full A2A
* task lifecycle including `completed`, `failed`, `input_required`, and
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Issue: Same input_required (underscore) vs input-required (hyphen) inconsistency appears here in the module-level doc.

Suggestion: input-required to match the A2A protocol.

@github-actions github-actions Bot removed the strands-running <strands-managed> Whether or not an agent is currently running label May 7, 2026
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