From 10c5bd8ce9de6bb18e14dbdfd05334b2ebb5341e Mon Sep 17 00:00:00 2001 From: Adam Weidman <65992621+adamfweidman@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:38:30 -0500 Subject: [PATCH] feat(core): improve A2A content extraction (#20487) Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com> --- packages/core/src/agents/a2aUtils.test.ts | 63 +++++++++++++++++++++++ packages/core/src/agents/a2aUtils.ts | 23 ++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/packages/core/src/agents/a2aUtils.test.ts b/packages/core/src/agents/a2aUtils.test.ts index 711650ea80..f0ea746025 100644 --- a/packages/core/src/agents/a2aUtils.test.ts +++ b/packages/core/src/agents/a2aUtils.test.ts @@ -284,5 +284,68 @@ describe('a2aUtils', () => { 'Analyzing...\n\nProcessing...\n\nArtifact (Code):\nprint("Done")', ); }); + + it('should fallback to history in a task chunk if no message or artifacts exist and task is terminal', () => { + const reassembler = new A2AResultReassembler(); + + reassembler.update({ + kind: 'task', + status: { state: 'completed' }, + history: [ + { + kind: 'message', + role: 'agent', + parts: [{ kind: 'text', text: 'Answer from history' }], + } as Message, + ], + } as unknown as SendMessageResult); + + expect(reassembler.toString()).toBe('Answer from history'); + }); + + it('should NOT fallback to history in a task chunk if task is not terminal', () => { + const reassembler = new A2AResultReassembler(); + + reassembler.update({ + kind: 'task', + status: { state: 'working' }, + history: [ + { + kind: 'message', + role: 'agent', + parts: [{ kind: 'text', text: 'Answer from history' }], + } as Message, + ], + } as unknown as SendMessageResult); + + expect(reassembler.toString()).toBe(''); + }); + + it('should not fallback to history if artifacts exist', () => { + const reassembler = new A2AResultReassembler(); + + reassembler.update({ + kind: 'task', + status: { state: 'completed' }, + artifacts: [ + { + artifactId: 'art-1', + name: 'Data', + parts: [{ kind: 'text', text: 'Artifact Content' }], + }, + ], + history: [ + { + kind: 'message', + role: 'agent', + parts: [{ kind: 'text', text: 'Answer from history' }], + } as Message, + ], + } as unknown as SendMessageResult); + + const output = reassembler.toString(); + expect(output).toContain('Artifact (Data):'); + expect(output).not.toContain('Answer from history'); + }); }); }); diff --git a/packages/core/src/agents/a2aUtils.ts b/packages/core/src/agents/a2aUtils.ts index e753d047d0..52817f4971 100644 --- a/packages/core/src/agents/a2aUtils.ts +++ b/packages/core/src/agents/a2aUtils.ts @@ -74,6 +74,26 @@ export class A2AResultReassembler { ]); } } + // History Fallback: Some agent implementations do not populate the + // status.message in their final terminal response, instead archiving + // the final answer in the task's history array. To ensure we don't + // present an empty result, we fallback to the most recent agent message + // in the history only when the task is terminal and no other content + // (message log or artifacts) has been reassembled. + if ( + isTerminalState(chunk.status?.state) && + this.messageLog.length === 0 && + this.artifacts.size === 0 && + chunk.history && + chunk.history.length > 0 + ) { + const lastAgentMsg = [...chunk.history] + .reverse() + .find((m) => m.role?.toLowerCase().includes('agent')); + if (lastAgentMsg) { + this.pushMessage(lastAgentMsg); + } + } break; case 'message': { @@ -126,7 +146,7 @@ export class A2AResultReassembler { * Handles Text, Data (JSON), and File parts. */ export function extractMessageText(message: Message | undefined): string { - if (!message) { + if (!message || !message.parts || !Array.isArray(message.parts)) { return ''; } @@ -158,7 +178,6 @@ function extractPartText(part: Part): string { } if (isDataPart(part)) { - // Attempt to format known data types if metadata exists, otherwise JSON stringify return `Data: ${JSON.stringify(part.data)}`; }