Skip to content

DataPart is never produced on outbound A2A paths (agents can only return text) #44

Description

@avsrma

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:

  1. 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.
  2. 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

  1. Build an agent (B) whose final result is a structured object (e.g. via a LangGraph responseFormat / structuredResponse).
  2. Have agent A call agent B over A2A and read the completed task message.
  3. 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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions