diff --git a/packages/cli/src/ui/hooks/useAgentStream.ts b/packages/cli/src/ui/hooks/useAgentStream.ts index 982391a437..6d46d2f8e6 100644 --- a/packages/cli/src/ui/hooks/useAgentStream.ts +++ b/packages/cli/src/ui/hooks/useAgentStream.ts @@ -224,14 +224,17 @@ export const useAgentStream = ({ if (tc.callId !== event.requestId) return tc; const legacyState = event._meta?.legacyState; - const evtStatus = legacyState?.status; - let status = tc.status; - if (evtStatus === 'executing') + if (event.status === 'executing') status = CoreToolCallStatus.Executing; - else if (evtStatus === 'error') status = CoreToolCallStatus.Error; - else if (evtStatus === 'success') + else if (event.status === 'pending_input') + status = CoreToolCallStatus.AwaitingApproval; + else if (event.status === 'errored') + status = CoreToolCallStatus.Error; + else if (event.status === 'succeeded') status = CoreToolCallStatus.Success; + else if (event.status === 'aborted') + status = CoreToolCallStatus.Cancelled; const display = event.display?.result; const liveOutput = @@ -272,11 +275,16 @@ export const useAgentStream = ({ const resultDisplay = displayContentToString(display) ?? tc.resultDisplay; + let status = CoreToolCallStatus.Success; + if (event.status === 'errored') status = CoreToolCallStatus.Error; + else if (event.status === 'aborted') + status = CoreToolCallStatus.Cancelled; + else if (event.status === 'succeeded') + status = CoreToolCallStatus.Success; + return { ...tc, - status: event.isError - ? CoreToolCallStatus.Error - : CoreToolCallStatus.Success, + status, display: event.display ? { ...tc.display, ...event.display } : tc.display, diff --git a/packages/core/src/agent/event-translator.ts b/packages/core/src/agent/event-translator.ts index f60822a8e6..f91b3e6ddd 100644 --- a/packages/core/src/agent/event-translator.ts +++ b/packages/core/src/agent/event-translator.ts @@ -235,6 +235,7 @@ export function translateEvent( makeEvent('tool_request', state, { requestId: event.value.callId, name: event.value.name, + status: 'pending', args: event.value.args, display: event.value.display, }), @@ -257,6 +258,7 @@ export function translateEvent( makeEvent('tool_response', state, { requestId: event.value.callId, name: state.pendingToolNames.get(event.value.callId) ?? 'unknown', + status: event.value.error ? 'errored' : 'succeeded', content: event.value.error ? [{ type: 'text', text: event.value.error.message }] : geminiPartsToContentParts(event.value.responseParts), diff --git a/packages/core/src/agent/legacy-agent-session.ts b/packages/core/src/agent/legacy-agent-session.ts index d65c583b0b..11007f444c 100644 --- a/packages/core/src/agent/legacy-agent-session.ts +++ b/packages/core/src/agent/legacy-agent-session.ts @@ -39,7 +39,12 @@ import type { ContentPart, StreamEndReason, Unsubscribe, + ToolEventStatus, } from './types.js'; +import { + MessageBusType, + type ToolCallsUpdateMessage, +} from '../confirmation-bus/types.js'; function isAbortLikeError(err: unknown): boolean { return err instanceof Error && err.name === 'AbortError'; @@ -64,6 +69,7 @@ export class LegacyAgentProtocol implements AgentProtocol { private _activeStreamId?: string; private _abortController = new AbortController(); private _nextStreamIdOverride?: string; + private _lastToolStatuses = new Map(); private readonly _client: GeminiClient; private readonly _scheduler: Scheduler; @@ -92,6 +98,11 @@ export class LegacyAgentProtocol implements AgentProtocol { } this._scheduler = scheduler; } + + this._config.messageBus.subscribe( + MessageBusType.TOOL_CALLS_UPDATE, + this._handleToolCallsUpdate.bind(this), + ); } get events(): readonly AgentEvent[] { @@ -274,6 +285,11 @@ export class LegacyAgentProtocol implements AgentProtocol { this._makeToolResponseEvent({ requestId: request.callId, name: request.name, + status: response.error + ? 'errored' + : tc.status === 'cancelled' + ? 'aborted' + : 'succeeded', content, isError: response.error !== undefined, ...(display ? { display } : {}), @@ -487,6 +503,71 @@ export class LegacyAgentProtocol implements AgentProtocol { } satisfies AgentEvent<'error'>; return event; } + + private _handleToolCallsUpdate(msg: ToolCallsUpdateMessage): void { + if (!this._activeStreamId) { + return; + } + + const eventsToEmit: AgentEvent[] = []; + + for (const tc of msg.toolCalls) { + const callId = tc.request.callId; + let status: ToolEventStatus = 'pending'; + if (tc.status === 'validating' || tc.status === 'scheduled') { + status = 'pending'; + } else if (tc.status === 'awaiting_approval') { + status = 'pending_input'; + } else if (tc.status === 'executing') { + status = 'executing'; + } else if (tc.status === 'success') { + status = 'succeeded'; + } else if (tc.status === 'error') { + status = 'errored'; + } else if (tc.status === 'cancelled') { + status = 'aborted'; + } + + const lastStatus = this._lastToolStatuses.get(callId); + + if (lastStatus !== status) { + this._lastToolStatuses.set(callId, status); + + const display = populateToolDisplay({ + name: tc.request.name, + invocation: 'invocation' in tc ? tc.invocation : undefined, + displayName: 'tool' in tc ? tc.tool?.displayName : undefined, + display: 'response' in tc ? tc.response?.display : undefined, + }); + + eventsToEmit.push( + this._makeToolUpdateEvent({ + requestId: callId, + status, + ...(display ? { display } : {}), + }), + ); + } + } + + if (eventsToEmit.length > 0) { + this._emit(eventsToEmit); + } + } + + private _makeToolUpdateEvent( + payload: Omit< + AgentEvent<'tool_update'>, + 'id' | 'timestamp' | 'streamId' | 'type' + >, + ): AgentEvent<'tool_update'> { + const event = { + ...this._nextEventFields(), + type: 'tool_update', + ...payload, + } satisfies AgentEvent<'tool_update'>; + return event; + } } export class LegacyAgentSession extends AgentSession { diff --git a/packages/core/src/agent/types.ts b/packages/core/src/agent/types.ts index 0d41c46602..1f6990703b 100644 --- a/packages/core/src/agent/types.ts +++ b/packages/core/src/agent/types.ts @@ -227,11 +227,21 @@ export interface ToolDisplay { format?: ToolDisplayFormat; } +export type ToolEventStatus = + | 'pending' + | 'pending_input' + | 'executing' + | 'succeeded' + | 'errored' + | 'aborted'; + export interface ToolRequest { /** A unique identifier for this tool request to be correlated by the response. */ requestId: string; /** The name of the tool being requested. */ name: string; + /** The status of the tool execution. */ + status: ToolEventStatus; /** The arguments for the tool. */ /** Tool-controlled display information. */ display?: ToolDisplay; @@ -255,6 +265,8 @@ export interface ToolRequest { */ export interface ToolUpdate { requestId: string; + /** The status of the tool execution. */ + status: ToolEventStatus; /** Tool-controlled display information. */ display?: ToolDisplay; content?: ContentPart[]; @@ -276,6 +288,8 @@ export interface ToolUpdate { export interface ToolResponse { requestId: string; name: string; + /** The status of the tool execution. */ + status: ToolEventStatus; /** Tool-controlled display information. */ display?: ToolDisplay; /** Multi-part content to be sent to the model. */ diff --git a/scripts/build_package.js b/scripts/build_package.js index 279e46fa94..5523748a39 100644 --- a/scripts/build_package.js +++ b/scripts/build_package.js @@ -18,7 +18,7 @@ // limitations under the License. import { execSync } from 'node:child_process'; -import { writeFileSync, existsSync, cpSync } from 'node:fs'; +import { writeFileSync, existsSync, cpSync, rmSync } from 'node:fs'; import { join, basename } from 'node:path'; if (!process.cwd().includes('packages')) { @@ -48,7 +48,14 @@ if (packageName === 'core') { const docsSource = join(process.cwd(), '..', '..', 'docs'); const docsTarget = join(process.cwd(), 'dist', 'docs'); if (existsSync(docsSource)) { - cpSync(docsSource, docsTarget, { recursive: true, dereference: true }); + if (existsSync(docsTarget)) { + rmSync(docsTarget, { recursive: true, force: true }); + } + cpSync(docsSource, docsTarget, { + recursive: true, + dereference: true, + force: true, + }); console.log('Copied documentation to dist/docs'); } }