diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index de8c7d2122..7d1e880159 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1093,6 +1093,7 @@ Logging in with Google... Restarting Gemini CLI to continue. }, [config]); const useAgentProtocol = config.getExperimentalUseAgentProtocol(); + const useActiveStream = useAgentProtocol ? useAgentStream : useGeminiStream; const { streamingState, @@ -1113,50 +1114,27 @@ Logging in with Google... Restarting Gemini CLI to continue. backgroundShells, dismissBackgroundShell, retryStatus, - // eslint-disable-next-line react-hooks/rules-of-hooks - } = useAgentProtocol - ? useAgentStream( - config.getGeminiClient(), - historyManager.history, - historyManager.addItem, - config, - settings, - setDebugMessage, - handleSlashCommand, - shellModeActive, - getPreferredEditor, - onAuthError, - performMemoryRefresh, - modelSwitchedFromQuotaError, - setModelSwitchedFromQuotaError, - onCancelSubmit, - setEmbeddedShellFocused, - terminalWidth, - terminalHeight, - embeddedShellFocused, - consumePendingHints, - ) - : useGeminiStream( - config.getGeminiClient(), - historyManager.history, - historyManager.addItem, - config, - settings, - setDebugMessage, - handleSlashCommand, - shellModeActive, - getPreferredEditor, - onAuthError, - performMemoryRefresh, - modelSwitchedFromQuotaError, - setModelSwitchedFromQuotaError, - onCancelSubmit, - setEmbeddedShellFocused, - terminalWidth, - terminalHeight, - embeddedShellFocused, - consumePendingHints, - ); + } = useActiveStream( + config.getGeminiClient(), + historyManager.history, + historyManager.addItem, + config, + settings, + setDebugMessage, + handleSlashCommand, + shellModeActive, + getPreferredEditor, + onAuthError, + performMemoryRefresh, + modelSwitchedFromQuotaError, + setModelSwitchedFromQuotaError, + onCancelSubmit, + setEmbeddedShellFocused, + terminalWidth, + terminalHeight, + embeddedShellFocused, + consumePendingHints, + ); const pendingHistoryItems = useMemo( () => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems], @@ -1729,7 +1707,7 @@ Logging in with Google... Restarting Gemini CLI to continue. if (keyMatchers[Command.QUIT](key)) { // If the user presses Ctrl+C, we want to cancel any ongoing requests. // This should happen regardless of the count. - cancelOngoingRequest?.(); + void cancelOngoingRequest?.(); handleCtrlCPress(); return true; diff --git a/packages/cli/src/ui/hooks/useAgentStream.test.tsx b/packages/cli/src/ui/hooks/useAgentStream.test.tsx index 5c0ae48edd..0c87aca84e 100644 --- a/packages/cli/src/ui/hooks/useAgentStream.test.tsx +++ b/packages/cli/src/ui/hooks/useAgentStream.test.tsx @@ -4,14 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - describe, - it, - expect, - vi, - beforeEach, -} from 'vitest'; +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 { renderHookWithProviders } from '../../test-utils/render.js'; // --- MOCKS --- @@ -47,9 +47,9 @@ vi.mock('./useLogger.js', () => ({ })); vi.mock('../contexts/SessionContext.js', async (importOriginal) => { - const actual = await importOriginal(); + const actual = await importOriginal>(); return { - ...(actual as any), + ...actual, useSessionStats: vi.fn(() => ({ startNewPrompt: vi.fn(), })), @@ -58,19 +58,18 @@ vi.mock('../contexts/SessionContext.js', async (importOriginal) => { // Mock core classes properly vi.mock('@google/gemini-cli-core', async (importOriginal) => { - const actual = await importOriginal() as any; + const actual = await importOriginal>(); return { ...actual, - LegacyAgentSession: vi.fn().mockImplementation(() => mockLegacyAgentSession), + LegacyAgentSession: vi + .fn() + .mockImplementation(() => mockLegacyAgentSession), }; }); // --- END MOCKS --- import { useAgentStream } from './useAgentStream.js'; -import { - LegacyAgentSession as MockLegacyAgentSession, -} from '@google/gemini-cli-core'; import { MessageType, StreamingState } from '../types.js'; describe('useAgentStream', () => { @@ -89,14 +88,14 @@ describe('useAgentStream', () => { getExperimentalUseAgentProtocol: () => true, getApprovalMode: () => 'default', getMessageBus: () => ({}), - } as any; + } as Config; const mockSettings = { merged: { billing: { overageStrategy: 'stop' }, ui: { errorVerbosity: 'full' }, }, - } as any; + } as unknown as LoadedSettings; beforeEach(() => { vi.clearAllMocks(); @@ -105,7 +104,7 @@ describe('useAgentStream', () => { it('should initialize LegacyAgentSession on mount', async () => { await renderHookWithProviders(() => useAgentStream( - {} as any, + {} as GeminiClient, [], mockAddItem, mockConfig, @@ -132,7 +131,7 @@ describe('useAgentStream', () => { it('should call session.send when submitQuery is called', async () => { const { result } = await renderHookWithProviders(() => useAgentStream( - {} as any, + {} as GeminiClient, [], mockAddItem, mockConfig, @@ -168,7 +167,7 @@ describe('useAgentStream', () => { it('should update streamingState based on agent_start and agent_end events', async () => { const { result } = await renderHookWithProviders(() => useAgentStream( - {} as any, + {} as GeminiClient, [], mockAddItem, mockConfig, @@ -188,7 +187,8 @@ describe('useAgentStream', () => { ), ); - const eventHandler = (mockLegacyAgentSession.subscribe as any).mock.calls[0][0]; + const eventHandler = vi.mocked(mockLegacyAgentSession.subscribe).mock + .calls[0][0]; expect(result.current.streamingState).toBe(StreamingState.Idle); @@ -206,7 +206,7 @@ describe('useAgentStream', () => { it('should accumulate text content and update pendingHistoryItems', async () => { const { result } = await renderHookWithProviders(() => useAgentStream( - {} as any, + {} as GeminiClient, [], mockAddItem, mockConfig, @@ -226,7 +226,8 @@ describe('useAgentStream', () => { ), ); - const eventHandler = (mockLegacyAgentSession.subscribe as any).mock.calls[0][0]; + const eventHandler = vi.mocked(mockLegacyAgentSession.subscribe).mock + .calls[0][0]; act(() => { eventHandler({ @@ -256,7 +257,7 @@ describe('useAgentStream', () => { it('should process thought events and update thought state', async () => { const { result } = await renderHookWithProviders(() => useAgentStream( - {} as any, + {} as GeminiClient, [], mockAddItem, mockConfig, @@ -276,7 +277,8 @@ describe('useAgentStream', () => { ), ); - const eventHandler = (mockLegacyAgentSession.subscribe as any).mock.calls[0][0]; + const eventHandler = vi.mocked(mockLegacyAgentSession.subscribe).mock + .calls[0][0]; act(() => { eventHandler({ @@ -293,15 +295,17 @@ describe('useAgentStream', () => { }); it('should display error message when a tool call requires approval', async () => { - let eventHandler: (event: any) => void = () => {}; - vi.spyOn(mockLegacyAgentSession, 'subscribe').mockImplementation((handler) => { - eventHandler = handler; - return () => {}; - }); + let eventHandler: (event: unknown) => void = () => {}; + vi.spyOn(mockLegacyAgentSession, 'subscribe').mockImplementation( + (handler) => { + eventHandler = handler; + return () => {}; + }, + ); await renderHookWithProviders(() => useAgentStream( - {} as any, + {} as GeminiClient, [], mockAddItem, mockConfig, @@ -325,7 +329,8 @@ describe('useAgentStream', () => { eventHandler({ type: 'error', status: 'UNIMPLEMENTED', - message: 'TODO: Tool approvals not yet implemented, please switch to YOLO mode to test.', + message: + 'TODO: Tool approvals not yet implemented, please switch to YOLO mode to test.', fatal: true, }); }); @@ -342,7 +347,7 @@ describe('useAgentStream', () => { it('should call session.abort when cancelOngoingRequest is called', async () => { const { result } = await renderHookWithProviders(() => useAgentStream( - {} as any, + {} as GeminiClient, [], mockAddItem, mockConfig, diff --git a/packages/cli/src/ui/hooks/useAgentStream.ts b/packages/cli/src/ui/hooks/useAgentStream.ts index 52bcbb626c..9c6e6c3bd6 100644 --- a/packages/cli/src/ui/hooks/useAgentStream.ts +++ b/packages/cli/src/ui/hooks/useAgentStream.ts @@ -8,25 +8,30 @@ import { useState, useRef, useCallback, useEffect, useMemo } from 'react'; import { getErrorMessage, MessageSenderType, - ApprovalMode, debugLogger, LegacyAgentSession, geminiPartsToContentParts, parseThought, + CoreToolCallStatus, } from '@google/gemini-cli-core'; -import type { - Config, - EditorType, - GeminiClient, - ThoughtSummary, - RetryAttemptPayload, - AgentEvent, +import { + type Config, + type GeminiClient, + type ApprovalMode, + Kind, + type EditorType, + type ThoughtSummary, + type RetryAttemptPayload, + type AgentEvent, + BaseDeclarativeTool, + type ToolResult, } from '@google/gemini-cli-core'; import { type PartListUnion } from '@google/genai'; import type { HistoryItem, HistoryItemWithoutId, LoopDetectionConfirmationRequest, + SlashCommandProcessorResult, } from '../types.js'; import { StreamingState, MessageType } from '../types.js'; import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js'; @@ -35,18 +40,47 @@ import { type BackgroundShell } from './shellCommandProcessor.js'; import type { UseHistoryManagerReturn } from './useHistoryManager.js'; import { useLogger } from './useLogger.js'; import { mapToDisplay as mapTrackedToolCallsToDisplay } from './toolMapping.js'; -import { - useToolScheduler, -} from './useToolScheduler.js'; -import type { - TrackedToolCall, -} from './useToolScheduler.js'; +import { useToolScheduler } from './useToolScheduler.js'; +import type { TrackedToolCall } from './useToolScheduler.js'; import { useSessionStats } from '../contexts/SessionContext.js'; import type { LoadedSettings } from '../../config/settings.js'; -import type { SlashCommandProcessorResult } from '../types.js'; import { useStateAndRef } from './useStateAndRef.js'; +class DummyTool extends BaseDeclarativeTool< + Record, + ToolResult +> { + constructor( + name: string, + description: string, + displayName: string, + isOutputMarkdown: boolean, + kind: Kind, + messageBus: import('@google/gemini-cli-core').MessageBus, + ) { + super( + name, + displayName, + description, + kind, + undefined, + messageBus, + isOutputMarkdown, + false, + ); + } + protected createInvocation(params: Record) { + return { + getDescription: () => this.description, + params, + execute: async () => ({ llmContent: [], returnDisplay: '' }), + toolLocations: () => [], + shouldConfirmExecute: async (): Promise => false, + }; + } +} + /** * useAgentStream implements the interactive agent loop using the LegacyAgentSession (AgentProtocol). * It attempts to maintain parity with useGeminiStream while consolidating model/tool orchestration @@ -67,7 +101,9 @@ export const useAgentStream = ( _onAuthError: (error: string) => void, _performMemoryRefresh: () => Promise, _modelSwitchedFromQuotaError: boolean, - _setModelSwitchedFromQuotaError: React.Dispatch>, + _setModelSwitchedFromQuotaError: React.Dispatch< + React.SetStateAction + >, onCancelSubmit: (shouldRestorePrompt?: boolean) => void, _setShellInputFocused: (value: boolean) => void, _terminalWidth: number, @@ -76,9 +112,7 @@ export const useAgentStream = ( _consumeUserHint?: () => string | null, ) => { const [initError] = useState(null); - const [retryStatus] = useState( - null, - ); + const [retryStatus] = useState(null); const [streamingState, setStreamingState] = useState( StreamingState.Idle, ); @@ -92,8 +126,9 @@ export const useAgentStream = ( const [pendingHistoryItem, pendingHistoryItemRef, setPendingHistoryItem] = useStateAndRef(null); - const [trackedTools, , setTrackedTools] = - useStateAndRef([]); + const [trackedTools, , setTrackedTools] = useStateAndRef( + [], + ); const [pushedToolCallIds, pushedToolCallIdsRef, setPushedToolCallIds] = useStateAndRef>(new Set()); const [_isFirstToolInGroup, isFirstToolInGroupRef, setIsFirstToolInGroup] = @@ -123,20 +158,19 @@ export const useAgentStream = ( const isBackgroundShellVisible = false; const toggleBackgroundShell = useCallback(() => {}, []); const backgroundCurrentShell = undefined; - const backgroundShells = new Map(); + const backgroundShells = useMemo( + () => new Map(), + [], + ); const dismissBackgroundShell = useCallback(async (_pid: number) => {}, []); // TODO: Support LoopDetection confirmation requests - const [ - loopDetectionConfirmationRequest, - ] = useState(null); + const [loopDetectionConfirmationRequest] = + useState(null); const flushPendingText = useCallback(() => { if (pendingHistoryItemRef.current) { - addItem( - pendingHistoryItemRef.current, - userMessageTimestampRef.current, - ); + addItem(pendingHistoryItemRef.current, userMessageTimestampRef.current); setPendingHistoryItem(null); geminiMessageBufferRef.current = ''; } @@ -206,84 +240,201 @@ export const useAgentStream = ( } } break; - case 'tool_request': + case 'tool_request': { flushPendingText(); - setTrackedTools((prev) => [ - ...prev, - { - request: { - callId: event.requestId, - name: event.name, - args: event.args, - isClientInitiated: false, - originalRequestName: event.name, - }, - status: 'scheduled', - tool: { - displayName: (event._meta?.['displayName'] as string) ?? event.name, - isOutputMarkdown: (event._meta?.['isOutputMarkdown'] as boolean) ?? false, - kind: event._meta?.['kind'] as any, - }, - invocation: { - getDescription: () => (event._meta?.['description'] as string) ?? '', - }, - } as unknown as TrackedToolCall, - ]); + const legacyState = event._meta?.legacyState; + const displayName = legacyState?.displayName ?? event.name; + const isOutputMarkdown = legacyState?.isOutputMarkdown ?? false; + const desc = legacyState?.description ?? ''; + + const args = + event.args && typeof event.args === 'object' ? event.args : {}; + const fallbackKind = Kind.Other; + const messageBus = config.getMessageBus(); + + const tool = + config.getToolRegistry().getTool(event.name) || + new DummyTool( + event.name, + desc, + displayName, + isOutputMarkdown, + fallbackKind, + messageBus, + ); + const invocation = tool.build(args); + + const newCall: TrackedToolCall = { + request: { + callId: event.requestId, + name: event.name, + args, + isClientInitiated: false, + originalRequestName: event.name, + prompt_id: '', + }, + status: CoreToolCallStatus.Scheduled, + tool, + invocation, + }; + setTrackedTools((prev) => [...prev, newCall]); break; - case 'tool_update': + } + case 'tool_update': { setTrackedTools((prev) => - prev.map((tc) => - tc.request.callId === event.requestId - ? ({ + prev.map((tc): TrackedToolCall => { + if (tc.request.callId !== event.requestId) return tc; + + const legacyState = event._meta?.legacyState; + const evtStatus = legacyState?.status; + + let status = tc.status; + if (evtStatus === 'executing') + status = CoreToolCallStatus.Executing; + else if (evtStatus === 'error') status = CoreToolCallStatus.Error; + else if (evtStatus === 'success') + status = CoreToolCallStatus.Success; + + const liveOutput = + event.displayContent?.[0]?.type === 'text' + ? event.displayContent[0].text + : 'liveOutput' in tc + ? tc.liveOutput + : undefined; + const progressMessage = + legacyState?.progressMessage ?? + ('progressMessage' in tc ? tc.progressMessage : undefined); + const progress = + legacyState?.progress ?? + ('progress' in tc ? tc.progress : undefined); + const progressTotal = + legacyState?.progressTotal ?? + ('progressTotal' in tc ? tc.progressTotal : undefined); + const pid = + legacyState?.pid ?? ('pid' in tc ? tc.pid : undefined); + const desc = + legacyState?.description ?? + ('invocation' in tc && tc.invocation + ? tc.invocation.getDescription() + : ''); + const invocation = + 'invocation' in tc && tc.invocation + ? { ...tc.invocation, getDescription: () => desc } + : undefined; + + const inProgressFields = { + pid, + liveOutput, + progress, + progressTotal, + progressMessage, + invocation, + }; + + const response = + 'response' in tc && tc.response + ? tc.response + : { callId: tc.request.callId, responseParts: [] }; + const responseSubmittedToGemini = + 'responseSubmittedToGemini' in tc + ? tc.responseSubmittedToGemini + : false; + + switch (status) { + case CoreToolCallStatus.Executing: + return { ...tc, - status: (event.data?.['status'] as any) ?? tc.status, - liveOutput: - event.displayContent?.[0]?.type === 'text' - ? event.displayContent[0].text - : (tc as any).liveOutput, - progressMessage: - (event.data?.['progressMessage'] as string | undefined) ?? - (tc as any).progressMessage, - progress: - (event.data?.['progress'] as number | undefined) ?? - (tc as any).progress, - progressTotal: - (event.data?.['progressTotal'] as number | undefined) ?? - (tc as any).progressTotal, - pid: - (event.data?.['pid'] as number | undefined) ?? - (tc as any).pid, - invocation: { - getDescription: () => - (event._meta?.['description'] as string) ?? - (tc as any).invocation?.getDescription(), - }, - } as unknown as TrackedToolCall) - : tc, - ), + ...inProgressFields, + status: CoreToolCallStatus.Executing, + }; + case CoreToolCallStatus.Error: + return { + ...tc, + ...inProgressFields, + status: CoreToolCallStatus.Error, + response, + responseSubmittedToGemini, + }; + case CoreToolCallStatus.Success: + return { + ...tc, + ...inProgressFields, + status: CoreToolCallStatus.Success, + response, + responseSubmittedToGemini, + }; + case CoreToolCallStatus.Scheduled: + return { + ...tc, + ...inProgressFields, + status: CoreToolCallStatus.Scheduled, + }; + case CoreToolCallStatus.Validating: + return { + ...tc, + ...inProgressFields, + status: CoreToolCallStatus.Validating, + }; + case CoreToolCallStatus.AwaitingApproval: + return { + ...tc, + ...inProgressFields, + status: CoreToolCallStatus.AwaitingApproval, + }; + case CoreToolCallStatus.Cancelled: + return { + ...tc, + ...inProgressFields, + status: CoreToolCallStatus.Cancelled, + }; + default: + return tc; + } + }), ); break; - case 'tool_response': + } + case 'tool_response': { setTrackedTools((prev) => - prev.map((tc) => - tc.request.callId === event.requestId - ? ({ - ...tc, - status: event.isError ? 'error' : 'success', - response: { - resultDisplay: - event._meta?.['resultDisplay'] ?? - (event.displayContent?.[0]?.type === 'text' - ? event.displayContent[0].text - : undefined), - outputFile: event._meta?.['outputFile'] as string | undefined, - }, - responseSubmittedToGemini: true, - } as unknown as TrackedToolCall) - : tc, - ), + prev.map((tc): TrackedToolCall => { + if (tc.request.callId !== event.requestId) return tc; + + const legacyState = event._meta?.legacyState; + const outputFile = legacyState?.outputFile; + const resultDisplay = + event.displayContent?.[0]?.type === 'text' + ? event.displayContent[0].text + : undefined; + + const response = { + callId: tc.request.callId, + responseParts: [], + resultDisplay, + outputFile, + ...(event.isError + ? { error: 'Tool error', errorType: 'UNKNOWN' } + : {}), + }; + + if (event.isError) { + return { + ...tc, + status: CoreToolCallStatus.Error, + response, + responseSubmittedToGemini: true, + }; + } else { + return { + ...tc, + status: CoreToolCallStatus.Success, + response, + responseSubmittedToGemini: true, + }; + } + }), ); break; + } case 'error': addItem( { type: MessageType.ERROR, text: event.message }, @@ -294,7 +445,7 @@ export const useAgentStream = ( break; } }, - [addItem, flushPendingText, pendingHistoryItemRef, setPendingHistoryItem], + [addItem, flushPendingText, setPendingHistoryItem, setTrackedTools, config], ); useEffect(() => { @@ -316,7 +467,7 @@ export const useAgentStream = ( const submitQuery = useCallback( async ( - query: PartListUnion, + query: Array | string, options?: { isContinuation: boolean }, _prompt_id?: string, ) => { @@ -335,7 +486,7 @@ export const useAgentStream = ( } const parts = geminiPartsToContentParts( - typeof query === 'string' ? [{ text: query }] : (query as any[]), + typeof query === 'string' ? [{ text: query }] : query, ); try { @@ -399,13 +550,14 @@ export const useAgentStream = ( } const isLastInBatch = - toolsToPush[toolsToPush.length - 1] === trackedTools[trackedTools.length - 1]; + toolsToPush[toolsToPush.length - 1] === + trackedTools[trackedTools.length - 1]; const historyItem = mapTrackedToolCallsToDisplay(toolsToPush, { borderTop: isFirstToolInGroupRef.current, borderBottom: isLastInBatch, ...getToolGroupBorderAppearance( - { type: 'tool_group', tools: trackedTools as any[] }, + { type: 'tool_group', tools: trackedTools }, activePtyId, !!_isShellFocused, [], @@ -437,7 +589,7 @@ export const useAgentStream = ( const items: HistoryItemWithoutId[] = []; const appearance = getToolGroupBorderAppearance( - { type: 'tool_group', tools: trackedTools as any[] }, + { type: 'tool_group', tools: trackedTools }, activePtyId, !!_isShellFocused, [], @@ -488,17 +640,18 @@ export const useAgentStream = ( }, [ trackedTools, pushedToolCallIds, - isFirstToolInGroupRef, activePtyId, _isShellFocused, backgroundShells, ]); - const pendingHistoryItems = useMemo(() => { - return [pendingHistoryItem, ...pendingToolGroupItems].filter( - (i): i is HistoryItemWithoutId => i !== undefined && i !== null, - ); - }, [pendingHistoryItem, pendingToolGroupItems]); + const pendingHistoryItems = useMemo( + () => + [pendingHistoryItem, ...pendingToolGroupItems].filter( + (i): i is HistoryItemWithoutId => i !== undefined && i !== null, + ), + [pendingHistoryItem, pendingToolGroupItems], + ); return { streamingState, diff --git a/packages/cli/src/ui/hooks/useToolScheduler.ts b/packages/cli/src/ui/hooks/useToolScheduler.ts index 670a8b76d5..86d18127be 100644 --- a/packages/cli/src/ui/hooks/useToolScheduler.ts +++ b/packages/cli/src/ui/hooks/useToolScheduler.ts @@ -31,9 +31,10 @@ export type CancelAllFn = (signal: AbortSignal) => void; * The shape expected by useGeminiStream. * It matches the Core ToolCall structure + the UI metadata flag. */ -export type TrackedToolCall = ToolCall & { - responseSubmittedToGemini?: boolean; -}; +export type Tracked = T extends unknown + ? T & { responseSubmittedToGemini?: boolean } + : never; +export type TrackedToolCall = Tracked; // Narrowed types for specific statuses (used by useGeminiStream) export type TrackedScheduledToolCall = Extract< diff --git a/packages/core/src/agent/legacy-agent-session.ts b/packages/core/src/agent/legacy-agent-session.ts index 58063f200a..e246eaba9e 100644 --- a/packages/core/src/agent/legacy-agent-session.ts +++ b/packages/core/src/agent/legacy-agent-session.ts @@ -211,7 +211,9 @@ class LegacyAgentProtocol implements AgentProtocol { this._emit(toolUpdates); }; - this._config.getMessageBus().subscribe(MessageBusType.TOOL_CALLS_UPDATE, handleToolCallsUpdate); + this._config + .getMessageBus() + .subscribe(MessageBusType.TOOL_CALLS_UPDATE, handleToolCallsUpdate); try { while (true) { @@ -246,17 +248,23 @@ class LegacyAgentProtocol implements AgentProtocol { toolCallRequests.push(event.value); } - const translatedEvents = translateEvent(event, this._translationState); + const translatedEvents = translateEvent( + event, + this._translationState, + ); for (const ev of translatedEvents) { if (ev.type === 'tool_request') { const tool = this._config.getToolRegistry().getTool(ev.name); const invocation = tool?.build(ev.args); ev._meta = { - displayName: tool?.displayName ?? ev.name, - description: invocation?.getDescription() ?? tool?.description ?? '', - isOutputMarkdown: tool?.isOutputMarkdown ?? false, - kind: tool?.kind, + legacyState: { + displayName: tool?.displayName ?? ev.name, + description: + invocation?.getDescription() ?? tool?.description ?? '', + isOutputMarkdown: tool?.isOutputMarkdown ?? false, + kind: tool?.kind, + }, }; } } @@ -371,7 +379,9 @@ class LegacyAgentProtocol implements AgentProtocol { currentParts = toolResponseParts; } } finally { - this._config.getMessageBus().unsubscribe(MessageBusType.TOOL_CALLS_UPDATE, handleToolCallsUpdate); + this._config + .getMessageBus() + .unsubscribe(MessageBusType.TOOL_CALLS_UPDATE, handleToolCallsUpdate); } } diff --git a/packages/core/src/agent/types.ts b/packages/core/src/agent/types.ts index 512a8c9507..2437f2d3ab 100644 --- a/packages/core/src/agent/types.ts +++ b/packages/core/src/agent/types.ts @@ -182,6 +182,16 @@ export interface ToolRequest { name: string; /** The arguments for the tool. */ args: Record; + /** UI specific metadata */ + _meta?: { + legacyState?: { + displayName?: string; + isOutputMarkdown?: boolean; + description?: string; + kind?: string; + }; + [key: string]: unknown; + }; } /** @@ -194,6 +204,18 @@ export interface ToolUpdate { displayContent?: ContentPart[]; content?: ContentPart[]; data?: Record; + /** UI specific metadata */ + _meta?: { + legacyState?: { + status?: string; + progressMessage?: string; + progress?: number; + progressTotal?: number; + pid?: number; + description?: string; + }; + [key: string]: unknown; + }; } export interface ToolResponse { @@ -207,6 +229,13 @@ export interface ToolResponse { data?: Record; /** When true, the tool call encountered an error that will be sent to the model. */ isError?: boolean; + /** UI specific metadata */ + _meta?: { + legacyState?: { + outputFile?: string; + }; + [key: string]: unknown; + }; } export type ElicitationRequest = {