diff --git a/packages/core/src/agents/a2aUtils.test.ts b/packages/core/src/agents/a2aUtils.test.ts index f0ea746025..2bcdad2c40 100644 --- a/packages/core/src/agents/a2aUtils.test.ts +++ b/packages/core/src/agents/a2aUtils.test.ts @@ -10,6 +10,7 @@ import { extractIdsFromResponse, isTerminalState, A2AResultReassembler, + AUTH_REQUIRED_MSG, } from './a2aUtils.js'; import type { SendMessageResult } from './a2a-client-manager.js'; import type { @@ -285,6 +286,66 @@ describe('a2aUtils', () => { ); }); + it('should handle auth-required state with a message', () => { + const reassembler = new A2AResultReassembler(); + + reassembler.update({ + kind: 'status-update', + status: { + state: 'auth-required', + message: { + kind: 'message', + role: 'agent', + parts: [{ kind: 'text', text: 'I need your permission.' }], + } as Message, + }, + } as unknown as SendMessageResult); + + expect(reassembler.toString()).toContain('I need your permission.'); + expect(reassembler.toString()).toContain(AUTH_REQUIRED_MSG); + }); + + it('should handle auth-required state without relying on metadata', () => { + const reassembler = new A2AResultReassembler(); + + reassembler.update({ + kind: 'status-update', + status: { + state: 'auth-required', + }, + } as unknown as SendMessageResult); + + expect(reassembler.toString()).toContain(AUTH_REQUIRED_MSG); + }); + + it('should not duplicate the auth instruction OR agent message if multiple identical auth-required chunks arrive', () => { + const reassembler = new A2AResultReassembler(); + + const chunk = { + kind: 'status-update', + status: { + state: 'auth-required', + message: { + kind: 'message', + role: 'agent', + parts: [{ kind: 'text', text: 'You need to login here.' }], + } as Message, + }, + } as unknown as SendMessageResult; + + reassembler.update(chunk); + // Simulate multiple updates with the same overall state + reassembler.update(chunk); + reassembler.update(chunk); + + const output = reassembler.toString(); + // The substring should only appear exactly once + expect(output.split(AUTH_REQUIRED_MSG).length - 1).toBe(1); + + // Crucially, the agent's actual custom message should ALSO only appear exactly once + expect(output.split('You need to login here.').length - 1).toBe(1); + }); + it('should fallback to history in a task chunk if no message or artifacts exist and task is terminal', () => { const reassembler = new A2AResultReassembler(); diff --git a/packages/core/src/agents/a2aUtils.ts b/packages/core/src/agents/a2aUtils.ts index 52817f4971..dc39f4e660 100644 --- a/packages/core/src/agents/a2aUtils.ts +++ b/packages/core/src/agents/a2aUtils.ts @@ -16,6 +16,8 @@ import type { } from '@a2a-js/sdk'; import type { SendMessageResult } from './a2a-client-manager.js'; +export const AUTH_REQUIRED_MSG = `[Authorization Required] The agent has indicated it requires authorization to proceed. Please follow the agent's instructions.`; + /** * Reassembles incremental A2A streaming updates into a coherent result. * Shows sequential status/messages followed by all reassembled artifacts. @@ -33,6 +35,7 @@ export class A2AResultReassembler { switch (chunk.kind) { case 'status-update': + this.appendStateInstructions(chunk.status?.state); this.pushMessage(chunk.status?.message); break; @@ -65,6 +68,7 @@ export class A2AResultReassembler { break; case 'task': + this.appendStateInstructions(chunk.status?.state); this.pushMessage(chunk.status?.message); if (chunk.artifacts) { for (const art of chunk.artifacts) { @@ -106,6 +110,17 @@ export class A2AResultReassembler { } } + private appendStateInstructions(state: TaskState | undefined) { + if (state !== 'auth-required') { + return; + } + + // Prevent duplicate instructions if multiple chunks report auth-required + if (!this.messageLog.includes(AUTH_REQUIRED_MSG)) { + this.messageLog.push(AUTH_REQUIRED_MSG); + } + } + private pushMessage(message: Message | undefined) { if (!message) return; const text = extractPartsText(message.parts, '\n');