mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-11 20:07:00 -07:00
refactor(core): end sessions after finished turns
This commit is contained in:
@@ -44,21 +44,21 @@ describe('translateEvent', () => {
|
||||
});
|
||||
|
||||
describe('Content events', () => {
|
||||
it('emits stream_start + message for first content event', () => {
|
||||
it('emits agent_start + message for first content event', () => {
|
||||
const event: ServerGeminiStreamEvent = {
|
||||
type: GeminiEventType.Content,
|
||||
value: 'Hello world',
|
||||
};
|
||||
const result = translateEvent(event, state);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]?.type).toBe('stream_start');
|
||||
expect(result[0]?.type).toBe('agent_start');
|
||||
expect(result[1]?.type).toBe('message');
|
||||
const msg = result[1] as AgentEvent<'message'>;
|
||||
expect(msg.role).toBe('agent');
|
||||
expect(msg.content).toEqual([{ type: 'text', text: 'Hello world' }]);
|
||||
});
|
||||
|
||||
it('skips stream_start for subsequent content events', () => {
|
||||
it('skips agent_start for subsequent content events', () => {
|
||||
state.streamStartEmitted = true;
|
||||
const event: ServerGeminiStreamEvent = {
|
||||
type: GeminiEventType.Content,
|
||||
@@ -301,14 +301,14 @@ describe('translateEvent', () => {
|
||||
});
|
||||
|
||||
describe('ModelInfo events', () => {
|
||||
it('emits stream_start and session_update when no stream started yet', () => {
|
||||
it('emits agent_start and session_update when no stream started yet', () => {
|
||||
const event: ServerGeminiStreamEvent = {
|
||||
type: GeminiEventType.ModelInfo,
|
||||
value: 'gemini-2.5-pro',
|
||||
};
|
||||
const result = translateEvent(event, state);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]?.type).toBe('stream_start');
|
||||
expect(result[0]?.type).toBe('agent_start');
|
||||
expect(result[1]?.type).toBe('session_update');
|
||||
const sessionUpdate = result[1] as AgentEvent<'session_update'>;
|
||||
expect(sessionUpdate.model).toBe('gemini-2.5-pro');
|
||||
@@ -329,7 +329,7 @@ describe('translateEvent', () => {
|
||||
});
|
||||
|
||||
describe('AgentExecutionStopped events', () => {
|
||||
it('emits stream_end with the final stop message in data.message', () => {
|
||||
it('emits agent_end with the final stop message in data.message', () => {
|
||||
state.streamStartEmitted = true;
|
||||
const event: ServerGeminiStreamEvent = {
|
||||
type: GeminiEventType.AgentExecutionStopped,
|
||||
@@ -341,8 +341,8 @@ describe('translateEvent', () => {
|
||||
};
|
||||
const result = translateEvent(event, state);
|
||||
expect(result).toHaveLength(1);
|
||||
const streamEnd = result[0] as AgentEvent<'stream_end'>;
|
||||
expect(streamEnd.type).toBe('stream_end');
|
||||
const streamEnd = result[0] as AgentEvent<'agent_end'>;
|
||||
expect(streamEnd.type).toBe('agent_end');
|
||||
expect(streamEnd.reason).toBe('completed');
|
||||
expect(streamEnd.data).toEqual({ message: 'Stopped by hook' });
|
||||
});
|
||||
@@ -355,7 +355,7 @@ describe('translateEvent', () => {
|
||||
};
|
||||
const result = translateEvent(event, state);
|
||||
expect(result).toHaveLength(1);
|
||||
const streamEnd = result[0] as AgentEvent<'stream_end'>;
|
||||
const streamEnd = result[0] as AgentEvent<'agent_end'>;
|
||||
expect(streamEnd.data).toEqual({ message: 'hook' });
|
||||
});
|
||||
});
|
||||
@@ -408,22 +408,22 @@ describe('translateEvent', () => {
|
||||
});
|
||||
|
||||
describe('MaxSessionTurns events', () => {
|
||||
it('emits stream_end with max_turns', () => {
|
||||
it('emits agent_end with max_turns', () => {
|
||||
state.streamStartEmitted = true;
|
||||
const event: ServerGeminiStreamEvent = {
|
||||
type: GeminiEventType.MaxSessionTurns,
|
||||
};
|
||||
const result = translateEvent(event, state);
|
||||
expect(result).toHaveLength(1);
|
||||
const streamEnd = result[0] as AgentEvent<'stream_end'>;
|
||||
expect(streamEnd.type).toBe('stream_end');
|
||||
const streamEnd = result[0] as AgentEvent<'agent_end'>;
|
||||
expect(streamEnd.type).toBe('agent_end');
|
||||
expect(streamEnd.reason).toBe('max_turns');
|
||||
expect(streamEnd.data).toEqual({ code: 'MAX_TURNS_EXCEEDED' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Finished events', () => {
|
||||
it('emits usage + stream_end for STOP', () => {
|
||||
it('emits usage for STOP', () => {
|
||||
state.streamStartEmitted = true;
|
||||
state.model = 'gemini-2.5-pro';
|
||||
const event: ServerGeminiStreamEvent = {
|
||||
@@ -438,27 +438,23 @@ describe('translateEvent', () => {
|
||||
},
|
||||
};
|
||||
const result = translateEvent(event, state);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toHaveLength(1);
|
||||
|
||||
const usage = result[0] as AgentEvent<'usage'>;
|
||||
expect(usage.model).toBe('gemini-2.5-pro');
|
||||
expect(usage.inputTokens).toBe(100);
|
||||
expect(usage.outputTokens).toBe(50);
|
||||
expect(usage.cachedTokens).toBe(10);
|
||||
|
||||
const end = result[1] as AgentEvent<'stream_end'>;
|
||||
expect(end.reason).toBe('completed');
|
||||
});
|
||||
|
||||
it('emits stream_end without usage when no metadata', () => {
|
||||
it('emits nothing when no usage metadata is present', () => {
|
||||
state.streamStartEmitted = true;
|
||||
const event: ServerGeminiStreamEvent = {
|
||||
type: GeminiEventType.Finished,
|
||||
value: { reason: undefined, usageMetadata: undefined },
|
||||
};
|
||||
const result = translateEvent(event, state);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]?.type).toBe('stream_end');
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -480,15 +476,15 @@ describe('translateEvent', () => {
|
||||
});
|
||||
|
||||
describe('UserCancelled events', () => {
|
||||
it('emits stream_end with reason aborted', () => {
|
||||
it('emits agent_end with reason aborted', () => {
|
||||
state.streamStartEmitted = true;
|
||||
const event: ServerGeminiStreamEvent = {
|
||||
type: GeminiEventType.UserCancelled,
|
||||
};
|
||||
const result = translateEvent(event, state);
|
||||
expect(result).toHaveLength(1);
|
||||
const end = result[0] as AgentEvent<'stream_end'>;
|
||||
expect(end.type).toBe('stream_end');
|
||||
const end = result[0] as AgentEvent<'agent_end'>;
|
||||
expect(end.type).toBe('agent_end');
|
||||
expect(end.reason).toBe('aborted');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,7 +77,7 @@ function makeEvent(
|
||||
|
||||
function ensureStreamStart(state: TranslationState, out: AgentEvent[]): void {
|
||||
if (!state.streamStartEmitted) {
|
||||
out.push(makeEvent('stream_start', state, {}));
|
||||
out.push(makeEvent('agent_start', state, {}));
|
||||
state.streamStartEmitted = true;
|
||||
}
|
||||
}
|
||||
@@ -148,7 +148,7 @@ export function translateEvent(
|
||||
case GeminiEventType.UserCancelled:
|
||||
ensureStreamStart(state, out);
|
||||
out.push(
|
||||
makeEvent('stream_end', state, {
|
||||
makeEvent('agent_end', state, {
|
||||
reason: 'aborted',
|
||||
}),
|
||||
);
|
||||
@@ -157,7 +157,7 @@ export function translateEvent(
|
||||
case GeminiEventType.MaxSessionTurns:
|
||||
ensureStreamStart(state, out);
|
||||
out.push(
|
||||
makeEvent('stream_end', state, {
|
||||
makeEvent('agent_end', state, {
|
||||
reason: 'max_turns',
|
||||
data: {
|
||||
code: 'MAX_TURNS_EXCEEDED',
|
||||
@@ -173,7 +173,7 @@ export function translateEvent(
|
||||
kind: 'loop_detected',
|
||||
}),
|
||||
);
|
||||
// No stream_end — the stream continues. Consumer decides how to handle:
|
||||
// No agent_end — the stream continues. Consumer decides how to handle:
|
||||
// non-interactive emits a warning, interactive shows a confirmation dialog.
|
||||
break;
|
||||
|
||||
@@ -191,7 +191,7 @@ export function translateEvent(
|
||||
case GeminiEventType.AgentExecutionStopped:
|
||||
ensureStreamStart(state, out);
|
||||
out.push(
|
||||
makeEvent('stream_end', state, {
|
||||
makeEvent('agent_end', state, {
|
||||
reason: 'completed',
|
||||
data: {
|
||||
message: event.value.systemMessage?.trim() || event.value.reason,
|
||||
@@ -285,18 +285,11 @@ function handleFinished(
|
||||
state: TranslationState,
|
||||
out: AgentEvent[],
|
||||
): void {
|
||||
ensureStreamStart(state, out);
|
||||
|
||||
if (value.usageMetadata) {
|
||||
ensureStreamStart(state, out);
|
||||
const usage = mapUsage(value.usageMetadata, state.model);
|
||||
out.push(makeEvent('usage', state, usage));
|
||||
}
|
||||
|
||||
out.push(
|
||||
makeEvent('stream_end', state, {
|
||||
reason: mapFinishReason(value.reason),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -319,7 +312,7 @@ function handleError(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Maps a Gemini FinishReason to a StreamEndReason.
|
||||
* Maps a Gemini FinishReason to an AgentEnd reason.
|
||||
*/
|
||||
export function mapFinishReason(
|
||||
reason: FinishReason | undefined,
|
||||
|
||||
Reference in New Issue
Block a user