feat(a2a): implement full A2A task lifecycle state support#1025
feat(a2a): implement full A2A task lifecycle state support#1025agent-of-mkmeral wants to merge 1 commit intostrands-agents:mainfrom
Conversation
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)
| * Terminal task states in the A2A protocol. | ||
| * These states indicate the task has finished processing. | ||
| */ | ||
| const TERMINAL_STATES = new Set(['completed', 'failed', 'canceled', 'rejected']) |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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'` |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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.
|
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
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 |
There was a problem hiding this comment.
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.
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)completedstopReason: 'endTurn'failedinput-requiredstopReason: 'interrupt'canceledcancelTask()called or agent returnsstopReason: 'cancelled'Key changes:
failedstate with a generic message (no internal details leaked).cancelTask()now works — uses AbortController to signal the agent. The agent stops at the next cancellation checkpoint and the task transitions tocanceled.stopReason: 'interrupt'), the task transitions toinput-requiredwith interrupt names/reasons in the status message.canceled(notfailed).Client-side (
a2a-agent.ts)completedendTurnfailedendTurncanceledcancelledrejectedendTurninput-requiredinterruptauth-requiredinterruptunknownendTurnKey changes:
_isCompleteEvent: Now recognizes ALL terminal and input states (was: onlycompleted). 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).a2aTaskStatemetadata: The task state is included ininvocationState.a2aTaskStatefor 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
All 54 tests pass ✅
Related
cc @mkmeral