Skip to content

fix(react-a2a): forward user attachments and file parts to the wire#5036

Open
ShobhitPatra wants to merge 1 commit into
mainfrom
fix/react-a2a-outbound-attachments
Open

fix(react-a2a): forward user attachments and file parts to the wire#5036
ShobhitPatra wants to merge 1 commit into
mainfrom
fix/react-a2a-outbound-attachments

Conversation

@ShobhitPatra

@ShobhitPatra ShobhitPatra commented Jul 18, 2026

Copy link
Copy Markdown
Member

This PR fixes a silent data-loss bug in the A2A adapter where anything a user attached to a message just vanished before the request went out.

What

  • threadMessageToA2AMessage now converts message.content plus each attachment's content parts (with attachment.contentType as fallback MIME), delegating to the exported contentPartsToA2AParts instead of keeping a second hand-rolled copy.
  • contentPartsToA2AParts gains file part handling (http URL, data URL, raw base64) and an optional trailing fallbackMimeType parameter.
  • Image data URLs go out as {raw, mediaType} with the embedded MIME; plain URLs as {url} plus fallback MIME when known, no mediaType otherwise. The old mediaType: "image/*" is a match pattern, not a valid MIME type.
  • filename propagated when present.

Why

UseA2ARuntimeOptions.adapters.attachments advertises attachment support and core carries attachments onto the user ThreadMessage, but the converter read only content and handled only text and image, so attached files never reached the server. Both siblings already forward attachment content (adk's attachments?.flatMap, ag-ui's per-attachment conversion with contentType fallback); this mirrors them in a2a's flat part format.

The fallback matters concretely: CloudFileAttachmentAdapter emits image parts as plain http URLs with the real MIME only on attachment.contentType, and file parts that can carry mimeType: "". MIME merging is truthy (||, matching ag-ui) so mediaType: "" is never emitted.

