feat(cli): migrate nonInteractiveCli to LegacyAgentSession (#22987)

This commit is contained in:
Adam Weidman
2026-04-02 16:21:40 -04:00
committed by GitHub
parent 7c3469713d
commit 6fb58bd31f
15 changed files with 3241 additions and 106 deletions
+19 -7
View File
@@ -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,
+1 -1
View File
@@ -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,8 +183,19 @@ 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 });
});
@@ -195,9 +219,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 +257,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 +293,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 +346,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 +403,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 +471,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 +520,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 +564,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 +602,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 +631,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 +673,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 +700,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 +748,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 +772,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 +809,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 +855,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 +899,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 +941,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 +970,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 +1006,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 +1014,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 +1058,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 +1116,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 +1167,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 +1188,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 +1236,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 +1283,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 +1306,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 +1334,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 +1357,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 +1377,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 +1397,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(
@@ -105,12 +105,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(message.content);
const userMessage = this._makeUserMessageEvent(
message.content,
message.displayContent,
payload._meta,
);
this._emit([userMessage]);
this._scheduleRunLoop(parts);
this._scheduleRunLoop(parts, message.displayContent);
return { streamId };
}
@@ -119,18 +123,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 +151,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 +176,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 +401,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;
+1 -1
View File
@@ -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();
+8 -1
View File
@@ -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,
}),
);
+4 -1
View File
@@ -46,7 +46,10 @@ type RequireExactlyOne<T> = {
}[keyof T];
interface AgentSendPayloads {
message: ContentPart[];
message: {
content: ContentPart[];
displayContent?: string;
};
elicitations: ElicitationResponse[];
update: { title?: string; model?: string; config?: Record<string, unknown> };
action: { type: string; data: unknown };