feat(a2a): add A2A protocol support with AgentBase interface#601
feat(a2a): add A2A protocol support with AgentBase interface#601dbschmigelski wants to merge 24 commits intostrands-agents:mainfrom
Conversation
src/a2a/a2a-agent.ts
Outdated
| * @returns Promise that resolves to the AgentResult | ||
| */ | ||
| async invoke(args: InvokeArgs): Promise<AgentResult> { | ||
| const text = this._extractTextFromA2AResponseFromArgs(args) |
There was a problem hiding this comment.
Issue: The method name _extractTextFromA2AResponseFromArgs is confusing — it suggests extracting from an A2A response, but it actually extracts text from InvokeArgs.
Suggestion: Rename to _extractTextFromArgs to match the TSDoc comment on line 168 ("Extracts a text string from InvokeArgs").
src/a2a/executor.ts
Outdated
| artifact: { | ||
| artifactId, | ||
| // If no deltas were streamed, publish the full result; otherwise empty to close the artifact | ||
| parts: [{ kind: 'text', text: isFirstChunk && next.value ? String(next.value) : '' }], |
There was a problem hiding this comment.
Issue: Using String(next.value) relies on implicit string conversion of AgentResult. While AgentResult.toString() exists, this pattern is fragile.
Suggestion: Use explicit conversion: next.value.toString() for clarity and to avoid potential issues if the class changes.
|
Assessment: Comment This PR introduces well-designed A2A protocol support with good alignment to the Python SDK patterns. The implementation is solid with proper documentation and test coverage. Review Summary
The PR description is thorough with clear design decisions and explicit documentation of what was intentionally deferred (e.g., real streaming in |
src/a2a/a2a-agent.ts
Outdated
| * @param result - The A2A response | ||
| * @returns Extracted text content | ||
| */ | ||
| export function extractTextFromA2AResponse(result: Task | A2AMessage): string { |
There was a problem hiding this comment.
Issue: extractTextFromA2AResponse is exported but appears to only be used internally within this module.
Suggestion: If this is intended as an internal helper, consider making it non-exported. If it's meant for public consumption, document a use case in the TSDoc.
|
Assessment: Approve ✅ All previous review comments have been addressed in the latest commit:
The implementation is solid and ready for merge. Consider adding the |
|
Assessment: Approve ✅ Latest commit ( All previous review feedback remains addressed:
Ready to merge. 🚀 |
|
Assessment: Approve ✅ Latest commit ( All previous fixes remain in place:
Ready to merge. 🚀 |
src/a2a/a2a-agent.ts
Outdated
| * | ||
| * @remarks | ||
| * Currently calls invoke() and yields the result as a single AgentResultEvent. | ||
| * TODO: Yield incremental A2A streaming events (TaskArtifactUpdateEvent, TaskStatusUpdateEvent) |
There was a problem hiding this comment.
Is this going to be a fast follow? Are we getting this for 1.0?
Introduce Agent-to-Agent (A2A) protocol integration using @a2a-js/sdk, enabling Strands agents to communicate over the A2A JSON-RPC protocol. - Add A2A server, client, executor, and converters with full test coverage - Add AgentBase interface mirroring Python SDK's Protocol pattern - Support text streaming and image/video/document content in A2A artifacts - Add A2A integration tests (text, image input via file parts) - Add ToolProvider interface for agent-as-tool composition - Reorganize integration tests into subdirectories (a2a/, mcp/, models/, tools/)
Introduce Agent-to-Agent (A2A) protocol integration using @a2a-js/sdk, enabling Strands agents to communicate over the A2A JSON-RPC protocol. - Add A2A server, client, executor, and converters with full test coverage - Add AgentBase interface mirroring Python SDK's Protocol pattern - Support text streaming and image/video/document content in A2A artifacts - Add A2A integration tests (text, image input via file parts) - Add ToolProvider interface for agent-as-tool composition - Reorganize integration tests into subdirectories (a2a/, mcp/, models/, tools/)
Align with Python SDK naming by renaming A2AClient to A2AAgent. Simplify the public API by making connect/disconnect/sendMessage private and using lazy client initialization via ClientFactory. Centralize experimental warning into shared module and streamline text extraction helpers. Co-authored-by: Josh Samuel <3156090+jsamuel1@users.noreply.github.com>
ToolProvider is not needed for A2A since A2AAgent implements AgentBase directly. Remove the interface, its duck-typing check, and the test file to minimize changes in this PR.
Align with Python SDK where converters are private internal API. Only export A2AServer, A2AAgent, and StrandsA2AExecutor.
- Rename _extractTextFromA2AResponseFromArgs back to _extractTextFromArgs to match what the method actually does (extracts from InvokeArgs) - Use next.value.toString() instead of String(next.value) in executor - Make extractTextFromA2AResponse non-exported since it is internal only - Refactor tests to exercise response extraction through invoke()
A2A tests depend on Node-only modules (express, @a2a-js/sdk, net) and cannot run in the browser test project.
…injectTraceContext
- Rename converters.ts to adapters.ts for consistency with gemini/adapters.ts - Rename experimental.ts to logging.ts to keep module focused - Fix log format in logExperimentalWarning to follow style guide - Add optional InvokeOptions param to AgentBase invoke/stream signatures
- Rewrite A2AAgent.stream() to use sendMessageStream() yielding A2AStreamUpdateEvent for each raw A2A protocol event - Build invoke() on top of stream() (matching Python SDK pattern) - Add A2AStreamUpdateEvent and A2AEventData types in src/a2a/events.ts - Add A2AStreamUpdateEvent to AgentStreamEvent union - Make A2AServer.createMiddleware() synchronous (no dynamic imports) - Rewrite unit tests for streaming pattern with comprehensive coverage
- Add end-to-end integration tests for A2AAgent (invoke, stream, agent card) - Fix _buildResult to accumulate text from streamed artifact-update events instead of only using the last complete event (which may have no text)
- Remove Python SDK references from agent-base.ts and adapters.ts docstrings - Convert partsToContentBlocks and contentBlocksToParts to use switch statements
Aligns with A2AServer and A2AAgent naming convention.
Keep the public API minimal — these can be added later if needed, but removing them would be a breaking change.
…igned port When serve() is called with port 0, the OS assigns a random port. The server was not updating _port or the agent card URL after binding, causing clients to connect to port 0 and fail with EADDRNOTAVAIL.
…ironment The test uses express and node:http which are not available in browsers.
| /** | ||
| * A2A server that exposes a Strands Agent as an A2A-compliant HTTP endpoint. | ||
| * | ||
| * Uses Express to serve the agent card and handle JSON-RPC requests. |
There was a problem hiding this comment.
Out of curiosity, why Express? In the future, we will want to be less opinionated and allow users to configure their own web server? Would it make sense to be more like the model providers and have A2AServer be a base class and A2AExpressServer be an implementation? Not saying we have to do this now since the feature is experimental but curious on your thoughts.
| artifactTexts.set(id, chunks) | ||
| } | ||
| } | ||
| yield new A2AStreamUpdateEvent(event) |
There was a problem hiding this comment.
I know this is experimental and so fine with this for now but it would be nice to coerce the event into a normal Agent stream event.
| }) | ||
| }) | ||
|
|
||
| describe('Agent._createEmptyUsage', () => { |
There was a problem hiding this comment.
Maybe need to rebase here. These look related to Liz's metrics PR.
There was a problem hiding this comment.
Could this be combined with a2a-agent.test.node.ts? It seems like the tests in here are built around A2AAgent as well.
| }) | ||
| }) | ||
|
|
||
| describe.skipIf(!supports.citations)('Citations', () => { |
There was a problem hiding this comment.
Seems like something carried over from your citations work.
Description
AgentBaseinterface as the minimal contract for all agent types, aligning with the Python SDK'sAgentBaseProtocol@a2a-js/sdk:A2AServerexposes a Strands Agent as an A2A HTTP endpoint,A2AAgentwraps a remote A2A agent as anAgentBaseKey components
AgentBase(src/agent/agent-base.ts) — Minimal interface withinvoke(args)andstream(args). BothAgentandA2AAgentimplement it.A2AServer(src/a2a/server.ts) — Wraps a StrandsAgentas an A2A HTTP endpoint using Express. Serves agent card at/.well-known/agent-card.jsonand handlesJSON-RPC at
/. Supports streaming via SSE. Equivalent to Python'sA2AServer.A2AAgent(src/a2a/a2a-agent.ts) — Wraps a remote A2A agent asAgentBase. Lazily connects viaClientFactory, populatesname/descriptionfrom the remoteagent card. Equivalent to Python's
A2AAgent.StrandsA2AExecutor(src/a2a/executor.ts) — Bridges A2A task execution to Strands agent streaming, converting between A2A messages and Strands content blocks.Equivalent to Python's
StrandsA2AExecutor.src/a2a/converters.ts) — InternalpartsToContentBlocks/contentBlocksToPartshandling text, image (base64 + URI), and file parts.Design decisions
AgentBaseis an interface, not an abstract class — maximizes flexibility; any object satisfying the contract works without inheritanceA2AAgentimplementsAgentBase, notToolProvider— aligns with Python whereA2AAgentis a standalone agent, not a tool source.ToolProviderwas removed fromthe PR to keep it focused
A2AAgenthas no publicconnect()/disconnect(). The A2A SDK client is created lazily on firstinvoke()viaClientFactory,matching Python's pattern
partsToContentBlocks/contentBlocksToPartsare not exported from thea2asub-path, matching Python where_converters.pyis privateexpressand@a2a-js/sdkare imported dynamically so they remain optional peer dependenciesWhat we intentionally did NOT do (compared to Python SDK)
A2AAgent— Python'sA2AAgent.stream_asyncyields incrementalTaskArtifactUpdateEvent/TaskStatusUpdateEventas they arrive from theremote agent. Our
stream()callsinvoke()and yields a singleAgentResultEvent. Left a TODO for follow-up.A2AAgentwithin aGraphBuilderorchestration. OurAgentNodecurrently takes concreteAgent. Updating it toaccept
AgentBaseis a follow-up.A2AAgent— Python'sconvert_input_to_messagevalidates input types. Our_extractTextFromArgssilently returns empty string forunsupported inputs, matching the current SDK pattern.
convert_input_to_message,A2AAgentonly sends text content. Non-text content blocks (images, files) arediscarded. This is a known limitation matching Python behavior.
Related Issues
#394
Documentation PR
Type of Change
New feature
Testing
How have you tested the change?
npm run checkChecklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.