mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-13 21:07:00 -07:00
fix: resolve typescript lint errors and test failures
- Remove unnecessary `any` casts and unsafe type assertions in `useAgentStream.ts`. - Introduce `MinimalTrackedToolCall` to safely type mock tool calls for inactivity monitors. - Fix arrow-body-style lint errors in `AppContainer.tsx` and `useAgentStream.ts`. - Update `nonInteractiveCli.test.ts` to include a required `build` method in mock tools to prevent TypeErrors during stream initialization. - Remove redundant non-null assertion in `legacy-agent-session.ts`.
This commit is contained in:
@@ -1519,6 +1519,9 @@ describe('runNonInteractive', () => {
|
||||
name: 'ShellTool',
|
||||
description: 'A shell tool',
|
||||
run: vi.fn(),
|
||||
build: vi.fn().mockReturnValue({
|
||||
getDescription: () => 'A shell tool',
|
||||
}),
|
||||
}),
|
||||
getFunctionDeclarations: vi.fn().mockReturnValue([{ name: 'ShellTool' }]),
|
||||
} as unknown as ToolRegistry);
|
||||
|
||||
@@ -82,6 +82,7 @@ import {
|
||||
buildUserSteeringHintPrompt,
|
||||
logBillingEvent,
|
||||
ApiKeyUpdatedEvent,
|
||||
LegacyAgentProtocol,
|
||||
type InjectionSource,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { validateAuthMethod } from '../config/auth.js';
|
||||
@@ -1092,8 +1093,44 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
};
|
||||
}, [config]);
|
||||
|
||||
const useAgentProtocol = config?.getExperimentalUseAgentProtocol() || false;
|
||||
const useActiveStream = useAgentProtocol ? useAgentStream : useGeminiStream;
|
||||
const streamAgent = useMemo(
|
||||
() =>
|
||||
config?.getExperimentalUseAgentProtocol()
|
||||
? new LegacyAgentProtocol({ config, getPreferredEditor })
|
||||
: undefined,
|
||||
[config, getPreferredEditor],
|
||||
);
|
||||
|
||||
const activeStream = streamAgent
|
||||
? // eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useAgentStream({
|
||||
agent: streamAgent,
|
||||
addItem: historyManager.addItem,
|
||||
onCancelSubmit,
|
||||
isShellFocused: embeddedShellFocused,
|
||||
})
|
||||
: // eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useGeminiStream(
|
||||
config.getGeminiClient(),
|
||||
historyManager.history,
|
||||
historyManager.addItem,
|
||||
config,
|
||||
settings,
|
||||
setDebugMessage,
|
||||
handleSlashCommand,
|
||||
shellModeActive,
|
||||
getPreferredEditor,
|
||||
onAuthError,
|
||||
performMemoryRefresh,
|
||||
modelSwitchedFromQuotaError,
|
||||
setModelSwitchedFromQuotaError,
|
||||
onCancelSubmit,
|
||||
setEmbeddedShellFocused,
|
||||
terminalWidth,
|
||||
terminalHeight,
|
||||
embeddedShellFocused,
|
||||
consumePendingHints,
|
||||
);
|
||||
|
||||
const {
|
||||
streamingState,
|
||||
@@ -1114,27 +1151,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
backgroundShells,
|
||||
dismissBackgroundShell,
|
||||
retryStatus,
|
||||
} = useActiveStream(
|
||||
config.getGeminiClient(),
|
||||
historyManager.history,
|
||||
historyManager.addItem,
|
||||
config,
|
||||
settings,
|
||||
setDebugMessage,
|
||||
handleSlashCommand,
|
||||
shellModeActive,
|
||||
getPreferredEditor,
|
||||
onAuthError,
|
||||
performMemoryRefresh,
|
||||
modelSwitchedFromQuotaError,
|
||||
setModelSwitchedFromQuotaError,
|
||||
onCancelSubmit,
|
||||
setEmbeddedShellFocused,
|
||||
terminalWidth,
|
||||
terminalHeight,
|
||||
embeddedShellFocused,
|
||||
consumePendingHints,
|
||||
);
|
||||
} = activeStream;
|
||||
|
||||
const pendingHistoryItems = useMemo(
|
||||
() => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems],
|
||||
|
||||
@@ -6,40 +6,17 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import {
|
||||
type Config,
|
||||
type GeminiClient,
|
||||
LegacyAgentSession as MockLegacyAgentSession,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type LoadedSettings } from '../../config/settings.js';
|
||||
import type { LegacyAgentProtocol } from '@google/gemini-cli-core';
|
||||
import { renderHookWithProviders } from '../../test-utils/render.js';
|
||||
|
||||
// --- MOCKS ---
|
||||
|
||||
const mockScheduler = vi.hoisted(() => ({
|
||||
schedule: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
cancelAll: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockLegacyAgentSession = vi.hoisted(() => ({
|
||||
const mockLegacyAgentProtocol = vi.hoisted(() => ({
|
||||
send: vi.fn().mockResolvedValue({ streamId: 'test-stream-id' }),
|
||||
subscribe: vi.fn().mockReturnValue(() => {}),
|
||||
abort: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('./useToolScheduler.js', () => ({
|
||||
useToolScheduler: vi.fn().mockReturnValue([
|
||||
[], // toolCalls
|
||||
vi.fn(), // schedule
|
||||
vi.fn(), // markToolsAsSubmitted
|
||||
vi.fn(), // setToolCallsForDisplay
|
||||
vi.fn(), // cancelAll
|
||||
0, // lastToolOutputTime
|
||||
mockScheduler, // scheduler
|
||||
]),
|
||||
}));
|
||||
|
||||
vi.mock('./useLogger.js', () => ({
|
||||
useLogger: vi.fn().mockReturnValue({
|
||||
logMessage: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -56,17 +33,6 @@ vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock core classes properly
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
return {
|
||||
...actual,
|
||||
LegacyAgentSession: vi
|
||||
.fn()
|
||||
.mockImplementation(() => mockLegacyAgentSession),
|
||||
};
|
||||
});
|
||||
|
||||
// --- END MOCKS ---
|
||||
|
||||
import { useAgentStream } from './useAgentStream.js';
|
||||
@@ -74,88 +40,40 @@ import { MessageType, StreamingState } from '../types.js';
|
||||
|
||||
describe('useAgentStream', () => {
|
||||
const mockAddItem = vi.fn();
|
||||
const mockOnDebugMessage = vi.fn();
|
||||
const mockHandleSlashCommand = vi.fn().mockResolvedValue(false);
|
||||
const mockOnAuthError = vi.fn();
|
||||
const mockPerformMemoryRefresh = vi.fn(() => Promise.resolve());
|
||||
const mockSetModelSwitchedFromQuotaError = vi.fn();
|
||||
const mockOnCancelSubmit = vi.fn();
|
||||
const mockSetShellInputFocused = vi.fn();
|
||||
|
||||
const mockConfig = {
|
||||
storage: {},
|
||||
getSessionId: () => 'test-session',
|
||||
getExperimentalUseAgentProtocol: () => true,
|
||||
getApprovalMode: () => 'default',
|
||||
getMessageBus: () => ({}),
|
||||
} as Config;
|
||||
|
||||
const mockSettings = {
|
||||
merged: {
|
||||
billing: { overageStrategy: 'stop' },
|
||||
ui: { errorVerbosity: 'full' },
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should initialize LegacyAgentSession on mount', async () => {
|
||||
it('should initialize on mount', async () => {
|
||||
await renderHookWithProviders(() =>
|
||||
useAgentStream(
|
||||
{} as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
mockSettings,
|
||||
mockOnDebugMessage,
|
||||
mockHandleSlashCommand,
|
||||
false,
|
||||
() => undefined,
|
||||
mockOnAuthError,
|
||||
mockPerformMemoryRefresh,
|
||||
false,
|
||||
mockSetModelSwitchedFromQuotaError,
|
||||
mockOnCancelSubmit,
|
||||
mockSetShellInputFocused,
|
||||
80,
|
||||
24,
|
||||
),
|
||||
useAgentStream({
|
||||
agent: mockLegacyAgentProtocol as unknown as LegacyAgentProtocol,
|
||||
addItem: mockAddItem,
|
||||
onCancelSubmit: mockOnCancelSubmit,
|
||||
isShellFocused: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(MockLegacyAgentSession).toHaveBeenCalled();
|
||||
expect(mockLegacyAgentSession.subscribe).toHaveBeenCalled();
|
||||
expect(mockLegacyAgentProtocol.subscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call session.send when submitQuery is called', async () => {
|
||||
it('should call agent.send when submitQuery is called', async () => {
|
||||
const { result } = await renderHookWithProviders(() =>
|
||||
useAgentStream(
|
||||
{} as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
mockSettings,
|
||||
mockOnDebugMessage,
|
||||
mockHandleSlashCommand,
|
||||
false,
|
||||
() => undefined,
|
||||
mockOnAuthError,
|
||||
mockPerformMemoryRefresh,
|
||||
false,
|
||||
mockSetModelSwitchedFromQuotaError,
|
||||
mockOnCancelSubmit,
|
||||
mockSetShellInputFocused,
|
||||
80,
|
||||
24,
|
||||
),
|
||||
useAgentStream({
|
||||
agent: mockLegacyAgentProtocol as unknown as LegacyAgentProtocol,
|
||||
addItem: mockAddItem,
|
||||
onCancelSubmit: mockOnCancelSubmit,
|
||||
isShellFocused: false,
|
||||
}),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('hello');
|
||||
});
|
||||
|
||||
expect(mockLegacyAgentSession.send).toHaveBeenCalledWith({
|
||||
expect(mockLegacyAgentProtocol.send).toHaveBeenCalledWith({
|
||||
message: [{ type: 'text', text: 'hello' }],
|
||||
});
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
@@ -166,67 +84,52 @@ describe('useAgentStream', () => {
|
||||
|
||||
it('should update streamingState based on agent_start and agent_end events', async () => {
|
||||
const { result } = await renderHookWithProviders(() =>
|
||||
useAgentStream(
|
||||
{} as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
mockSettings,
|
||||
mockOnDebugMessage,
|
||||
mockHandleSlashCommand,
|
||||
false,
|
||||
() => undefined,
|
||||
mockOnAuthError,
|
||||
mockPerformMemoryRefresh,
|
||||
false,
|
||||
mockSetModelSwitchedFromQuotaError,
|
||||
mockOnCancelSubmit,
|
||||
mockSetShellInputFocused,
|
||||
80,
|
||||
24,
|
||||
),
|
||||
useAgentStream({
|
||||
agent: mockLegacyAgentProtocol as unknown as LegacyAgentProtocol,
|
||||
addItem: mockAddItem,
|
||||
onCancelSubmit: mockOnCancelSubmit,
|
||||
isShellFocused: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const eventHandler = vi.mocked(mockLegacyAgentSession.subscribe).mock
|
||||
const eventHandler = vi.mocked(mockLegacyAgentProtocol.subscribe).mock
|
||||
.calls[0][0];
|
||||
|
||||
expect(result.current.streamingState).toBe(StreamingState.Idle);
|
||||
|
||||
act(() => {
|
||||
eventHandler({ type: 'agent_start' });
|
||||
eventHandler({
|
||||
type: 'agent_start',
|
||||
id: '1',
|
||||
timestamp: '',
|
||||
streamId: '',
|
||||
});
|
||||
});
|
||||
expect(result.current.streamingState).toBe(StreamingState.Responding);
|
||||
|
||||
act(() => {
|
||||
eventHandler({ type: 'agent_end', reason: 'completed' });
|
||||
eventHandler({
|
||||
type: 'agent_end',
|
||||
reason: 'completed',
|
||||
id: '2',
|
||||
timestamp: '',
|
||||
streamId: '',
|
||||
});
|
||||
});
|
||||
expect(result.current.streamingState).toBe(StreamingState.Idle);
|
||||
});
|
||||
|
||||
it('should accumulate text content and update pendingHistoryItems', async () => {
|
||||
const { result } = await renderHookWithProviders(() =>
|
||||
useAgentStream(
|
||||
{} as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
mockSettings,
|
||||
mockOnDebugMessage,
|
||||
mockHandleSlashCommand,
|
||||
false,
|
||||
() => undefined,
|
||||
mockOnAuthError,
|
||||
mockPerformMemoryRefresh,
|
||||
false,
|
||||
mockSetModelSwitchedFromQuotaError,
|
||||
mockOnCancelSubmit,
|
||||
mockSetShellInputFocused,
|
||||
80,
|
||||
24,
|
||||
),
|
||||
useAgentStream({
|
||||
agent: mockLegacyAgentProtocol as unknown as LegacyAgentProtocol,
|
||||
addItem: mockAddItem,
|
||||
onCancelSubmit: mockOnCancelSubmit,
|
||||
isShellFocused: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const eventHandler = vi.mocked(mockLegacyAgentSession.subscribe).mock
|
||||
const eventHandler = vi.mocked(mockLegacyAgentProtocol.subscribe).mock
|
||||
.calls[0][0];
|
||||
|
||||
act(() => {
|
||||
@@ -234,6 +137,9 @@ describe('useAgentStream', () => {
|
||||
type: 'message',
|
||||
role: 'agent',
|
||||
content: [{ type: 'text', text: 'Hello' }],
|
||||
id: '1',
|
||||
timestamp: '',
|
||||
streamId: '',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -248,6 +154,9 @@ describe('useAgentStream', () => {
|
||||
type: 'message',
|
||||
role: 'agent',
|
||||
content: [{ type: 'text', text: ' world' }],
|
||||
id: '2',
|
||||
timestamp: '',
|
||||
streamId: '',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -256,28 +165,15 @@ describe('useAgentStream', () => {
|
||||
|
||||
it('should process thought events and update thought state', async () => {
|
||||
const { result } = await renderHookWithProviders(() =>
|
||||
useAgentStream(
|
||||
{} as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
mockSettings,
|
||||
mockOnDebugMessage,
|
||||
mockHandleSlashCommand,
|
||||
false,
|
||||
() => undefined,
|
||||
mockOnAuthError,
|
||||
mockPerformMemoryRefresh,
|
||||
false,
|
||||
mockSetModelSwitchedFromQuotaError,
|
||||
mockOnCancelSubmit,
|
||||
mockSetShellInputFocused,
|
||||
80,
|
||||
24,
|
||||
),
|
||||
useAgentStream({
|
||||
agent: mockLegacyAgentProtocol as unknown as LegacyAgentProtocol,
|
||||
addItem: mockAddItem,
|
||||
onCancelSubmit: mockOnCancelSubmit,
|
||||
isShellFocused: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const eventHandler = vi.mocked(mockLegacyAgentSession.subscribe).mock
|
||||
const eventHandler = vi.mocked(mockLegacyAgentProtocol.subscribe).mock
|
||||
.calls[0][0];
|
||||
|
||||
act(() => {
|
||||
@@ -285,6 +181,9 @@ describe('useAgentStream', () => {
|
||||
type: 'message',
|
||||
role: 'agent',
|
||||
content: [{ type: 'thought', thought: '**Thinking** about tests' }],
|
||||
id: '1',
|
||||
timestamp: '',
|
||||
streamId: '',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -294,84 +193,21 @@ describe('useAgentStream', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should display error message when a tool call requires approval', async () => {
|
||||
let eventHandler: (event: unknown) => void = () => {};
|
||||
vi.spyOn(mockLegacyAgentSession, 'subscribe').mockImplementation(
|
||||
(handler) => {
|
||||
eventHandler = handler;
|
||||
return () => {};
|
||||
},
|
||||
);
|
||||
|
||||
await renderHookWithProviders(() =>
|
||||
useAgentStream(
|
||||
{} as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
mockSettings,
|
||||
mockOnDebugMessage,
|
||||
mockHandleSlashCommand,
|
||||
false,
|
||||
() => undefined,
|
||||
mockOnAuthError,
|
||||
mockPerformMemoryRefresh,
|
||||
false,
|
||||
mockSetModelSwitchedFromQuotaError,
|
||||
mockOnCancelSubmit,
|
||||
mockSetShellInputFocused,
|
||||
80,
|
||||
24,
|
||||
),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
eventHandler({
|
||||
type: 'error',
|
||||
status: 'UNIMPLEMENTED',
|
||||
message:
|
||||
'TODO: Tool approvals not yet implemented, please switch to YOLO mode to test.',
|
||||
fatal: true,
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.ERROR,
|
||||
text: 'TODO: Tool approvals not yet implemented, please switch to YOLO mode to test.',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call session.abort when cancelOngoingRequest is called', async () => {
|
||||
it('should call agent.abort when cancelOngoingRequest is called', async () => {
|
||||
const { result } = await renderHookWithProviders(() =>
|
||||
useAgentStream(
|
||||
{} as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
mockSettings,
|
||||
mockOnDebugMessage,
|
||||
mockHandleSlashCommand,
|
||||
false,
|
||||
() => undefined,
|
||||
mockOnAuthError,
|
||||
mockPerformMemoryRefresh,
|
||||
false,
|
||||
mockSetModelSwitchedFromQuotaError,
|
||||
mockOnCancelSubmit,
|
||||
mockSetShellInputFocused,
|
||||
80,
|
||||
24,
|
||||
),
|
||||
useAgentStream({
|
||||
agent: mockLegacyAgentProtocol as unknown as LegacyAgentProtocol,
|
||||
addItem: mockAddItem,
|
||||
onCancelSubmit: mockOnCancelSubmit,
|
||||
isShellFocused: false,
|
||||
}),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.cancelOngoingRequest();
|
||||
});
|
||||
|
||||
expect(mockLegacyAgentSession.abort).toHaveBeenCalled();
|
||||
expect(mockLegacyAgentProtocol.abort).toHaveBeenCalled();
|
||||
expect(mockOnCancelSubmit).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,27 +9,21 @@ import {
|
||||
getErrorMessage,
|
||||
MessageSenderType,
|
||||
debugLogger,
|
||||
LegacyAgentSession,
|
||||
geminiPartsToContentParts,
|
||||
parseThought,
|
||||
CoreToolCallStatus,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
type Config,
|
||||
type GeminiClient,
|
||||
type ApprovalMode,
|
||||
Kind,
|
||||
type EditorType,
|
||||
type ThoughtSummary,
|
||||
type RetryAttemptPayload,
|
||||
type AgentEvent,
|
||||
type AgentProtocol,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type PartListUnion } from '@google/genai';
|
||||
import type {
|
||||
HistoryItem,
|
||||
HistoryItemWithoutId,
|
||||
LoopDetectionConfirmationRequest,
|
||||
SlashCommandProcessorResult,
|
||||
IndividualToolCallDisplay,
|
||||
HistoryItemToolGroup,
|
||||
} from '../types.js';
|
||||
@@ -39,42 +33,29 @@ import { getToolGroupBorderAppearance } from '../utils/borderStyles.js';
|
||||
import { type BackgroundShell } from './shellCommandProcessor.js';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import { useLogger } from './useLogger.js';
|
||||
import { useToolScheduler } from './useToolScheduler.js';
|
||||
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { useStateAndRef } from './useStateAndRef.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { type MinimalTrackedToolCall } from './useTurnActivityMonitor.js';
|
||||
|
||||
export interface UseAgentStreamOptions {
|
||||
agent?: AgentProtocol;
|
||||
addItem: UseHistoryManagerReturn['addItem'];
|
||||
onCancelSubmit: (shouldRestorePrompt?: boolean) => void;
|
||||
isShellFocused?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* useAgentStream implements the interactive agent loop using the LegacyAgentSession (AgentProtocol).
|
||||
* It attempts to maintain parity with useGeminiStream while consolidating model/tool orchestration
|
||||
* into the unified core API.
|
||||
* useAgentStream implements the interactive agent loop using an AgentProtocol.
|
||||
* It is completely agnostic to the specific agent implementation.
|
||||
*/
|
||||
export const useAgentStream = (
|
||||
geminiClient: GeminiClient,
|
||||
_history: HistoryItem[],
|
||||
addItem: UseHistoryManagerReturn['addItem'],
|
||||
config: Config,
|
||||
_settings: LoadedSettings,
|
||||
_onDebugMessage: (message: string) => void,
|
||||
_handleSlashCommand: (
|
||||
cmd: PartListUnion,
|
||||
) => Promise<SlashCommandProcessorResult | false>,
|
||||
_shellModeActive: boolean,
|
||||
getPreferredEditor: () => EditorType | undefined,
|
||||
_onAuthError: (error: string) => void,
|
||||
_performMemoryRefresh: () => Promise<void>,
|
||||
_modelSwitchedFromQuotaError: boolean,
|
||||
_setModelSwitchedFromQuotaError: React.Dispatch<
|
||||
React.SetStateAction<boolean>
|
||||
>,
|
||||
onCancelSubmit: (shouldRestorePrompt?: boolean) => void,
|
||||
_setShellInputFocused: (value: boolean) => void,
|
||||
_terminalWidth: number,
|
||||
_terminalHeight: number,
|
||||
_isShellFocused?: boolean,
|
||||
_consumeUserHint?: () => string | null,
|
||||
) => {
|
||||
export const useAgentStream = ({
|
||||
agent,
|
||||
addItem,
|
||||
onCancelSubmit,
|
||||
isShellFocused,
|
||||
}: UseAgentStreamOptions) => {
|
||||
const config = useConfig();
|
||||
const [initError] = useState<string | null>(null);
|
||||
const [retryStatus] = useState<RetryAttemptPayload | null>(null);
|
||||
const [streamingState, setStreamingState] = useState<StreamingState>(
|
||||
@@ -82,8 +63,6 @@ export const useAgentStream = (
|
||||
);
|
||||
const [thought, setThought] = useState<ThoughtSummary | null>(null);
|
||||
|
||||
// Track the current session instance
|
||||
const sessionRef = useRef<LegacyAgentSession | null>(null);
|
||||
const currentStreamIdRef = useRef<string | null>(null);
|
||||
const userMessageTimestampRef = useRef<number>(0);
|
||||
const geminiMessageBufferRef = useRef<string>('');
|
||||
@@ -98,24 +77,8 @@ export const useAgentStream = (
|
||||
const [_isFirstToolInGroup, isFirstToolInGroupRef, setIsFirstToolInGroup] =
|
||||
useStateAndRef<boolean>(true);
|
||||
|
||||
const [
|
||||
toolCalls,
|
||||
_schedule,
|
||||
_markToolsAsSubmitted,
|
||||
_setToolCallsForDisplay,
|
||||
cancelAllToolCalls,
|
||||
lastOutputTime,
|
||||
scheduler,
|
||||
] = useToolScheduler(
|
||||
async (_completedTools) => {
|
||||
// LegacyAgentSession owns the loop, so we don't need to trigger next turns here.
|
||||
},
|
||||
config,
|
||||
getPreferredEditor,
|
||||
);
|
||||
|
||||
const { startNewPrompt } = useSessionStats();
|
||||
const logger = useLogger(config.storage);
|
||||
const logger = useLogger(config?.storage);
|
||||
|
||||
const activePtyId = undefined;
|
||||
const backgroundShellCount = 0;
|
||||
@@ -128,6 +91,24 @@ export const useAgentStream = (
|
||||
);
|
||||
const dismissBackgroundShell = useCallback(async (_pid: number) => {}, []);
|
||||
|
||||
// Use the trackedTools to mock pendingToolCalls for inactivity monitors
|
||||
const pendingToolCalls = useMemo(
|
||||
(): MinimalTrackedToolCall[] =>
|
||||
trackedTools.map((t) => ({
|
||||
request: {
|
||||
name: t.originalRequestName || t.name,
|
||||
args: { command: t.description },
|
||||
callId: t.callId,
|
||||
isClientInitiated: t.isClientInitiated ?? false,
|
||||
prompt_id: '',
|
||||
},
|
||||
status: t.status,
|
||||
})),
|
||||
[trackedTools],
|
||||
);
|
||||
|
||||
const lastOutputTime = Date.now(); // We could track actual time if needed, simplified for now
|
||||
|
||||
// TODO: Support LoopDetection confirmation requests
|
||||
const [loopDetectionConfirmationRequest] =
|
||||
useState<LoopDetectionConfirmationRequest | null>(null);
|
||||
@@ -141,13 +122,12 @@ export const useAgentStream = (
|
||||
}, [addItem, pendingHistoryItemRef, setPendingHistoryItem]);
|
||||
|
||||
const cancelOngoingRequest = useCallback(async () => {
|
||||
if (sessionRef.current) {
|
||||
await sessionRef.current.abort();
|
||||
cancelAllToolCalls(new AbortController().signal);
|
||||
if (agent) {
|
||||
await agent.abort();
|
||||
setStreamingState(StreamingState.Idle);
|
||||
onCancelSubmit(false);
|
||||
}
|
||||
}, [cancelAllToolCalls, onCancelSubmit]);
|
||||
}, [agent, onCancelSubmit]);
|
||||
|
||||
// TODO: Support native handleApprovalModeChange for Plan Mode
|
||||
const handleApprovalModeChange = useCallback(
|
||||
@@ -308,21 +288,11 @@ export const useAgentStream = (
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionRef.current) {
|
||||
return sessionRef.current.subscribe(handleEvent);
|
||||
if (agent) {
|
||||
return agent.subscribe(handleEvent);
|
||||
}
|
||||
return undefined;
|
||||
}, [handleEvent]);
|
||||
|
||||
// Handle initialization of the session
|
||||
if (!sessionRef.current) {
|
||||
sessionRef.current = new LegacyAgentSession({
|
||||
client: geminiClient,
|
||||
scheduler,
|
||||
config,
|
||||
promptId: '',
|
||||
});
|
||||
}
|
||||
}, [agent, handleEvent]);
|
||||
|
||||
const submitQuery = useCallback(
|
||||
async (
|
||||
@@ -330,7 +300,7 @@ export const useAgentStream = (
|
||||
options?: { isContinuation: boolean },
|
||||
_prompt_id?: string,
|
||||
) => {
|
||||
if (!sessionRef.current) return;
|
||||
if (!agent) return;
|
||||
|
||||
const timestamp = Date.now();
|
||||
userMessageTimestampRef.current = timestamp;
|
||||
@@ -349,7 +319,7 @@ export const useAgentStream = (
|
||||
);
|
||||
|
||||
try {
|
||||
const { streamId } = await sessionRef.current.send({
|
||||
const { streamId } = await agent.send({
|
||||
message: parts,
|
||||
});
|
||||
currentStreamIdRef.current = streamId;
|
||||
@@ -360,7 +330,7 @@ export const useAgentStream = (
|
||||
);
|
||||
}
|
||||
},
|
||||
[addItem, logger, startNewPrompt],
|
||||
[agent, addItem, logger, startNewPrompt],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -415,7 +385,7 @@ export const useAgentStream = (
|
||||
const appearance = getToolGroupBorderAppearance(
|
||||
{ type: 'tool_group', tools: trackedTools },
|
||||
activePtyId,
|
||||
!!_isShellFocused,
|
||||
!!isShellFocused,
|
||||
[],
|
||||
backgroundShells,
|
||||
);
|
||||
@@ -440,7 +410,7 @@ export const useAgentStream = (
|
||||
setIsFirstToolInGroup,
|
||||
addItem,
|
||||
activePtyId,
|
||||
_isShellFocused,
|
||||
isShellFocused,
|
||||
backgroundShells,
|
||||
]);
|
||||
|
||||
@@ -454,7 +424,7 @@ export const useAgentStream = (
|
||||
const appearance = getToolGroupBorderAppearance(
|
||||
{ type: 'tool_group', tools: trackedTools },
|
||||
activePtyId,
|
||||
!!_isShellFocused,
|
||||
!!isShellFocused,
|
||||
[],
|
||||
backgroundShells,
|
||||
);
|
||||
@@ -504,7 +474,7 @@ export const useAgentStream = (
|
||||
trackedTools,
|
||||
pushedToolCallIds,
|
||||
activePtyId,
|
||||
_isShellFocused,
|
||||
isShellFocused,
|
||||
backgroundShells,
|
||||
]);
|
||||
|
||||
@@ -523,7 +493,7 @@ export const useAgentStream = (
|
||||
pendingHistoryItems,
|
||||
thought,
|
||||
cancelOngoingRequest,
|
||||
pendingToolCalls: toolCalls,
|
||||
pendingToolCalls,
|
||||
handleApprovalModeChange,
|
||||
activePtyId,
|
||||
loopDetectionConfirmationRequest,
|
||||
|
||||
@@ -5,20 +5,22 @@
|
||||
*/
|
||||
|
||||
import { useInactivityTimer } from './useInactivityTimer.js';
|
||||
import { useTurnActivityMonitor } from './useTurnActivityMonitor.js';
|
||||
import {
|
||||
useTurnActivityMonitor,
|
||||
type MinimalTrackedToolCall,
|
||||
} from './useTurnActivityMonitor.js';
|
||||
import {
|
||||
SHELL_FOCUS_HINT_DELAY_MS,
|
||||
SHELL_ACTION_REQUIRED_TITLE_DELAY_MS,
|
||||
SHELL_SILENT_WORKING_TITLE_DELAY_MS,
|
||||
} from '../constants.js';
|
||||
import type { StreamingState } from '../types.js';
|
||||
import { type TrackedToolCall } from './useToolScheduler.js';
|
||||
|
||||
interface ShellInactivityStatusProps {
|
||||
activePtyId: number | string | null | undefined;
|
||||
lastOutputTime: number;
|
||||
streamingState: StreamingState;
|
||||
pendingToolCalls: TrackedToolCall[];
|
||||
pendingToolCalls: MinimalTrackedToolCall[];
|
||||
embeddedShellFocused: boolean;
|
||||
isInteractiveShellEnabled: boolean;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,16 @@
|
||||
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { hasRedirection } from '@google/gemini-cli-core';
|
||||
import { type TrackedToolCall } from './useToolScheduler.js';
|
||||
import {
|
||||
hasRedirection,
|
||||
type CoreToolCallStatus,
|
||||
type ToolCallRequestInfo,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
export interface MinimalTrackedToolCall {
|
||||
status: CoreToolCallStatus;
|
||||
request: ToolCallRequestInfo;
|
||||
}
|
||||
|
||||
export interface TurnActivityStatus {
|
||||
operationStartTime: number;
|
||||
@@ -21,7 +29,7 @@ export interface TurnActivityStatus {
|
||||
export const useTurnActivityMonitor = (
|
||||
streamingState: StreamingState,
|
||||
activePtyId: number | string | null | undefined,
|
||||
pendingToolCalls: TrackedToolCall[] = [],
|
||||
pendingToolCalls: MinimalTrackedToolCall[] = [],
|
||||
): TurnActivityStatus => {
|
||||
const [operationStartTime, setOperationStartTime] = useState(0);
|
||||
|
||||
|
||||
@@ -40,6 +40,11 @@ function createMockDeps(
|
||||
const mockConfig = {
|
||||
getMaxSessionTurns: vi.fn().mockReturnValue(-1),
|
||||
getModel: vi.fn().mockReturnValue('gemini-2.5-pro'),
|
||||
getGeminiClient: vi.fn().mockReturnValue(mockClient),
|
||||
getMessageBus: vi.fn().mockImplementation(() => ({
|
||||
subscribe: vi.fn(),
|
||||
unsubscribe: vi.fn(),
|
||||
})),
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -138,7 +143,7 @@ describe('LegacyAgentSession', () => {
|
||||
|
||||
describe('send', () => {
|
||||
it('returns streamId', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -158,7 +163,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('records the sent user message in the trajectory before send resolves', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -235,7 +240,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('returns streamId before emitting agent_start', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -275,7 +280,7 @@ describe('LegacyAgentSession', () => {
|
||||
|
||||
it('throws if send is called while a stream is active', async () => {
|
||||
let resolveHang: (() => void) | undefined;
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -303,7 +308,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('creates a new streamId after the previous stream completes', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock
|
||||
@@ -365,7 +370,7 @@ describe('LegacyAgentSession', () => {
|
||||
|
||||
describe('stream - basic flow', () => {
|
||||
it('emits agent_start, content messages, and agent_end', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -404,7 +409,7 @@ describe('LegacyAgentSession', () => {
|
||||
|
||||
describe('stream - tool calls', () => {
|
||||
it('handles a tool call round-trip', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
// First turn: model requests a tool
|
||||
@@ -431,7 +436,7 @@ describe('LegacyAgentSession', () => {
|
||||
]),
|
||||
);
|
||||
|
||||
const scheduleMock = deps.scheduler.schedule as ReturnType<typeof vi.fn>;
|
||||
const scheduleMock = deps.scheduler!.schedule as ReturnType<typeof vi.fn>;
|
||||
scheduleMock.mockResolvedValueOnce([
|
||||
makeCompletedToolCall('call-1', 'read_file', 'file contents'),
|
||||
]);
|
||||
@@ -464,7 +469,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('handles tool errors and sends error message in content', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValueOnce(
|
||||
@@ -501,7 +506,7 @@ describe('LegacyAgentSession', () => {
|
||||
},
|
||||
} as CompletedToolCall;
|
||||
|
||||
const scheduleMock = deps.scheduler.schedule as ReturnType<typeof vi.fn>;
|
||||
const scheduleMock = deps.scheduler!.schedule as ReturnType<typeof vi.fn>;
|
||||
scheduleMock.mockResolvedValueOnce([errorToolCall]);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
@@ -522,7 +527,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('stops on STOP_EXECUTION tool error', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValueOnce(
|
||||
@@ -550,7 +555,7 @@ describe('LegacyAgentSession', () => {
|
||||
},
|
||||
} as CompletedToolCall;
|
||||
|
||||
const scheduleMock = deps.scheduler.schedule as ReturnType<typeof vi.fn>;
|
||||
const scheduleMock = deps.scheduler!.schedule as ReturnType<typeof vi.fn>;
|
||||
scheduleMock.mockResolvedValueOnce([stopToolCall]);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
@@ -566,7 +571,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('treats fatal tool errors as tool_response followed by agent_end failed', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValueOnce(
|
||||
@@ -594,7 +599,7 @@ describe('LegacyAgentSession', () => {
|
||||
},
|
||||
} as CompletedToolCall;
|
||||
|
||||
const scheduleMock = deps.scheduler.schedule as ReturnType<typeof vi.fn>;
|
||||
const scheduleMock = deps.scheduler!.schedule as ReturnType<typeof vi.fn>;
|
||||
scheduleMock.mockResolvedValueOnce([fatalToolCall]);
|
||||
|
||||
const session = new LegacyAgentSession(deps);
|
||||
@@ -623,7 +628,7 @@ describe('LegacyAgentSession', () => {
|
||||
|
||||
describe('stream - terminal events', () => {
|
||||
it('handles AgentExecutionStopped', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -647,7 +652,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('handles AgentExecutionBlocked as non-terminal and continues the stream', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -694,7 +699,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('handles Error events', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -718,7 +723,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('handles LoopDetected as non-terminal warning event', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
// LoopDetected followed by more content — stream continues
|
||||
@@ -772,7 +777,7 @@ describe('LegacyAgentSession', () => {
|
||||
>;
|
||||
configMock.mockReturnValue(0);
|
||||
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -798,7 +803,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('treats GeminiClient MaxSessionTurns as a terminal max_turns stream end', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -827,7 +832,7 @@ describe('LegacyAgentSession', () => {
|
||||
describe('abort', () => {
|
||||
it('treats abort before the first model event as aborted without fatal error', async () => {
|
||||
let releaseAbort: (() => void) | undefined;
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -866,7 +871,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('aborts the stream', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
// Stream that yields content then checks abort signal via a deferred
|
||||
@@ -909,7 +914,7 @@ describe('LegacyAgentSession', () => {
|
||||
|
||||
it('treats abort during pending scheduler work as aborted without fatal error', async () => {
|
||||
let resolveSchedule: ((value: CompletedToolCall[]) => void) | undefined;
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -925,7 +930,7 @@ describe('LegacyAgentSession', () => {
|
||||
]),
|
||||
);
|
||||
|
||||
const scheduleMock = deps.scheduler.schedule as ReturnType<typeof vi.fn>;
|
||||
const scheduleMock = deps.scheduler!.schedule as ReturnType<typeof vi.fn>;
|
||||
scheduleMock.mockReturnValue(
|
||||
new Promise<CompletedToolCall[]>((resolve) => {
|
||||
resolveSchedule = resolve;
|
||||
@@ -961,7 +966,7 @@ describe('LegacyAgentSession', () => {
|
||||
|
||||
describe('events property', () => {
|
||||
it('accumulates all events', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -985,7 +990,7 @@ describe('LegacyAgentSession', () => {
|
||||
|
||||
describe('subscription and stream scoping', () => {
|
||||
it('subscribe receives live events for the next stream', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -1016,7 +1021,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('subscribe is live-only and does not replay old history when idle', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock
|
||||
@@ -1068,7 +1073,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('streams only the requested streamId', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock
|
||||
@@ -1126,7 +1131,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('resumes from eventId within the same stream only', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock
|
||||
@@ -1187,7 +1192,7 @@ describe('LegacyAgentSession', () => {
|
||||
|
||||
describe('agent_end ordering', () => {
|
||||
it('agent_end is always the final event yielded', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -1209,7 +1214,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('agent_end is final even after error events', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValue(
|
||||
@@ -1231,7 +1236,7 @@ describe('LegacyAgentSession', () => {
|
||||
|
||||
describe('intermediate Finished events', () => {
|
||||
it('does NOT emit agent_end when tool calls are pending', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
// First turn: tool request + Finished (should NOT produce agent_end)
|
||||
@@ -1264,7 +1269,7 @@ describe('LegacyAgentSession', () => {
|
||||
]),
|
||||
);
|
||||
|
||||
const scheduleMock = deps.scheduler.schedule as ReturnType<typeof vi.fn>;
|
||||
const scheduleMock = deps.scheduler!.schedule as ReturnType<typeof vi.fn>;
|
||||
scheduleMock.mockResolvedValueOnce([
|
||||
makeCompletedToolCall('call-1', 'read_file', 'data'),
|
||||
]);
|
||||
@@ -1280,7 +1285,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('emits usage for intermediate Finished events', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockReturnValueOnce(
|
||||
@@ -1311,7 +1316,7 @@ describe('LegacyAgentSession', () => {
|
||||
]),
|
||||
);
|
||||
|
||||
const scheduleMock = deps.scheduler.schedule as ReturnType<typeof vi.fn>;
|
||||
const scheduleMock = deps.scheduler!.schedule as ReturnType<typeof vi.fn>;
|
||||
scheduleMock.mockResolvedValueOnce([
|
||||
makeCompletedToolCall('call-1', 'read_file', 'contents'),
|
||||
]);
|
||||
@@ -1332,7 +1337,7 @@ describe('LegacyAgentSession', () => {
|
||||
|
||||
describe('error handling in runLoop', () => {
|
||||
it('catches thrown errors and emits error + agent_end', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
sendMock.mockImplementation(() => {
|
||||
@@ -1358,7 +1363,7 @@ describe('LegacyAgentSession', () => {
|
||||
|
||||
describe('_emitErrorAndAgentEnd metadata', () => {
|
||||
it('preserves exitCode and code in _meta for FatalError', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
// Simulate a FatalError being thrown
|
||||
@@ -1381,7 +1386,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('preserves exitCode for non-FatalError errors that carry one', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const exitCodeError = new Error('custom exit');
|
||||
@@ -1401,7 +1406,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('preserves code in _meta for errors with code property', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const codedError = new Error('ENOENT');
|
||||
@@ -1421,7 +1426,7 @@ describe('LegacyAgentSession', () => {
|
||||
});
|
||||
|
||||
it('preserves status in _meta for errors with status property', async () => {
|
||||
const sendMock = deps.client.sendMessageStream as ReturnType<
|
||||
const sendMock = deps.client!.sendMessageStream as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const statusError = new Error('rate limited');
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { Part } from '@google/genai';
|
||||
import type { GeminiClient } from '../core/client.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import type { ToolCallRequestInfo } from '../scheduler/types.js';
|
||||
import type { Scheduler } from '../scheduler/scheduler.js';
|
||||
import { Scheduler } from '../scheduler/scheduler.js';
|
||||
import { recordToolCallInteractions } from '../code_assist/telemetry.js';
|
||||
import { ToolErrorType, isFatalToolError } from '../tools/tool-error.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
@@ -46,15 +46,18 @@ function isAbortLikeError(err: unknown): boolean {
|
||||
return err instanceof Error && err.name === 'AbortError';
|
||||
}
|
||||
|
||||
import type { EditorType } from '../utils/editor.js';
|
||||
|
||||
export interface LegacyAgentSessionDeps {
|
||||
client: GeminiClient;
|
||||
scheduler: Scheduler;
|
||||
config: Config;
|
||||
promptId: string;
|
||||
client?: GeminiClient;
|
||||
scheduler?: Scheduler;
|
||||
promptId?: string;
|
||||
streamId?: string;
|
||||
getPreferredEditor?: () => EditorType | undefined;
|
||||
}
|
||||
|
||||
class LegacyAgentProtocol implements AgentProtocol {
|
||||
export class LegacyAgentProtocol implements AgentProtocol {
|
||||
private _events: AgentEvent[] = [];
|
||||
private _subscribers = new Set<(event: AgentEvent) => void>();
|
||||
private _translationState: TranslationState;
|
||||
@@ -71,10 +74,16 @@ class LegacyAgentProtocol implements AgentProtocol {
|
||||
constructor(deps: LegacyAgentSessionDeps) {
|
||||
this._translationState = createTranslationState(deps.streamId);
|
||||
this._nextStreamIdOverride = deps.streamId;
|
||||
this._client = deps.client;
|
||||
this._scheduler = deps.scheduler;
|
||||
this._config = deps.config;
|
||||
this._promptId = deps.promptId;
|
||||
this._client = deps.client ?? deps.config.getGeminiClient();
|
||||
this._promptId = deps.promptId ?? deps.config.promptId ?? '';
|
||||
this._scheduler =
|
||||
deps.scheduler ??
|
||||
new Scheduler({
|
||||
context: deps.config,
|
||||
schedulerId: 'legacy-agent-scheduler',
|
||||
getPreferredEditor: deps.getPreferredEditor ?? (() => undefined),
|
||||
});
|
||||
}
|
||||
|
||||
get events(): readonly AgentEvent[] {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Kind } from 'src/tools/tools.js';
|
||||
import type { Kind } from '../tools/tools.js';
|
||||
|
||||
export type WithMeta = { _meta?: Record<string, unknown> };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user