mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-17 15:23:08 -07:00
fix(core): preserve first-turn display content
This commit is contained in:
committed by
Adam Weidman
parent
1088e1febf
commit
bb459defe9
@@ -265,6 +265,9 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Test input' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-1',
|
||||
undefined,
|
||||
false,
|
||||
'Test input',
|
||||
);
|
||||
expect(getWrittenOutput()).toBe('Hello World\n');
|
||||
// Note: Telemetry shutdown is now handled in runExitCleanup() in cleanup.ts
|
||||
@@ -429,6 +432,9 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Tool response' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-2',
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
expect(getWrittenOutput()).toBe('Final answer\n');
|
||||
});
|
||||
@@ -586,6 +592,9 @@ describe('runNonInteractive', () => {
|
||||
],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-3',
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
expect(getWrittenOutput()).toBe('Sorry, let me try again.\n');
|
||||
});
|
||||
@@ -725,6 +734,9 @@ describe('runNonInteractive', () => {
|
||||
processedParts,
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-7',
|
||||
undefined,
|
||||
false,
|
||||
rawInput,
|
||||
);
|
||||
|
||||
// 6. Assert the final output is correct
|
||||
@@ -758,6 +770,9 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Test input' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-1',
|
||||
undefined,
|
||||
false,
|
||||
'Test input',
|
||||
);
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify(
|
||||
@@ -961,6 +976,9 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Empty response test' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-empty',
|
||||
undefined,
|
||||
false,
|
||||
'Empty response test',
|
||||
);
|
||||
|
||||
// This should output JSON with empty response but include stats
|
||||
@@ -1095,6 +1113,9 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Prompt from command' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-slash',
|
||||
undefined,
|
||||
false,
|
||||
'/testcommand',
|
||||
);
|
||||
|
||||
expect(getWrittenOutput()).toBe('Response from command\n');
|
||||
@@ -1138,6 +1159,9 @@ describe('runNonInteractive', () => {
|
||||
[{ text: 'Slash command output' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-slash',
|
||||
undefined,
|
||||
false,
|
||||
'/help',
|
||||
);
|
||||
expect(getWrittenOutput()).toBe('Response to slash command\n');
|
||||
handleSlashCommandSpy.mockRestore();
|
||||
@@ -1268,17 +1292,16 @@ describe('runNonInteractive', () => {
|
||||
(process.stdin as any).setRawMode = vi.fn();
|
||||
}
|
||||
|
||||
const stdinOnSpy = vi.spyOn(process.stdin, 'on').mockImplementation(
|
||||
(
|
||||
event: string | symbol,
|
||||
listener: (...args: unknown[]) => void,
|
||||
) => {
|
||||
if (event === 'keypress') {
|
||||
listener('\u0003', { ctrl: true, name: 'c' });
|
||||
}
|
||||
return process.stdin;
|
||||
},
|
||||
);
|
||||
const stdinOnSpy = vi
|
||||
.spyOn(process.stdin, 'on')
|
||||
.mockImplementation(
|
||||
(event: string | symbol, listener: (...args: unknown[]) => void) => {
|
||||
if (event === 'keypress') {
|
||||
listener('\u0003', { ctrl: true, name: 'c' });
|
||||
}
|
||||
return process.stdin;
|
||||
},
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
vi.spyOn(process.stdin as any, 'setRawMode').mockImplementation(() => true);
|
||||
vi.spyOn(process.stdin, 'resume').mockImplementation(() => process.stdin);
|
||||
@@ -1377,6 +1400,9 @@ describe('runNonInteractive', () => {
|
||||
[{ text: '/unknowncommand' }],
|
||||
expect.any(AbortSignal),
|
||||
'prompt-id-unknown',
|
||||
undefined,
|
||||
false,
|
||||
'/unknowncommand',
|
||||
);
|
||||
|
||||
expect(getWrittenOutput()).toBe('Response to unknown\n');
|
||||
|
||||
@@ -301,7 +301,10 @@ export async function runNonInteractive({
|
||||
|
||||
// Start the agentic loop (runs in background)
|
||||
const { streamId } = await session.send({
|
||||
message: geminiPartsToContentParts(query),
|
||||
message: {
|
||||
content: geminiPartsToContentParts(query),
|
||||
displayContent: input,
|
||||
},
|
||||
});
|
||||
if (streamId === null) {
|
||||
throw new Error(
|
||||
@@ -465,18 +468,17 @@ export async function runNonInteractive({
|
||||
}
|
||||
|
||||
if (event.data?.['errorType'] === ToolErrorType.NO_SPACE_LEFT) {
|
||||
runTerminalExitHandler(() =>
|
||||
handleToolError(
|
||||
event.name,
|
||||
new Error(errorMsg),
|
||||
config,
|
||||
typeof event.data?.['errorType'] === 'string'
|
||||
? event.data['errorType']
|
||||
: undefined,
|
||||
displayText,
|
||||
),
|
||||
terminalProcessExitHandled = true;
|
||||
handleToolError(
|
||||
event.name,
|
||||
new Error(errorMsg),
|
||||
config,
|
||||
typeof event.data?.['errorType'] === 'string'
|
||||
? event.data['errorType']
|
||||
: undefined,
|
||||
displayText,
|
||||
);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
handleToolError(
|
||||
event.name,
|
||||
@@ -528,7 +530,9 @@ export async function runNonInteractive({
|
||||
typeof event.data?.['turnCount'] === 'number';
|
||||
|
||||
if (isConfiguredTurnLimit) {
|
||||
runTerminalExitHandler(() => handleMaxTurnsExceededError(config));
|
||||
runTerminalExitHandler(() =>
|
||||
handleMaxTurnsExceededError(config),
|
||||
);
|
||||
} else if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.ERROR,
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
isFatalToolError,
|
||||
debugLogger,
|
||||
coreEvents,
|
||||
getErrorType,
|
||||
getErrorMessage,
|
||||
getErrorType,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
@@ -7,7 +7,19 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { AgentSession } from './agent-session.js';
|
||||
import { MockAgentProtocol } from './mock.js';
|
||||
import type { AgentEvent } from './types.js';
|
||||
import type { AgentEvent, AgentSend } from './types.js';
|
||||
|
||||
function makeMessageSend(
|
||||
text: string,
|
||||
displayContent?: string,
|
||||
): Extract<AgentSend, { message: unknown }> {
|
||||
return {
|
||||
message: {
|
||||
content: [{ type: 'text', text }],
|
||||
...(displayContent ? { displayContent } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('AgentSession', () => {
|
||||
it('should passthrough simple methods', async () => {
|
||||
@@ -51,7 +63,7 @@ describe('AgentSession', () => {
|
||||
|
||||
const events: AgentEvent[] = [];
|
||||
for await (const event of session.sendStream({
|
||||
message: [{ type: 'text', text: 'hi' }],
|
||||
...makeMessageSend('hi'),
|
||||
})) {
|
||||
events.push(event);
|
||||
}
|
||||
@@ -139,7 +151,7 @@ describe('AgentSession', () => {
|
||||
|
||||
const events: AgentEvent[] = [];
|
||||
for await (const event of session.sendStream({
|
||||
message: [{ type: 'text', text: 'hi' }],
|
||||
...makeMessageSend('hi'),
|
||||
})) {
|
||||
events.push(event);
|
||||
}
|
||||
@@ -178,7 +190,7 @@ describe('AgentSession', () => {
|
||||
|
||||
protocol.pushResponse([{ type: 'message' }]);
|
||||
const { streamId } = await session.send({
|
||||
message: [{ type: 'text', text: 'request' }],
|
||||
...makeMessageSend('request'),
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
@@ -242,7 +254,7 @@ describe('AgentSession', () => {
|
||||
},
|
||||
]);
|
||||
await session.send({
|
||||
message: [{ type: 'text', text: 'request' }],
|
||||
...makeMessageSend('request'),
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
@@ -303,7 +315,7 @@ describe('AgentSession', () => {
|
||||
},
|
||||
]);
|
||||
const { streamId: streamId1 } = await session.send({
|
||||
message: [{ type: 'text', text: 'first request' }],
|
||||
...makeMessageSend('first request'),
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
@@ -315,7 +327,7 @@ describe('AgentSession', () => {
|
||||
},
|
||||
]);
|
||||
await session.send({
|
||||
message: [{ type: 'text', text: 'second request' }],
|
||||
...makeMessageSend('second request'),
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
|
||||
@@ -679,6 +679,7 @@ describe('mapError', () => {
|
||||
expect(result.status).toBe('RESOURCE_EXHAUSTED');
|
||||
expect(result.message).toBe('Rate limit');
|
||||
expect(result.fatal).toBe(true);
|
||||
expect(result._meta?.['status']).toBe(429);
|
||||
expect(result._meta?.['rawError']).toEqual({
|
||||
message: 'Rate limit',
|
||||
status: 429,
|
||||
|
||||
@@ -403,7 +403,7 @@ export function mapError(
|
||||
}
|
||||
|
||||
if (isStructuredError(error)) {
|
||||
const structuredMeta = { ...meta, rawError: error };
|
||||
const structuredMeta = { ...meta, rawError: error, status: error.status };
|
||||
return {
|
||||
status: mapHttpToGrpcStatus(error.status),
|
||||
message: error.message,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { LegacyAgentSession } from './legacy-agent-session.js';
|
||||
import type { LegacyAgentSessionDeps } from './legacy-agent-session.js';
|
||||
import { GeminiEventType } from '../core/turn.js';
|
||||
import type { ServerGeminiStreamEvent } from '../core/turn.js';
|
||||
import type { AgentEvent } from './types.js';
|
||||
import type { AgentEvent, AgentSend } from './types.js';
|
||||
import { ToolErrorType } from '../tools/tool-error.js';
|
||||
import type {
|
||||
CompletedToolCall,
|
||||
@@ -72,6 +72,18 @@ function makeToolRequest(callId: string, name: string): ToolCallRequestInfo {
|
||||
};
|
||||
}
|
||||
|
||||
function makeMessageSend(
|
||||
text: string,
|
||||
displayContent?: string,
|
||||
): Extract<AgentSend, { message: unknown }> {
|
||||
return {
|
||||
message: {
|
||||
content: [{ type: 'text', text }],
|
||||
...(displayContent ? { displayContent } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeCompletedToolCall(
|
||||
callId: string,
|
||||
name: string,
|
||||
@@ -140,9 +152,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
const result = await session.send({
|
||||
message: [{ type: 'text', text: 'hi' }],
|
||||
});
|
||||
const result = await session.send(makeMessageSend('hi'));
|
||||
|
||||
expect(result.streamId).toBe('test-stream');
|
||||
});
|
||||
@@ -162,7 +172,10 @@ describe('LegacyAgentSession', () => {
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
const { streamId } = await session.send({
|
||||
message: [{ type: 'text', text: 'hi' }],
|
||||
message: {
|
||||
content: [{ type: 'text', text: 'hi' }],
|
||||
displayContent: 'raw input',
|
||||
},
|
||||
_meta: { source: 'user-test' },
|
||||
});
|
||||
|
||||
@@ -170,12 +183,57 @@ describe('LegacyAgentSession', () => {
|
||||
(e): e is AgentEvent<'message'> =>
|
||||
e.type === 'message' && e.role === 'user' && e.streamId === streamId,
|
||||
);
|
||||
expect(userMessage?.content).toEqual([{ type: 'text', text: 'hi' }]);
|
||||
expect(userMessage?.content).toEqual([
|
||||
{ type: 'text', text: 'raw input' },
|
||||
]);
|
||||
expect(userMessage?._meta).toEqual({ source: 'user-test' });
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(sendMock).toHaveBeenCalledWith(
|
||||
[{ text: 'hi' }],
|
||||
expect.any(AbortSignal),
|
||||
'test-prompt',
|
||||
undefined,
|
||||
false,
|
||||
'raw input',
|
||||
);
|
||||
|
||||
await collectEvents(session, { streamId: streamId ?? undefined });
|
||||
});
|
||||
|
||||
it('accepts legacy message-array sends without displayContent', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
makeStream([
|
||||
{
|
||||
type: GeminiEventType.Finished,
|
||||
value: { reason: FinishReason.STOP, usageMetadata: undefined },
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
const { streamId } = await session.send({
|
||||
message: [{ type: 'text', text: 'hi' }],
|
||||
});
|
||||
|
||||
const userMessage = session.events.find(
|
||||
(e): e is AgentEvent<'message'> =>
|
||||
e.type === 'message' && e.role === 'user' && e.streamId === streamId,
|
||||
);
|
||||
expect(userMessage?.content).toEqual([{ type: 'text', text: 'hi' }]);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(sendMock).toHaveBeenCalledWith(
|
||||
[{ text: 'hi' }],
|
||||
expect.any(AbortSignal),
|
||||
'test-prompt',
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns streamId before emitting agent_start', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
@@ -195,9 +253,7 @@ describe('LegacyAgentSession', () => {
|
||||
liveEvents.push(event);
|
||||
});
|
||||
|
||||
const { streamId } = await session.send({
|
||||
message: [{ type: 'text', text: 'hi' }],
|
||||
});
|
||||
const { streamId } = await session.send(makeMessageSend('hi'));
|
||||
|
||||
expect(streamId).toBe('test-stream');
|
||||
expect(liveEvents.some((event) => event.type === 'agent_start')).toBe(
|
||||
@@ -235,14 +291,12 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
const { streamId } = await session.send({
|
||||
message: [{ type: 'text', text: 'first' }],
|
||||
});
|
||||
const { streamId } = await session.send(makeMessageSend('first'));
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
await expect(
|
||||
session.send({ message: [{ type: 'text', text: 'second' }] }),
|
||||
).rejects.toThrow('cannot be called while a stream is active');
|
||||
await expect(session.send(makeMessageSend('second'))).rejects.toThrow(
|
||||
'cannot be called while a stream is active',
|
||||
);
|
||||
|
||||
resolveHang?.();
|
||||
await collectEvents(session, { streamId: streamId ?? undefined });
|
||||
@@ -273,16 +327,12 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
const first = await session.send({
|
||||
message: [{ type: 'text', text: 'first' }],
|
||||
});
|
||||
const first = await session.send(makeMessageSend('first'));
|
||||
const firstEvents = await collectEvents(session, {
|
||||
streamId: first.streamId ?? undefined,
|
||||
});
|
||||
|
||||
const second = await session.send({
|
||||
message: [{ type: 'text', text: 'second' }],
|
||||
});
|
||||
const second = await session.send(makeMessageSend('second'));
|
||||
const secondEvents = await collectEvents(session, {
|
||||
streamId: second.streamId ?? undefined,
|
||||
});
|
||||
@@ -330,7 +380,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const types = events.map((e) => e.type);
|
||||
@@ -387,7 +437,7 @@ describe('LegacyAgentSession', () => {
|
||||
]);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'read a file' }] });
|
||||
await session.send(makeMessageSend('read a file'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const types = events.map((e) => e.type);
|
||||
@@ -455,9 +505,7 @@ describe('LegacyAgentSession', () => {
|
||||
scheduleMock.mockResolvedValueOnce([errorToolCall]);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({
|
||||
message: [{ type: 'text', text: 'write file' }],
|
||||
});
|
||||
await session.send(makeMessageSend('write file'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const toolResp = events.find(
|
||||
@@ -506,9 +554,7 @@ describe('LegacyAgentSession', () => {
|
||||
scheduleMock.mockResolvedValueOnce([stopToolCall]);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({
|
||||
message: [{ type: 'text', text: 'do something' }],
|
||||
});
|
||||
await session.send(makeMessageSend('do something'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const streamEnd = events.find(
|
||||
@@ -552,9 +598,7 @@ describe('LegacyAgentSession', () => {
|
||||
scheduleMock.mockResolvedValueOnce([fatalToolCall]);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({
|
||||
message: [{ type: 'text', text: 'write file' }],
|
||||
});
|
||||
await session.send(makeMessageSend('write file'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const toolResp = events.find(
|
||||
@@ -592,7 +636,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const streamEnd = events.find(
|
||||
@@ -621,7 +665,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const blocked = events.find(
|
||||
@@ -663,7 +707,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const err = events.find(
|
||||
@@ -690,7 +734,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const warning = events.find(
|
||||
@@ -738,7 +782,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const streamEnd = events.find(
|
||||
@@ -762,7 +806,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const errorEvents = events.filter(
|
||||
@@ -799,9 +843,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
const { streamId } = await session.send({
|
||||
message: [{ type: 'text', text: 'hi' }],
|
||||
});
|
||||
const { streamId } = await session.send(makeMessageSend('hi'));
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
await session.abort();
|
||||
@@ -847,7 +889,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
|
||||
// Give the loop time to start processing
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
@@ -891,9 +933,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
const { streamId } = await session.send({
|
||||
message: [{ type: 'text', text: 'hi' }],
|
||||
});
|
||||
const { streamId } = await session.send(makeMessageSend('hi'));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
await session.abort();
|
||||
@@ -935,7 +975,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
await collectEvents(session);
|
||||
|
||||
expect(session.events.length).toBeGreaterThan(0);
|
||||
@@ -964,9 +1004,7 @@ describe('LegacyAgentSession', () => {
|
||||
liveEvents.push(event);
|
||||
});
|
||||
|
||||
const { streamId } = await session.send({
|
||||
message: [{ type: 'text', text: 'hi' }],
|
||||
});
|
||||
const { streamId } = await session.send(makeMessageSend('hi'));
|
||||
await collectEvents(session, { streamId: streamId ?? undefined });
|
||||
unsubscribe();
|
||||
|
||||
@@ -1002,9 +1040,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
const first = await session.send({
|
||||
message: [{ type: 'text', text: 'first request' }],
|
||||
});
|
||||
const first = await session.send(makeMessageSend('first request'));
|
||||
await collectEvents(session, { streamId: first.streamId ?? undefined });
|
||||
|
||||
const liveEvents: AgentEvent[] = [];
|
||||
@@ -1012,9 +1048,7 @@ describe('LegacyAgentSession', () => {
|
||||
liveEvents.push(event);
|
||||
});
|
||||
|
||||
const second = await session.send({
|
||||
message: [{ type: 'text', text: 'second request' }],
|
||||
});
|
||||
const second = await session.send(makeMessageSend('second request'));
|
||||
await collectEvents(session, { streamId: second.streamId ?? undefined });
|
||||
unsubscribe();
|
||||
|
||||
@@ -1058,14 +1092,10 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
const first = await session.send({
|
||||
message: [{ type: 'text', text: 'first request' }],
|
||||
});
|
||||
const first = await session.send(makeMessageSend('first request'));
|
||||
await collectEvents(session, { streamId: first.streamId ?? undefined });
|
||||
|
||||
const second = await session.send({
|
||||
message: [{ type: 'text', text: 'second request' }],
|
||||
});
|
||||
const second = await session.send(makeMessageSend('second request'));
|
||||
await collectEvents(session, { streamId: second.streamId ?? undefined });
|
||||
|
||||
const firstStreamEvents = await collectEvents(session, {
|
||||
@@ -1120,14 +1150,10 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
const first = await session.send({
|
||||
message: [{ type: 'text', text: 'first request' }],
|
||||
});
|
||||
const first = await session.send(makeMessageSend('first request'));
|
||||
await collectEvents(session, { streamId: first.streamId ?? undefined });
|
||||
|
||||
await session.send({
|
||||
message: [{ type: 'text', text: 'second request' }],
|
||||
});
|
||||
await session.send(makeMessageSend('second request'));
|
||||
await collectEvents(session);
|
||||
|
||||
const firstAgentMessage = session.events.find(
|
||||
@@ -1175,7 +1201,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
expect(events.length).toBeGreaterThan(0);
|
||||
@@ -1196,7 +1222,7 @@ describe('LegacyAgentSession', () => {
|
||||
);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
expect(events[events.length - 1]?.type).toBe('agent_end');
|
||||
@@ -1244,7 +1270,7 @@ describe('LegacyAgentSession', () => {
|
||||
]);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'do it' }] });
|
||||
await session.send(makeMessageSend('do it'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
// Only one agent_end at the very end
|
||||
@@ -1291,7 +1317,7 @@ describe('LegacyAgentSession', () => {
|
||||
]);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'go' }] });
|
||||
await session.send(makeMessageSend('go'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
// Should have at least one usage event from the intermediate Finished
|
||||
@@ -1314,7 +1340,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const err = events.find(
|
||||
@@ -1342,7 +1368,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const err = events.find(
|
||||
@@ -1365,7 +1391,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const err = events.find(
|
||||
@@ -1385,7 +1411,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const err = events.find(
|
||||
@@ -1405,7 +1431,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
await session.send({ message: [{ type: 'text', text: 'hi' }] });
|
||||
await session.send(makeMessageSend('hi'));
|
||||
const events = await collectEvents(session);
|
||||
|
||||
const err = events.find(
|
||||
|
||||
@@ -93,6 +93,9 @@ class LegacyAgentProtocol implements AgentProtocol {
|
||||
'LegacyAgentSession.send() only supports message sends for the moment.',
|
||||
);
|
||||
}
|
||||
const normalizedMessage = Array.isArray(message)
|
||||
? { content: message, displayContent: undefined }
|
||||
: message;
|
||||
|
||||
if (this._activeStreamId) {
|
||||
// TODO: Interactive may eventually allow selected in-stream sends such as
|
||||
@@ -105,12 +108,16 @@ class LegacyAgentProtocol implements AgentProtocol {
|
||||
|
||||
this._beginNewStream();
|
||||
const streamId = this._translationState.streamId;
|
||||
const parts = contentPartsToGeminiParts(message);
|
||||
const userMessage = this._makeUserMessageEvent(message, payload._meta);
|
||||
const parts = contentPartsToGeminiParts(normalizedMessage.content);
|
||||
const userMessage = this._makeUserMessageEvent(
|
||||
normalizedMessage.content,
|
||||
normalizedMessage.displayContent,
|
||||
payload._meta,
|
||||
);
|
||||
|
||||
this._emit([userMessage]);
|
||||
|
||||
this._scheduleRunLoop(parts);
|
||||
this._scheduleRunLoop(parts, normalizedMessage.displayContent);
|
||||
|
||||
return { streamId };
|
||||
}
|
||||
@@ -119,18 +126,24 @@ class LegacyAgentProtocol implements AgentProtocol {
|
||||
this._abortController.abort();
|
||||
}
|
||||
|
||||
private _scheduleRunLoop(initialParts: Part[]): void {
|
||||
private _scheduleRunLoop(
|
||||
initialParts: Part[],
|
||||
displayContent?: string,
|
||||
): void {
|
||||
// Use a macrotask so send() resolves with the streamId before agent_start
|
||||
// is emitted and consumers can attach to the stream without racing startup.
|
||||
setTimeout(() => {
|
||||
void this._runLoopInBackground(initialParts);
|
||||
void this._runLoopInBackground(initialParts, displayContent);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private async _runLoopInBackground(initialParts: Part[]): Promise<void> {
|
||||
private async _runLoopInBackground(
|
||||
initialParts: Part[],
|
||||
displayContent?: string,
|
||||
): Promise<void> {
|
||||
this._ensureAgentStart();
|
||||
try {
|
||||
await this._runLoop(initialParts);
|
||||
await this._runLoop(initialParts, displayContent);
|
||||
} catch (err: unknown) {
|
||||
if (this._abortController.signal.aborted || isAbortLikeError(err)) {
|
||||
this._ensureAgentEnd('aborted');
|
||||
@@ -141,8 +154,12 @@ class LegacyAgentProtocol implements AgentProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private async _runLoop(initialParts: Part[]): Promise<void> {
|
||||
private async _runLoop(
|
||||
initialParts: Part[],
|
||||
initialDisplayContent?: string,
|
||||
): Promise<void> {
|
||||
let currentParts: Part[] = initialParts;
|
||||
let currentDisplayContent = initialDisplayContent;
|
||||
let turnCount = 0;
|
||||
const maxTurns = this._config.getMaxSessionTurns();
|
||||
|
||||
@@ -162,7 +179,11 @@ class LegacyAgentProtocol implements AgentProtocol {
|
||||
currentParts,
|
||||
this._abortController.signal,
|
||||
this._promptId,
|
||||
undefined,
|
||||
false,
|
||||
currentDisplayContent,
|
||||
);
|
||||
currentDisplayContent = undefined;
|
||||
|
||||
for await (const event of responseStream) {
|
||||
if (this._abortController.signal.aborted) {
|
||||
@@ -383,13 +404,17 @@ class LegacyAgentProtocol implements AgentProtocol {
|
||||
|
||||
private _makeUserMessageEvent(
|
||||
content: ContentPart[],
|
||||
displayContent?: string,
|
||||
meta?: Record<string, unknown>,
|
||||
): AgentEvent<'message'> {
|
||||
const eventContent: ContentPart[] = displayContent
|
||||
? [{ type: 'text', text: displayContent }]
|
||||
: content;
|
||||
const event = {
|
||||
...this._nextEventFields(),
|
||||
type: 'message',
|
||||
role: 'user',
|
||||
content,
|
||||
content: eventContent,
|
||||
...(meta ? { _meta: meta } : {}),
|
||||
} satisfies AgentEvent<'message'>;
|
||||
return event;
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('MockAgentProtocol', () => {
|
||||
const streamPromise = waitForStreamEnd(session);
|
||||
|
||||
const { streamId } = await session.send({
|
||||
message: [{ type: 'text', text: 'hi' }],
|
||||
message: { content: [{ type: 'text', text: 'hi' }] },
|
||||
});
|
||||
expect(streamId).toBeDefined();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
AgentEventData,
|
||||
AgentProtocol,
|
||||
AgentSend,
|
||||
ContentPart,
|
||||
Unsubscribe,
|
||||
} from './types.js';
|
||||
|
||||
@@ -133,11 +134,17 @@ export class MockAgentProtocol implements AgentProtocol {
|
||||
|
||||
// 1. User/Update event (BEFORE agent_start)
|
||||
if ('message' in payload && payload.message) {
|
||||
const message = Array.isArray(payload.message)
|
||||
? { content: payload.message, displayContent: undefined }
|
||||
: payload.message;
|
||||
const userContent: ContentPart[] = message.displayContent
|
||||
? [{ type: 'text', text: message.displayContent }]
|
||||
: message.content;
|
||||
eventsToEmit.push(
|
||||
normalize({
|
||||
type: 'message',
|
||||
role: 'user',
|
||||
content: payload.message,
|
||||
content: userContent,
|
||||
_meta: payload._meta,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -46,7 +46,12 @@ type RequireExactlyOne<T> = {
|
||||
}[keyof T];
|
||||
|
||||
interface AgentSendPayloads {
|
||||
message: ContentPart[];
|
||||
message:
|
||||
| ContentPart[]
|
||||
| {
|
||||
content: ContentPart[];
|
||||
displayContent?: string;
|
||||
};
|
||||
elicitations: ElicitationResponse[];
|
||||
update: { title?: string; model?: string; config?: Record<string, unknown> };
|
||||
action: { type: string; data: unknown };
|
||||
|
||||
Reference in New Issue
Block a user