Impact

  • Attachments from any adapter (simple, cloud, composite) now reach the wire.
  • Outbound images carry a valid MIME or none. The old image/* only round-tripped as an image by accident of the inbound startsWith("image/") check; an unknown-MIME URL image now renders inbound as a link, the spec-correct behavior. Extension sniffing was considered and rejected: no repo precedent, and ag-ui also omits MIME for undeclared URL sources.
  • No public surface change: no new exports; input widening plus an optional trailing param, both backward compatible. Regenerated api-surface snapshot included, so that CI check is green without an autofix commit.

Notes

  • Defensive-converter rule throughout: missing image/data skips the part, non-canonical data URLs fall through to url/raw, and message.attachments ?? [] guards history-loaded messages built via casts that lack the field at runtime despite the type saying required (covered by a test). Pure string handling, safe in Node, react-ink, and React Native.
  • Delegating to contentPartsToA2AParts removes a duplicated mapping that had already drifted; behavior verified equivalent.
  • Pre-existing, out of scope: audio attachment parts are still dropped; follow-up.
  • Tests cover both converter directions plus the runtime wire path; new runtime tests appended at end of file to stay conflict-free with fix(react-a2a): preserve history branches on load #4968.
  • Patch changeset on @assistant-ui/react-a2a; full turbo build and repo lint pass locally.

Review in cubic

@changeset-bot

changeset-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4046fa3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@assistant-ui/react-a2a Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@rupic-app rupic-app Bot added stage/draft Author still working; not ready for review pkg/react-a2a Changes a package under packages/ R3 Draft; not ready for review type/bugfix Bug fix labels Jul 18, 2026
@ShobhitPatra
ShobhitPatra marked this pull request as ready for review July 18, 2026 08:59
@ShobhitPatra
ShobhitPatra requested a review from a team July 18, 2026 08:59
@rupic-app rupic-app Bot added stage/needs-review No reviews yet; waiting for a maintainer R1 Actionable, normal priority and removed stage/draft Author still working; not ready for review R3 Draft; not ready for review labels Jul 18, 2026
@claude

claude Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @ShobhitPatra's task in 3m 51s —— View job


Review — PR #5036 (commit 4046fa3)

Verdict: solid fix, no blocking issues. The silent attachment data-loss is real (the old converter read only message.content and handled only text/image), the fix mirrors the adk/ag-ui sibling precedent correctly, and CI is fully green (build/tests, Code Quality, Changeset Semver, autofix.ci all pass; changeset ✓, api-surface snapshot regenerated ✓, no docs pages reference the changed export ✓).

The head commit is unchanged in substance since the earlier bot reviews (re-push of the same content), so this review focuses on adjudicating the open inline findings from greptile/cubic rather than re-describing the diff.

Adjudication of open bot findings

1. Non-base64 data URL falls into raw in the file case (greptile P1, cubic V3) — valid, but recommend fixing in the follow-up, not here.
conversions.ts:149-153: a file part with data: "data:text/plain,hello" fails parseDataUrl (requires ;base64,), fails httpUrlPattern, and lands in raw — which types.ts:23 documents as base64 bytes, so a receiver decodes garbage. Two mitigating facts the bot comments miss:

  • It's unreachable via first-party adapters — core's attachment path builds data URLs with FileReader.readAsDataURL, which always emits ;base64,.
  • It exactly mirrors the existing behavior of buildInputSource in packages/react-ag-ui/src/runtime/adapter/conversions.ts:158-174 (value: parsed?.data ?? value) and react-langchain's copy — all four copies of this helper share the flaw.

Since the previous review already flagged hoisting parseDataUrl/httpUrlPattern into @assistant-ui/core/internal as a follow-up (now the 4th byte-identical copy), the right move is to fix the fallthrough once in the shared helper rather than patch one copy and let the adapters diverge. If you prefer to fix it in this PR, a one-line guard before the raw fallback works: if (part.data.startsWith("data:")) return null; (or emit { url: part.data }, matching the image case). Fix this →

2. Zero-byte data URL fails to parse, .+ vs .* (cubic V1) — valid but marginal; same shared-copy situation.
data:<mime>;base64, (an empty file through readAsDataURL) fails the .+ payload match and takes the same corrupting fallthrough as #1. The .* suggestion is safe and trivial — but again the regex is byte-identical across four packages (this PR copied it faithfully from ag-ui), so fixing it only here creates drift. Bundle with #1 in the hoist follow-up, or fix both here together.

3. type: "data" parts dropped (greptile P1, cubic V2) — pre-existing gap, correctly out of scope; add to the follow-up.
The old code dropped everything except text/image too, so this PR doesn't regress anything — it's the same category as the audio gap the PR description already declares as follow-up. That said, the case is unusually cheap for A2A specifically: A2APart has a first-class data?: unknown payload (types.ts:25) and the inbound converter already handles it (conversions.ts:37-39), so outbound case "data": return { data: part.data } would restore round-trip symmetry. Worth naming data alongside audio in the follow-up note.

Suggested improvements

  • The five near-identical object literals in the image/file cases (already flagged inline at conversions.ts:154) — a small buildPart(source, mediaType?, filename?) helper collapses them. One caution on the simplify agent's variant of this: react-langchain's merged form (raw: parsed?.data ?? part.data) would bake in the finding-Merge EditBar into Composer and revamp the Context API #1 fallthrough, so if you restructure, keep the data-URL-parse-failure path distinct.

Good practices

  • Root-cause fix with sibling-adapter precedent cited and verified (adk, ag-ui), not a symptom patch.
  • Removed a drifted hand-rolled duplicate of the converter in A2AThreadRuntimeCore.threadMessageToA2AMessage by delegating to the exported function.
  • Both converter directions plus the runtime wire path tested; defensive ?? [] guards each covered by a dedicated runtime test.
  • Append-only surface: structural input widening + optional trailing param; api-surface snapshot regenerated in-PR.
  • Correctly dropped the invalid mediaType: "image/*" (a match pattern, not a MIME type) with the behavior change documented.

Simplify

The background simplify pass found no efficiency issues and two things worth noting beyond the inline comments already on the PR:

Findings (both minor, non-blocking)

The agent also explicitly cleared the things that look like smells but aren't: the ?? [] guards are endorsed by the defensive-converter rule (and attachment.content genuinely is optional in packages/core/src/types/attachment.ts:33), the structural parameter type (vs. core's ThreadUserMessagePart union) is required by the append-only surface rule, and the | undefined unions are required by exactOptionalPropertyTypes.


@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "fix(react-a2a): forward user attachments..." | Re-trigger Greptile

Comment thread packages/react-a2a/src/conversions.ts
Comment thread packages/react-a2a/src/conversions.ts
@rupic-app rupic-app Bot added stage/awaiting-reviewer Has reviews but not approved; reviewer follow-up needed and removed stage/needs-review No reviews yet; waiting for a maintainer labels Jul 18, 2026
Comment thread packages/react-a2a/src/conversions.ts
Comment thread packages/react-a2a/src/conversions.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/react-a2a/src/conversions.ts
Comment thread packages/react-a2a/src/conversions.ts
Comment thread packages/react-a2a/src/conversions.ts
@ShobhitPatra

Copy link
Copy Markdown
Member Author

noting the follow-up here so it's tracked with the PR: after this and #5039 merge, one small PR to finish outbound converter part coverage :

  • audio parts (base64 + audio/ contentType → {raw, mediaType})
  • data parts (structured payload → a2a data field) — the greptile/cubic finding
  • route unparseable data: strings to url in the file branch, matching the image branch (also covers the zero-byte ;base64, case)
  • tests for the fallthrough branches
  • swap the local parseDataUrl/httpUrlPattern for the shared helpers from refactor: share parseDataUrl and httpUrlPattern from core internal #5039

kept out of this PR to stay on the reported bug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg/react-a2a Changes a package under packages/ R1 Actionable, normal priority stage/awaiting-reviewer Has reviews but not approved; reviewer follow-up needed type/bugfix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant