diff --git a/workspace/a2a_cli.py b/workspace/a2a_cli.py index 5ba7381c..60a19777 100644 --- a/workspace/a2a_cli.py +++ b/workspace/a2a_cli.py @@ -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}], } diff --git a/workspace/a2a_client.py b/workspace/a2a_client.py index d740bd6a..ec9a09be 100644 --- a/workspace/a2a_client.py +++ b/workspace/a2a_client.py @@ -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}], } diff --git a/workspace/a2a_executor.py b/workspace/a2a_executor.py index 30b936f7..bbda258c 100644 --- a/workspace/a2a_executor.py +++ b/workspace/a2a_executor.py @@ -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) diff --git a/workspace/adapters/google-adk/adapter.py b/workspace/adapters/google-adk/adapter.py index 8b3fe9db..e0a3c667 100644 --- a/workspace/adapters/google-adk/adapter.py +++ b/workspace/adapters/google-adk/adapter.py @@ -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: diff --git a/workspace/builtin_tools/a2a_tools.py b/workspace/builtin_tools/a2a_tools.py index df4f9d78..07dd26dc 100644 --- a/workspace/builtin_tools/a2a_tools.py +++ b/workspace/builtin_tools/a2a_tools.py @@ -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}], }, diff --git a/workspace/builtin_tools/delegation.py b/workspace/builtin_tools/delegation.py index 25d0ae55..85943e52 100644 --- a/workspace/builtin_tools/delegation.py +++ b/workspace/builtin_tools/delegation.py @@ -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}", }, diff --git a/workspace/heartbeat.py b/workspace/heartbeat.py index 2af915a1..a7ab8de5 100644 --- a/workspace/heartbeat.py +++ b/workspace/heartbeat.py @@ -375,7 +375,7 @@ class HeartbeatLoop: "method": "message/send", "params": { "message": { - "role": "user", + "role": "ROLE_USER", "parts": [{"type": "text", "text": trigger_msg}], }, }, diff --git a/workspace/main.py b/workspace/main.py index c90dd4ce..2e6bf37b 100644 --- a/workspace/main.py +++ b/workspace/main.py @@ -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}], },