feat(agent-server): A2A protocol server-mode support (Agent Card + JSON-RPC/SSE) - #4590
feat(agent-server): A2A protocol server-mode support (Agent Card + JSON-RPC/SSE)#4590lukegalea wants to merge 2 commits into
Conversation
…C 2.0 endpoint) Expose the agent-server as an A2A agent (Linux Foundation a2a-spec, rev ~0.3, JSON-RPC transport), addressing OpenHands#1060. - /.well-known/agent-card.json: AgentCard v0.3 discovery document (no auth) - POST /api/a2a: JSON-RPC 2.0 methods message/send, message/stream (SSE), tasks/get, tasks/cancel; taskId maps to conversationId - Auth accepts Authorization: Bearer or X-Session-API-Key - Minimal local pydantic models; no a2a-sdk dependency, no new deps - Tests, example script, additive api.py wiring only
|
✅ Review complete. This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here. |
all-hands-bot
left a comment
There was a problem hiding this comment.
🟡 Taste Rating: Acceptable - Good direction, but the streaming path has a real race with EventService subscription semantics.
[CRITICAL ISSUES]
- [openhands-agent-server/openhands/agent_server/a2a_router.py, Line 683] Streaming correctness:
message/streamsubscribes before sending the user message and treats the initial current-state snapshot as terminal. See inline comment. - [openhands-agent-server/openhands/agent_server/a2a_router.py, Line 586] JSON-RPC correlation: helper-generated errors drop the request id. See inline comment.
[TESTING GAPS]
- The
message/streamtest mockssubscribe_to_eventsbut does not preserve the real initial-state push fromEventService.subscribe_to_events. Add coverage that uses the real contract or a fake that enqueues the initialIDLEsnapshot before run updates. - CI currently reports
Validate PR descriptionfailing because the PR body does not keep the requiredHUMAN:,AGENT:,## Why, and## How to Testtemplate sections.
[RISK ASSESSMENT]
- [Overall PR]
⚠️ Risk Assessment: 🟡 MEDIUM
New unauthenticated discovery endpoint plus authenticated JSON-RPC/SSE protocol surface in agent-server. The architecture is isolated and dependency-free, but the stream endpoint can terminate before doing useful work and several error paths break JSON-RPC response correlation.
VERDICT:
❌ Needs rework: Fix the stream terminal-state race and preserve JSON-RPC ids on helper error paths before this should be approved.
KEY INSIGHT:
The API shape is reasonable, but the implementation must respect existing EventService subscription semantics instead of mocking them away in tests.
Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:
- Add a
.agents/skills/custom-codereview-guide.mdfile to your branch (or edit it if one already exists) with the/codereviewtrigger and the context the reviewer is missing (e.g., "Security concerns about X do not apply here because Y"). See the customization docs for the required frontmatter format.- Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
- When your PR is merged, the guideline file goes through normal code review by repository maintainers.
Resolve with AI? Install the iterate skill in your agent and run
/iterateto automatically drive this PR through CI, review, and QA until it is merge-ready.Was this review helpful? React with 👍 or 👎 to give feedback.
This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation
| async def event_stream() -> AsyncIterator[str]: | ||
| queue: asyncio.Queue = asyncio.Queue() | ||
| subscriber = _QueueSubscriber(queue) | ||
| subscriber_id = await event_service.subscribe_to_events(subscriber) |
There was a problem hiding this comment.
🟠 Important: EventService.subscribe_to_events() immediately invokes the subscriber with the current execution status. For a newly created A2A task that initial event is usually IDLE; because this code subscribes before _send_user_message() and later treats idle as terminal, message/stream can emit a final completed task and close before the run actually starts or produces artifacts. Either ignore the pre-send state snapshot, or send the message before subscribing, and add a test that preserves this real EventService behavior.
| try: | ||
| task_id = _parse_task_id(send_params.message.taskId) | ||
| except ValueError as exc: | ||
| return _jsonrpc_error(JSONRPC_INVALID_PARAMS, str(exc)) |
There was a problem hiding this comment.
🟠 Important: Errors from _start_or_get_conversation() are returned directly to message/send and message/stream, but this helper does not receive the caller rpc_id, so invalid taskId, missing task, no profile, and unavailable service responses come back with id: null instead of echoing the request id. JSON-RPC clients rely on id correlation. Thread rpc_id through this helper, or have callers wrap the error, and assert ids on these error paths.
enyst
left a comment
There was a problem hiding this comment.
Hey @lukegalea thank you for the PR! ❤️
This repo has a convention for a temporary .pr/ directory that can be used for live-tests, example runs results, and/or a markdown summary for human readability. Do you think your agents could try to run live and record some trace we can see? The .pr/ directory will not stay committed, we have an automation removing it upon approval or merge.
In this case, I would love it if you could add a tiny flow e.g. from send message to possible outcomes? 🙏
|
📁 PR Artifacts Notice This PR contains a |
|
@lukegalea, could you please confirm that the following issue #1060 acceptance criteria are incorporated?
The current implementation and tests do not yet demonstrate these points. |
HUMAN:
Adding A2A protocol server support so agent-server becomes a discoverable node in A2A meshes — implements #1060. The HUMAN note: I've wanted OpenHands agents callable from my own orchestration stack via A2A for months; this PR makes the agent-server speak the protocol end to end.
AGENT:
Why
agent-server could not be discovered or driven by external A2A (Agent2Agent, Linux Foundation a2a-spec) clients. This PR exposes it as a first-class A2A agent: Agent Card discovery plus the standard JSON-RPC task lifecycle, mapping A2A tasks 1:1 onto existing conversations. Closes #1060 (maintainer-invited community contribution).
Summary
GET /.well-known/agent-card.json— Agent Card v0.3 generated from server config + registered agent profilesPOST /api/a2a— JSON-RPC 2.0:message/send,message/stream(SSE viasubscribe_to_events),tasks/get,tasks/cancel;get_agent_final_responsesurfaced as text artifactX-Session-API-KeyorAuthorization: Bearer(sameconfig.session_api_keys)a2a-sdkas an optional extra if maintainers preferIssue Number
#1060
How to Test
End-to-end against a real server (what I ran):
Unit tests:
uv run pytest tests/agent_server -q→ 2053 passed, 13 deselected (stress suite excluded by default), including 18 new A2A tests (tests/agent_server/test_a2a_router.py): card shape, both auth headers + rejection, JSON-RPC errors (-32601/-32700/-32602), message/send happy path, task reuse, tasks/get, tasks/cancel, SSE content-type + event sequence. OpenAPI quality gate passes (97 allowlisted weak locations, unchanged from main). A runnable httpx example is atexamples/02_remote_agent_server/17_a2a_agent_card.py.Video/Screenshots
Not applicable (protocol-level backend feature, no UI); the How-to-Test commands + example script reproduce the full flow.
Design Doc
Added
.pr/design.htmlwith the object model, endpoint mapping table, and SSE event sequence, linked here:https://htmlpreview.github.io/?https://github.com/lukegalea/software-agent-sdk/blob/feat/a2a-server/.pr/design.html
Type
Notes
api.py; no existing router/model touched, no REST contract impact.a2a-sdk(possibly as an optional extra)? Implementer's preference is zero-dep for the core; will follow maintainer guidance.