fix(a2a-v1): rewrite FilePart emit using v1 protobuf Part struct

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) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-04-29 00:46:47 -07:00
parent de2ab5ab33
commit 1a84de8a61

View File

@ -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: