fix(cli): revert legacy agent session stream crash on terminal events to match main snapshot

This commit is contained in:
Adam Weidman
2026-03-17 17:21:25 -04:00
parent 5ba92577c5
commit 7ba25a1344
5 changed files with 39 additions and 33 deletions
+3 -6
View File
@@ -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"
@@ -3,7 +3,8 @@
exports[`runNonInteractive > should emit appropriate error event in streaming JSON mode: 'loop detected' 1`] = `
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Loop test"}
{"type":"result","timestamp":"<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":<DURATION>,"tool_calls":0,"models":{}}}
{"type":"error","timestamp":"<TIMESTAMP>","severity":"warning","message":"Loop detected, stopping execution"}
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"models":{}}}
"
`;
@@ -12,7 +13,6 @@ exports[`runNonInteractive > should emit appropriate error event in streaming JS
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Max turns test"}
{"type":"error","timestamp":"<TIMESTAMP>","severity":"error","message":"Maximum session turns exceeded"}
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"models":{}}}
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"models":{}}}
"
`;
+24 -2
View File
@@ -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);
@@ -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');
@@ -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;
}