refactor(core): treat max session turns as stream end

This commit is contained in:
Adam Weidman
2026-03-20 10:58:58 -04:00
parent cc2058296c
commit 6c9f644417
2 changed files with 39 additions and 14 deletions
@@ -181,7 +181,13 @@ describe('translateEvent', () => {
it('stringifies object resultDisplay correctly', () => {
state.streamStartEmitted = true;
state.pendingToolNames.set('call-3', 'diff_tool');
const objectDisplay = { type: 'FileDiff', before: 'a', after: 'b' };
const objectDisplay = {
fileDiff: '@@ -1 +1 @@\n-a\n+b',
fileName: 'test.txt',
filePath: '/tmp/test.txt',
originalContent: 'a',
newContent: 'b',
};
const event: ServerGeminiStreamEvent = {
type: GeminiEventType.ToolCallResponse,
value: {
@@ -402,19 +408,17 @@ describe('translateEvent', () => {
});
describe('MaxSessionTurns events', () => {
it('emits a non-fatal max-turns error event', () => {
it('emits stream_end with max_turns', () => {
state.streamStartEmitted = true;
const event: ServerGeminiStreamEvent = {
type: GeminiEventType.MaxSessionTurns,
};
const result = translateEvent(event, state);
expect(result).toHaveLength(1);
const err = result[0] as AgentEvent<'error'>;
expect(err.type).toBe('error');
expect(err.fatal).toBe(false);
expect(err.status).toBe('RESOURCE_EXHAUSTED');
expect(err._meta?.['code']).toBe('MAX_TURNS_EXCEEDED');
expect(err.message).toBe('Maximum session turns exceeded');
const streamEnd = result[0] as AgentEvent<'stream_end'>;
expect(streamEnd.type).toBe('stream_end');
expect(streamEnd.reason).toBe('max_turns');
expect(streamEnd.data).toEqual({ code: 'MAX_TURNS_EXCEEDED' });
});
});
@@ -628,6 +632,24 @@ describe('mapFinishReason', () => {
it('maps PROHIBITED_CONTENT to refusal', () => {
expect(mapFinishReason(FinishReason.PROHIBITED_CONTENT)).toBe('refusal');
});
it('maps IMAGE_SAFETY to refusal', () => {
expect(mapFinishReason(FinishReason.IMAGE_SAFETY)).toBe('refusal');
});
it('maps IMAGE_PROHIBITED_CONTENT to refusal', () => {
expect(mapFinishReason(FinishReason.IMAGE_PROHIBITED_CONTENT)).toBe(
'refusal',
);
});
it('maps UNEXPECTED_TOOL_CALL to failed', () => {
expect(mapFinishReason(FinishReason.UNEXPECTED_TOOL_CALL)).toBe('failed');
});
it('maps NO_IMAGE to failed', () => {
expect(mapFinishReason(FinishReason.NO_IMAGE)).toBe('failed');
});
});
describe('mapHttpToGrpcStatus', () => {
+9 -6
View File
@@ -157,11 +157,11 @@ export function translateEvent(
case GeminiEventType.MaxSessionTurns:
ensureStreamStart(state, out);
out.push(
makeEvent('error', state, {
status: 'RESOURCE_EXHAUSTED',
message: 'Maximum session turns exceeded',
fatal: false,
_meta: { code: 'MAX_TURNS_EXCEEDED' },
makeEvent('stream_end', state, {
reason: 'max_turns',
data: {
code: 'MAX_TURNS_EXCEEDED',
},
}),
);
break;
@@ -338,12 +338,15 @@ export function mapFinishReason(
case 'BLOCKLIST':
case 'PROHIBITED_CONTENT':
case 'SPII':
case 'IMAGE_SAFETY':
case 'IMAGE_PROHIBITED_CONTENT':
return 'refusal';
case 'MALFORMED_FUNCTION_CALL':
case 'OTHER':
case 'UNEXPECTED_TOOL_CALL':
case 'NO_IMAGE':
return 'failed';
default:
((_x: never) => {})(reason);
return 'failed';
}
}