diff --git a/package-lock.json b/package-lock.json index d31c0544f9..3757403f78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -486,8 +486,7 @@ "version": "2.11.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", - "license": "(Apache-2.0 AND BSD-3-Clause)", - "peer": true + "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@bundled-es-modules/cookie": { "version": "2.0.1", @@ -1490,7 +1489,6 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" @@ -7413,8 +7411,7 @@ "version": "0.0.1581282", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dezalgo": { "version": "1.0.4", @@ -16250,6 +16247,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, "license": "0BSD", "peer": true }, @@ -17867,7 +17865,6 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" diff --git a/packages/cli/src/__snapshots__/nonInteractiveCli.test.ts.snap b/packages/cli/src/__snapshots__/nonInteractiveCli.test.ts.snap index be8ff60d59..92f396a59c 100644 --- a/packages/cli/src/__snapshots__/nonInteractiveCli.test.ts.snap +++ b/packages/cli/src/__snapshots__/nonInteractiveCli.test.ts.snap @@ -3,7 +3,8 @@ exports[`runNonInteractive > should emit appropriate error event in streaming JSON mode: 'loop detected' 1`] = ` "{"type":"init","timestamp":"","session_id":"test-session-id","model":"test-model"} {"type":"message","timestamp":"","role":"user","content":"Loop test"} -{"type":"result","timestamp":"","status":"error","error":{"type":"Error","message":"[API Error: Loop detected]"},"stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":,"tool_calls":0,"models":{}}} +{"type":"error","timestamp":"","severity":"warning","message":"Loop detected, stopping execution"} +{"type":"result","timestamp":"","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":,"tool_calls":0,"models":{}}} " `; @@ -12,7 +13,6 @@ exports[`runNonInteractive > should emit appropriate error event in streaming JS {"type":"message","timestamp":"","role":"user","content":"Max turns test"} {"type":"error","timestamp":"","severity":"error","message":"Maximum session turns exceeded"} {"type":"result","timestamp":"","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":,"tool_calls":0,"models":{}}} -{"type":"result","timestamp":"","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":,"tool_calls":0,"models":{}}} " `; diff --git a/packages/core/src/agent/event-translator.ts b/packages/core/src/agent/event-translator.ts index 50ae6f7c71..e6fd7f1c42 100644 --- a/packages/core/src/agent/event-translator.ts +++ b/packages/core/src/agent/event-translator.ts @@ -81,7 +81,7 @@ function ensureStreamStart(state: TranslationState, out: AgentEvent[]): void { * Converts @google/genai Part[] to ContentPart[]. * Text parts become text ContentParts; inline data becomes media ContentParts. */ -function mapResponseParts(parts: Part[]): ContentPart[] { +export function mapResponseParts(parts: Part[]): ContentPart[] { const result: ContentPart[] = []; for (const part of parts) { if (part.text !== undefined) { @@ -285,7 +285,29 @@ export function translateEvent( name: state.pendingToolNames.get(event.value.callId) ?? 'unknown', content: mapResponseParts(event.value.responseParts), isError: event.value.error !== undefined, - ...(event.value.data ? { data: event.value.data } : {}), + ...(event.value.resultDisplay !== undefined + ? { + displayContent: [ + { + type: 'text', + text: + typeof event.value.resultDisplay === 'string' + ? event.value.resultDisplay + : JSON.stringify(event.value.resultDisplay), + }, + ], + } + : {}), + ...(event.value.data || event.value.errorType + ? { + data: { + ...(event.value.data || {}), + ...(event.value.errorType + ? { errorType: event.value.errorType } + : {}), + }, + } + : {}), }), ); state.pendingToolNames.delete(event.value.callId); diff --git a/packages/core/src/agent/legacy-agent-session.test.ts b/packages/core/src/agent/legacy-agent-session.test.ts index 00fac23b16..c7ddfb3886 100644 --- a/packages/core/src/agent/legacy-agent-session.test.ts +++ b/packages/core/src/agent/legacy-agent-session.test.ts @@ -1067,7 +1067,7 @@ describe('LegacyAgentSession', () => { expect(resp.isError).toBe(true); expect(resp.name).toBe('write_file'); expect(resp.content).toEqual([ - { type: 'text', text: 'Permission denied: /bad' }, + { type: 'text', text: 'Permission denied' }, ]); expect(resp.displayContent).toEqual([ { type: 'text', text: 'Cannot write to /bad' }, @@ -1140,7 +1140,7 @@ describe('LegacyAgentSession', () => { // LoopDetected emits error{fatal:true} + stream_end{failed} // --------------------------------------------------------------------- - it('LoopDetected emits fatal error and stream_end(failed)', async () => { + it('LoopDetected emits non-fatal error and stream_end(failed)', async () => { const client = makeAsyncClient([ [ { type: GeminiEventType.ModelInfo, value: 'gemini-2.5-pro' }, @@ -1163,8 +1163,8 @@ describe('LegacyAgentSession', () => { const error = events.find( (e) => e.type === 'error', ) as AgentEvent<'error'>; - expect(error.fatal).toBe(true); - expect(error.message).toBe('Loop detected'); + expect(error.fatal).toBe(false); + expect(error.message).toBe('Loop detected, stopping execution'); const end = events[events.length - 1] as AgentEvent<'stream_end'>; expect(end.reason).toBe('failed'); diff --git a/packages/core/src/agent/legacy-agent-session.ts b/packages/core/src/agent/legacy-agent-session.ts index 2fb1e6378f..20cbaf9e02 100644 --- a/packages/core/src/agent/legacy-agent-session.ts +++ b/packages/core/src/agent/legacy-agent-session.ts @@ -21,6 +21,7 @@ import { debugLogger } from '../utils/debugLogger.js'; import { translateEvent, createTranslationState, + mapResponseParts, type TranslationState, } from './event-translator.js'; import type { @@ -218,7 +219,8 @@ export class LegacyAgentSession implements AgentSession { if ( event.type === GeminiEventType.AgentExecutionStopped || event.type === GeminiEventType.LoopDetected || - event.type === GeminiEventType.AgentExecutionBlocked + event.type === GeminiEventType.AgentExecutionBlocked || + event.type === GeminiEventType.MaxSessionTurns ) { this._streamDone = true; return; @@ -248,7 +250,9 @@ export class LegacyAgentSession implements AgentSession { this.makeInternalEvent('tool_response', { requestId: request.callId, name: request.name, - content: mapCompletedToolResponseParts(response.responseParts), + content: response.error + ? [{ type: 'text', text: response.error.message }] + : mapResponseParts(response.responseParts), isError: response.error !== undefined, ...(response.resultDisplay !== undefined ? { @@ -441,20 +445,3 @@ function contentPartsToGeminiParts(parts: ContentPart[]): Part[] { } }); } - -/** Convert @google/genai Part[] → AgentEvent ContentPart[] */ -function mapCompletedToolResponseParts(parts: Part[]): ContentPart[] { - const result: ContentPart[] = []; - for (const part of parts) { - if (part.text !== undefined) { - result.push({ type: 'text', text: part.text }); - } else if (part.inlineData) { - result.push({ - type: 'media', - data: part.inlineData.data, - mimeType: part.inlineData.mimeType, - }); - } - } - return result; -}