Merge pull request #2184 from Molecule-AI/fix/jsonrpc-routes-rpc-url

fix: pass rpc_url='/' to create_jsonrpc_routes (a2a-sdk 1.x)
This commit is contained in:
Hongming Wang 2026-04-27 16:45:48 +00:00 committed by GitHub
commit 9532890f04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 47 additions and 25 deletions

View File

@ -71,7 +71,7 @@ async def delegate(target_id: str, task: str, async_mode: bool = False):
"method": "message/send",
"params": {
"message": {
"role": "user",
"role": "ROLE_USER",
"messageId": str(uuid.uuid4()),
"parts": [{"kind": "text", "text": task}],
}
@ -108,7 +108,7 @@ async def delegate(target_id: str, task: str, async_mode: bool = False):
"method": "message/send",
"params": {
"message": {
"role": "user",
"role": "ROLE_USER",
"messageId": str(uuid.uuid4()),
"parts": [{"kind": "text", "text": task}],
}

View File

@ -71,7 +71,7 @@ async def send_a2a_message(target_url: str, message: str) -> str:
"method": "message/send",
"params": {
"message": {
"role": "user",
"role": "ROLE_USER",
"messageId": str(uuid.uuid4()),
"parts": [{"kind": "text", "text": message}],
}

View File

@ -446,20 +446,32 @@ class LangGraphA2AExecutor(AgentExecutor):
# making the earlier `Part(text=text)` call (line ~358, inside
# the astream_events loop) raise UnboundLocalError because
# the local binding is not yet in scope at that point.
from a2a.types import FilePart, FileWithUri, Message, Role, TextPart
_parts: list[Part] = [Part(root=TextPart(text=final_text))] if final_text else []
#
# a2a-sdk 1.x flattened the Part shape: 0.x used
# `Part(root=TextPart(text=...))` / `Part(root=FilePart(file=
# FileWithUri(uri=..., name=..., mimeType=...)))` (Pydantic
# discriminated-union style). 1.x's Part is a single proto
# message with flat fields: text, url, filename, media_type,
# raw, data, metadata. TextPart/FilePart/FileWithUri were
# removed. Same for Message: messageId/taskId/contextId
# camelCase became message_id/task_id/context_id.
from a2a.types import Message, Role
_parts: list[Part] = [Part(text=final_text)] if final_text else []
for f in _outbound:
_parts.append(Part(root=FilePart(file=FileWithUri(
uri="workspace:" + f["path"],
name=f["name"],
mimeType=f["mime_type"],
))))
_parts.append(Part(
url="workspace:" + f["path"],
filename=f["name"],
media_type=f["mime_type"],
))
msg = Message(
messageId=uuid.uuid4().hex,
role=Role.agent,
message_id=uuid.uuid4().hex,
# 1.x Role is a protobuf enum: ROLE_UNSPECIFIED,
# ROLE_USER, ROLE_AGENT. Old `Role.agent` (Pydantic
# lowercase enum) doesn't exist anymore.
role=Role.ROLE_AGENT,
parts=_parts,
taskId=task_id,
contextId=context_id,
task_id=task_id,
context_id=context_id,
)
else:
msg = new_text_message(final_text, task_id=task_id, context_id=context_id)

View File

@ -36,7 +36,7 @@ from typing import TYPE_CHECKING, Any
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.helpers import new_agent_text_message
from a2a.helpers import new_text_message
from adapter_base import AdapterConfig, BaseAdapter
@ -191,7 +191,7 @@ class GoogleADKA2AExecutor(AgentExecutor):
if not user_text:
parts = getattr(getattr(context, "message", None), "parts", None)
logger.warning("GoogleADKA2AExecutor: no text in message parts: %s", parts)
await event_queue.enqueue_event(new_agent_text_message(_NO_TEXT_MSG))
await event_queue.enqueue_event(new_text_message(_NO_TEXT_MSG))
return
session_id = getattr(context, "context_id", None) or "default-session"
@ -223,7 +223,7 @@ class GoogleADKA2AExecutor(AgentExecutor):
response_parts.append(text)
final_text = "".join(response_parts).strip() or _NO_RESPONSE_MSG
await event_queue.enqueue_event(new_agent_text_message(final_text))
await event_queue.enqueue_event(new_text_message(final_text))
except Exception as exc:
logger.error(
@ -234,7 +234,7 @@ class GoogleADKA2AExecutor(AgentExecutor):
)
# Mirror sanitize_agent_error() convention: expose class name only.
await event_queue.enqueue_event(
new_agent_text_message(f"Agent error: {type(exc).__name__}")
new_text_message(f"Agent error: {type(exc).__name__}")
)
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:

View File

@ -57,7 +57,7 @@ async def delegate_task(workspace_id: str, task: str) -> str:
"method": "message/send",
"params": {
"message": {
"role": "user",
"role": "ROLE_USER",
"messageId": str(uuid.uuid4()),
"parts": [{"kind": "text", "text": task}],
},

View File

@ -269,7 +269,7 @@ async def _execute_delegation(task_id: str, workspace_id: str, task: str):
"id": f"delegation-{task_id}-{attempt}",
"params": {
"message": {
"role": "user",
"role": "ROLE_USER",
"parts": [{"kind": "text", "text": task}],
"messageId": f"msg-{task_id}-{attempt}",
},

View File

@ -375,7 +375,7 @@ class HeartbeatLoop:
"method": "message/send",
"params": {
"message": {
"role": "user",
"role": "ROLE_USER",
"parts": [{"type": "text", "text": trigger_msg}],
},
},

View File

@ -242,10 +242,20 @@ async def main(): # pragma: no cover
agent_card=agent_card,
)
# v1: replace A2AStarletteApplication with Starlette route factory
# v1: replace A2AStarletteApplication with Starlette route factory.
# rpc_url is required in a2a-sdk 1.x (was implicit at root in 0.x).
# Use '/' to match a2a.utils.constants.DEFAULT_RPC_URL — that's also
# what the platform's a2a_proxy.go POSTs to (it forwards to the
# workspace's URL without appending a path). Card endpoint stays at
# the well-known path /.well-known/agent-card.json (handled by
# create_agent_card_routes default).
routes = []
routes.extend(create_agent_card_routes(agent_card))
routes.extend(create_jsonrpc_routes(request_handler=handler))
# enable_v0_3_compat=True so any external 0.3.x A2A client (still using
# `"role": "user"` lowercase + camelCase Pydantic field names) can talk
# to us without re-deploying. Internally our outbound payloads are now
# 1.x-shaped (ROLE_USER), but inbound is opt-in compatible.
routes.extend(create_jsonrpc_routes(request_handler=handler, rpc_url="/", enable_v0_3_compat=True))
app = Starlette(routes=routes)
# 8. Register with platform
@ -451,7 +461,7 @@ async def main(): # pragma: no cover
"method": "message/send",
"params": {
"message": {
"role": "user",
"role": "ROLE_USER",
"messageId": f"initial-{_uuid.uuid4().hex[:8]}",
"parts": [{"kind": "text", "text": config.initial_prompt}],
},
@ -550,7 +560,7 @@ async def main(): # pragma: no cover
"method": "message/send",
"params": {
"message": {
"role": "user",
"role": "ROLE_USER",
"messageId": f"idle-{_uuid.uuid4().hex[:8]}",
"parts": [{"kind": "text", "text": config.idle_prompt}],
},