Summary
The A2A protocol defines three Part types — TextPart, FilePart, and DataPart. A conformant A2A agent must be able to speak all three. This plugin handles DataParts inbound but never produces one outbound, so a completing agent can only return a TextPart. Structured agent-to-agent data exchange over A2A is therefore impossible: a callee agent (B) finishes a task and has no way to hand a caller agent (A) a structured object.
Current behavior
Inbound is fine — incoming DataParts are parsed correctly by partsToText, firstDataPart, and partsToMessageContent in lib/utils/message-handling.js.
Outbound is not. There are two distinct gaps:
-
Agent final answer → completed message. The completion emit uses the output mapper, which is string-only:
defaultOutputMapper (srv/handlers/graph-executor.js) returns a string and JSON-stringifies structured results into a TextPart.
- The completed status message and the authoritative
response artifact both hardcode a single TextPart.
- The
agentMessage(text, data) primitive already appends a {kind:"data", data} DataPart when given data, but the only call site that ever supplies data is the HITL interrupt path — every terminal path (completed / canceled / failed) is text-only.
-
Tool-result content → artifact-update. The post-stream scanner in graph-executor.js walks each ToolMessage.content for embedded {"kind":"file"} JSON and republishes those as artifact-update events, but it ignores {"kind":"data"}. A DataPart produced inside a tool result never surfaces as an artifact.
Expected behavior
- An agent returning structured data yields a DataPart; an agent returning text yields a TextPart (exactly as today); both may appear in the same message when the agent returns text and data.
- A caller reading the completed message with the plugin's own inbound utility (
firstDataPart) gets the structured object back — a clean B→A round-trip.
- Structured tool output (a
{"kind":"data"} object in a tool result) surfaces as an artifact-update event, mirroring how FileParts already do.
Reproduction
- Build an agent (B) whose final result is a structured object (e.g. via a LangGraph
responseFormat / structuredResponse).
- Have agent A call agent B over A2A and read the completed task message.
- Observe that the object arrives only as a JSON-stringified TextPart (or a malformed TextPart when
result.output is an object) — firstDataPart returns undefined.
Proposed fix
- Let the output mapper optionally return
{ text, data } (a plain string still means text-only, preserving backward compatibility); normalize at the completion call site and emit the DataPart on both the completed status message and the response artifact.
- Extend
defaultOutputMapper to recognize structured results (result.structuredResponse, plain-object result.output) instead of stringifying them.
- Generalize the tool-result scanner to also collect
{"kind":"data"} objects and publish them as data-* artifact-update events; add an emit_data_part tool as a first-class producer (companion to emit_file_part).
Wire format
Emit the v0.3.x shape {kind:"data", data}, consistent with the rest of the emit path (no v1.0 content.$case on the emit side). No @a2a-js/sdk version change required; this round-trips with inbound firstDataPart, which already reads both wire shapes.
Environment
@cap-js/agents@0.9.1
@a2a-js/sdk@^0.3.12
Summary
The A2A protocol defines three Part types — TextPart, FilePart, and DataPart. A conformant A2A agent must be able to speak all three. This plugin handles DataParts inbound but never produces one outbound, so a completing agent can only return a TextPart. Structured agent-to-agent data exchange over A2A is therefore impossible: a callee agent (B) finishes a task and has no way to hand a caller agent (A) a structured object.
Current behavior
Inbound is fine — incoming DataParts are parsed correctly by
partsToText,firstDataPart, andpartsToMessageContentinlib/utils/message-handling.js.Outbound is not. There are two distinct gaps:
Agent final answer → completed message. The completion emit uses the output mapper, which is string-only:
defaultOutputMapper(srv/handlers/graph-executor.js) returns a string and JSON-stringifies structured results into a TextPart.responseartifact both hardcode a single TextPart.agentMessage(text, data)primitive already appends a{kind:"data", data}DataPart when givendata, but the only call site that ever suppliesdatais the HITL interrupt path — every terminal path (completed / canceled / failed) is text-only.Tool-result content →
artifact-update. The post-stream scanner ingraph-executor.jswalks eachToolMessage.contentfor embedded{"kind":"file"}JSON and republishes those asartifact-updateevents, but it ignores{"kind":"data"}. A DataPart produced inside a tool result never surfaces as an artifact.Expected behavior
firstDataPart) gets the structured object back — a clean B→A round-trip.{"kind":"data"}object in a tool result) surfaces as anartifact-updateevent, mirroring how FileParts already do.Reproduction
responseFormat/structuredResponse).result.outputis an object) —firstDataPartreturnsundefined.Proposed fix
{ text, data }(a plain string still means text-only, preserving backward compatibility); normalize at the completion call site and emit the DataPart on both the completed status message and theresponseartifact.defaultOutputMapperto recognize structured results (result.structuredResponse, plain-objectresult.output) instead of stringifying them.{"kind":"data"}objects and publish them asdata-*artifact-updateevents; add anemit_data_parttool as a first-class producer (companion toemit_file_part).Wire format
Emit the v0.3.x shape
{kind:"data", data}, consistent with the rest of the emit path (no v1.0content.$caseon the emit side). No@a2a-js/sdkversion change required; this round-trips with inboundfirstDataPart, which already reads both wire shapes.Environment
@cap-js/agents@0.9.1@a2a-js/sdk@^0.3.12