From 1a84de8a6114d611d567b27eec67d0b9ae6af6b6 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Wed, 29 Apr 2026 00:46:47 -0700 Subject: [PATCH] fix(a2a-v1): rewrite FilePart emit using v1 protobuf Part struct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit a2a-sdk v1.0.2 replaced the v0 Pydantic discriminated-union types (Part(root=TextPart(...))/Part(root=FilePart(file=FileWithUri(...)))) with a single protobuf Part struct that has optional `text`, `url`, `raw`, `data`, `filename`, `media_type` fields. The classes FilePart, TextPart, FileWithUri don't exist in v1 — import fails: File "claude_sdk_executor.py", line 592 from a2a.types import FilePart, FileWithUri, Message, Part, Role, TextPart ImportError: cannot import name 'FilePart' from 'a2a.types' Production impact: every claude-code workspace (Design Director, UX Researcher, all coordinators in molecule-core teams) crashes on result delivery whenever the response includes a /workspace/* file reference. The A2A delegation loop is broken at the result-delivery step. Workspaces can receive tasks but can't ship results back. Fix: - Drop FilePart/TextPart/FileWithUri imports (don't exist in v1). - `Part(root=TextPart(text=t))` → `Part(text=t)`. - `Part(root=FilePart(file=FileWithUri(uri=u, name=n, mimeType=m)))` → `Part(url=u, filename=n, media_type=m)`. - `messageId=...` → `message_id=...` (snake_case in protobuf). - `Role.agent` → `Role.ROLE_AGENT` (v1 enum). Verified by constructing the exact shape against v1.0.2 in the running claude-code template image: Message: message_id: 03ff9367 role: ROLE_AGENT parts count: 2 text part: hello file part: workspace:foo.txt foo.txt text/plain Refs: molecule-core memory `reference_a2a_sdk_v0_to_v1_migration` documents the Pydantic→protobuf shift; this is the fifth migration finding today (after the new_agent_text_message rename in crewai/openclaw/autogen/gemini-cli). Test plan: - [x] `python3 -m py_compile claude_sdk_executor.py` clean. - [x] Runtime construction smoke verified against the live v1.0.2 a2a-sdk in the claude-code template image. - [ ] End-to-end: provision a claude-code workspace, send a task whose response references a /workspace/* file, confirm result lands without ImportError. Co-Authored-By: Claude Opus 4.7 (1M context) --- claude_sdk_executor.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/claude_sdk_executor.py b/claude_sdk_executor.py index 29f0fe1..e54c24d 100644 --- a/claude_sdk_executor.py +++ b/claude_sdk_executor.py @@ -586,21 +586,27 @@ class ClaudeSDKExecutor(AgentExecutor): # preparing its prompt while this turn's response ships. Event # ordering is preserved per-queue by the A2A server, so no races. # If the response mentions /workspace/... files, stage each and - # emit FileParts alongside the text so the canvas can download. + # emit file parts alongside the text so the canvas can download. + # + # a2a-sdk v1 uses protobuf, NOT the v0 Pydantic discriminated-union + # types. There is no FilePart / TextPart / FileWithUri class — Part + # is one struct with optional `text`, `url`, `raw`, `data`, + # `filename`, `media_type` fields (plus `metadata`). Set the field + # that matches the part's nature; leave the rest unset. outbound = collect_outbound_files(response_text) if outbound: - from a2a.types import FilePart, FileWithUri, Message, Part, Role, TextPart + from a2a.types import Message, Part, Role import uuid as _uuid - parts: list = [Part(root=TextPart(text=response_text))] if response_text else [] + parts: list = [Part(text=response_text)] if response_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"], + )) await event_queue.enqueue_event(Message( - messageId=_uuid.uuid4().hex, - role=Role.agent, + message_id=_uuid.uuid4().hex, + role=Role.ROLE_AGENT, parts=parts, )) else: