Reuse CoreToolScheduler for nonInteractiveToolExecutor (#6714)

This commit is contained in:
Tommaso Sciortino 2025-08-21 16:49:12 -07:00 committed by GitHub
parent 29699274bb
commit 15c62bade3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 93 additions and 258 deletions

View File

@ -13,7 +13,7 @@ import {
GeminiEventType, GeminiEventType,
parseAndFormatApiError, parseAndFormatApiError,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { Content, Part, FunctionCall } from '@google/genai'; import { Content, Part } from '@google/genai';
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js'; import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
import { handleAtCommand } from './ui/hooks/atCommandProcessor.js'; import { handleAtCommand } from './ui/hooks/atCommandProcessor.js';
@ -74,7 +74,7 @@ export async function runNonInteractive(
); );
return; return;
} }
const functionCalls: FunctionCall[] = []; const toolCallRequests: ToolCallRequestInfo[] = [];
const responseStream = geminiClient.sendMessageStream( const responseStream = geminiClient.sendMessageStream(
currentMessages[0]?.parts || [], currentMessages[0]?.parts || [],
@ -91,29 +91,13 @@ export async function runNonInteractive(
if (event.type === GeminiEventType.Content) { if (event.type === GeminiEventType.Content) {
process.stdout.write(event.value); process.stdout.write(event.value);
} else if (event.type === GeminiEventType.ToolCallRequest) { } else if (event.type === GeminiEventType.ToolCallRequest) {
const toolCallRequest = event.value; toolCallRequests.push(event.value);
const fc: FunctionCall = {
name: toolCallRequest.name,
args: toolCallRequest.args,
id: toolCallRequest.callId,
};
functionCalls.push(fc);
} }
} }
if (functionCalls.length > 0) { if (toolCallRequests.length > 0) {
const toolResponseParts: Part[] = []; const toolResponseParts: Part[] = [];
for (const requestInfo of toolCallRequests) {
for (const fc of functionCalls) {
const callId = fc.id ?? `${fc.name}-${Date.now()}`;
const requestInfo: ToolCallRequestInfo = {
callId,
name: fc.name as string,
args: (fc.args ?? {}) as Record<string, unknown>,
isClientInitiated: false,
prompt_id,
};
const toolResponse = await executeToolCall( const toolResponse = await executeToolCall(
config, config,
requestInfo, requestInfo,
@ -122,7 +106,7 @@ export async function runNonInteractive(
if (toolResponse.error) { if (toolResponse.error) {
console.error( console.error(
`Error executing tool ${fc.name}: ${toolResponse.resultDisplay || toolResponse.error.message}`, `Error executing tool ${requestInfo.name}: ${toolResponse.resultDisplay || toolResponse.error.message}`,
); );
} }

View File

@ -134,7 +134,6 @@ export function useReactToolScheduler(
const scheduler = useMemo( const scheduler = useMemo(
() => () =>
new CoreToolScheduler({ new CoreToolScheduler({
toolRegistry: config.getToolRegistry(),
outputUpdateHandler, outputUpdateHandler,
onAllToolCallsComplete: allToolCallsCompleteHandler, onAllToolCallsComplete: allToolCallsCompleteHandler,
onToolCallsUpdate: toolCallsUpdateHandler, onToolCallsUpdate: toolCallsUpdateHandler,

View File

@ -129,11 +129,11 @@ describe('CoreToolScheduler', () => {
model: 'test-model', model: 'test-model',
authType: 'oauth-personal', authType: 'oauth-personal',
}), }),
getToolRegistry: () => mockToolRegistry,
} as unknown as Config; } as unknown as Config;
const scheduler = new CoreToolScheduler({ const scheduler = new CoreToolScheduler({
config: mockConfig, config: mockConfig,
toolRegistry: mockToolRegistry,
onAllToolCallsComplete, onAllToolCallsComplete,
onToolCallsUpdate, onToolCallsUpdate,
getPreferredEditor: () => 'vscode', getPreferredEditor: () => 'vscode',
@ -189,11 +189,11 @@ describe('CoreToolScheduler with payload', () => {
model: 'test-model', model: 'test-model',
authType: 'oauth-personal', authType: 'oauth-personal',
}), }),
getToolRegistry: () => mockToolRegistry,
} as unknown as Config; } as unknown as Config;
const scheduler = new CoreToolScheduler({ const scheduler = new CoreToolScheduler({
config: mockConfig, config: mockConfig,
toolRegistry: mockToolRegistry,
onAllToolCallsComplete, onAllToolCallsComplete,
onToolCallsUpdate, onToolCallsUpdate,
getPreferredEditor: () => 'vscode', getPreferredEditor: () => 'vscode',
@ -462,15 +462,14 @@ class MockEditTool extends BaseDeclarativeTool<
describe('CoreToolScheduler edit cancellation', () => { describe('CoreToolScheduler edit cancellation', () => {
it('should preserve diff when an edit is cancelled', async () => { it('should preserve diff when an edit is cancelled', async () => {
const mockEditTool = new MockEditTool(); const mockEditTool = new MockEditTool();
const declarativeTool = mockEditTool;
const mockToolRegistry = { const mockToolRegistry = {
getTool: () => declarativeTool, getTool: () => mockEditTool,
getFunctionDeclarations: () => [], getFunctionDeclarations: () => [],
tools: new Map(), tools: new Map(),
discovery: {}, discovery: {},
registerTool: () => {}, registerTool: () => {},
getToolByName: () => declarativeTool, getToolByName: () => mockEditTool,
getToolByDisplayName: () => declarativeTool, getToolByDisplayName: () => mockEditTool,
getTools: () => [], getTools: () => [],
discoverTools: async () => {}, discoverTools: async () => {},
getAllTools: () => [], getAllTools: () => [],
@ -489,11 +488,11 @@ describe('CoreToolScheduler edit cancellation', () => {
model: 'test-model', model: 'test-model',
authType: 'oauth-personal', authType: 'oauth-personal',
}), }),
getToolRegistry: () => mockToolRegistry,
} as unknown as Config; } as unknown as Config;
const scheduler = new CoreToolScheduler({ const scheduler = new CoreToolScheduler({
config: mockConfig, config: mockConfig,
toolRegistry: mockToolRegistry,
onAllToolCallsComplete, onAllToolCallsComplete,
onToolCallsUpdate, onToolCallsUpdate,
getPreferredEditor: () => 'vscode', getPreferredEditor: () => 'vscode',
@ -581,11 +580,11 @@ describe('CoreToolScheduler YOLO mode', () => {
model: 'test-model', model: 'test-model',
authType: 'oauth-personal', authType: 'oauth-personal',
}), }),
getToolRegistry: () => mockToolRegistry,
} as unknown as Config; } as unknown as Config;
const scheduler = new CoreToolScheduler({ const scheduler = new CoreToolScheduler({
config: mockConfig, config: mockConfig,
toolRegistry: mockToolRegistry,
onAllToolCallsComplete, onAllToolCallsComplete,
onToolCallsUpdate, onToolCallsUpdate,
getPreferredEditor: () => 'vscode', getPreferredEditor: () => 'vscode',
@ -670,11 +669,11 @@ describe('CoreToolScheduler request queueing', () => {
model: 'test-model', model: 'test-model',
authType: 'oauth-personal', authType: 'oauth-personal',
}), }),
getToolRegistry: () => mockToolRegistry,
} as unknown as Config; } as unknown as Config;
const scheduler = new CoreToolScheduler({ const scheduler = new CoreToolScheduler({
config: mockConfig, config: mockConfig,
toolRegistry: mockToolRegistry,
onAllToolCallsComplete, onAllToolCallsComplete,
onToolCallsUpdate, onToolCallsUpdate,
getPreferredEditor: () => 'vscode', getPreferredEditor: () => 'vscode',
@ -783,11 +782,11 @@ describe('CoreToolScheduler request queueing', () => {
model: 'test-model', model: 'test-model',
authType: 'oauth-personal', authType: 'oauth-personal',
}), }),
getToolRegistry: () => mockToolRegistry,
} as unknown as Config; } as unknown as Config;
const scheduler = new CoreToolScheduler({ const scheduler = new CoreToolScheduler({
config: mockConfig, config: mockConfig,
toolRegistry: mockToolRegistry,
onAllToolCallsComplete, onAllToolCallsComplete,
onToolCallsUpdate, onToolCallsUpdate,
getPreferredEditor: () => 'vscode', getPreferredEditor: () => 'vscode',
@ -864,7 +863,9 @@ describe('CoreToolScheduler request queueing', () => {
getTools: () => [], getTools: () => [],
discoverTools: async () => {}, discoverTools: async () => {},
discovery: {}, discovery: {},
}; } as unknown as ToolRegistry;
mockConfig.getToolRegistry = () => toolRegistry;
const onAllToolCallsComplete = vi.fn(); const onAllToolCallsComplete = vi.fn();
const onToolCallsUpdate = vi.fn(); const onToolCallsUpdate = vi.fn();
@ -874,7 +875,6 @@ describe('CoreToolScheduler request queueing', () => {
const scheduler = new CoreToolScheduler({ const scheduler = new CoreToolScheduler({
config: mockConfig, config: mockConfig,
toolRegistry: toolRegistry as unknown as ToolRegistry,
onAllToolCallsComplete, onAllToolCallsComplete,
onToolCallsUpdate: (toolCalls) => { onToolCallsUpdate: (toolCalls) => {
onToolCallsUpdate(toolCalls); onToolCallsUpdate(toolCalls);

View File

@ -226,12 +226,11 @@ const createErrorResponse = (
}); });
interface CoreToolSchedulerOptions { interface CoreToolSchedulerOptions {
toolRegistry: ToolRegistry; config: Config;
outputUpdateHandler?: OutputUpdateHandler; outputUpdateHandler?: OutputUpdateHandler;
onAllToolCallsComplete?: AllToolCallsCompleteHandler; onAllToolCallsComplete?: AllToolCallsCompleteHandler;
onToolCallsUpdate?: ToolCallsUpdateHandler; onToolCallsUpdate?: ToolCallsUpdateHandler;
getPreferredEditor: () => EditorType | undefined; getPreferredEditor: () => EditorType | undefined;
config: Config;
onEditorClose: () => void; onEditorClose: () => void;
} }
@ -255,7 +254,7 @@ export class CoreToolScheduler {
constructor(options: CoreToolSchedulerOptions) { constructor(options: CoreToolSchedulerOptions) {
this.config = options.config; this.config = options.config;
this.toolRegistry = options.toolRegistry; this.toolRegistry = options.config.getToolRegistry();
this.outputUpdateHandler = options.outputUpdateHandler; this.outputUpdateHandler = options.outputUpdateHandler;
this.onAllToolCallsComplete = options.onAllToolCallsComplete; this.onAllToolCallsComplete = options.onAllToolCallsComplete;
this.onToolCallsUpdate = options.onToolCallsUpdate; this.onToolCallsUpdate = options.onToolCallsUpdate;

View File

@ -12,6 +12,7 @@ import {
ToolResult, ToolResult,
Config, Config,
ToolErrorType, ToolErrorType,
ApprovalMode,
} from '../index.js'; } from '../index.js';
import { Part } from '@google/genai'; import { Part } from '@google/genai';
import { MockTool } from '../test-utils/tools.js'; import { MockTool } from '../test-utils/tools.js';
@ -27,10 +28,11 @@ describe('executeToolCall', () => {
mockToolRegistry = { mockToolRegistry = {
getTool: vi.fn(), getTool: vi.fn(),
// Add other ToolRegistry methods if needed, or use a more complete mock
} as unknown as ToolRegistry; } as unknown as ToolRegistry;
mockConfig = { mockConfig = {
getToolRegistry: () => mockToolRegistry,
getApprovalMode: () => ApprovalMode.DEFAULT,
getSessionId: () => 'test-session-id', getSessionId: () => 'test-session-id',
getUsageStatisticsEnabled: () => true, getUsageStatisticsEnabled: () => true,
getDebugMode: () => false, getDebugMode: () => false,
@ -38,7 +40,6 @@ describe('executeToolCall', () => {
model: 'test-model', model: 'test-model',
authType: 'oauth-personal', authType: 'oauth-personal',
}), }),
getToolRegistry: () => mockToolRegistry,
} as unknown as Config; } as unknown as Config;
abortController = new AbortController(); abortController = new AbortController();
@ -57,7 +58,7 @@ describe('executeToolCall', () => {
returnDisplay: 'Success!', returnDisplay: 'Success!',
}; };
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool); vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
vi.spyOn(mockTool, 'validateBuildAndExecute').mockResolvedValue(toolResult); mockTool.executeFn.mockReturnValue(toolResult);
const response = await executeToolCall( const response = await executeToolCall(
mockConfig, mockConfig,
@ -66,18 +67,18 @@ describe('executeToolCall', () => {
); );
expect(mockToolRegistry.getTool).toHaveBeenCalledWith('testTool'); expect(mockToolRegistry.getTool).toHaveBeenCalledWith('testTool');
expect(mockTool.validateBuildAndExecute).toHaveBeenCalledWith( expect(mockTool.executeFn).toHaveBeenCalledWith(request.args);
request.args, expect(response).toStrictEqual({
abortController.signal, callId: 'call1',
); error: undefined,
expect(response.callId).toBe('call1'); errorType: undefined,
expect(response.error).toBeUndefined(); resultDisplay: 'Success!',
expect(response.resultDisplay).toBe('Success!'); responseParts: {
expect(response.responseParts).toEqual({ functionResponse: {
functionResponse: { name: 'testTool',
name: 'testTool', id: 'call1',
id: 'call1', response: { output: 'Tool executed successfully' },
response: { output: 'Tool executed successfully' }, },
}, },
}); });
}); });
@ -98,23 +99,19 @@ describe('executeToolCall', () => {
abortController.signal, abortController.signal,
); );
expect(response.callId).toBe('call2'); expect(response).toStrictEqual({
expect(response.error).toBeInstanceOf(Error); callId: 'call2',
expect(response.error?.message).toBe( error: new Error('Tool "nonexistentTool" not found in registry.'),
'Tool "nonexistentTool" not found in registry.', errorType: ToolErrorType.TOOL_NOT_REGISTERED,
); resultDisplay: 'Tool "nonexistentTool" not found in registry.',
expect(response.resultDisplay).toBe( responseParts: {
'Tool "nonexistentTool" not found in registry.',
);
expect(response.responseParts).toEqual([
{
functionResponse: { functionResponse: {
name: 'nonexistentTool', name: 'nonexistentTool',
id: 'call2', id: 'call2',
response: { error: 'Tool "nonexistentTool" not found in registry.' }, response: { error: 'Tool "nonexistentTool" not found in registry.' },
}, },
}, },
]); });
}); });
it('should return an error if tool validation fails', async () => { it('should return an error if tool validation fails', async () => {
@ -125,24 +122,17 @@ describe('executeToolCall', () => {
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-3', prompt_id: 'prompt-id-3',
}; };
const validationErrorResult: ToolResult = {
llmContent: 'Error: Invalid parameters',
returnDisplay: 'Invalid parameters',
error: {
message: 'Invalid parameters',
type: ToolErrorType.INVALID_TOOL_PARAMS,
},
};
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool); vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
vi.spyOn(mockTool, 'validateBuildAndExecute').mockResolvedValue( vi.spyOn(mockTool, 'build').mockImplementation(() => {
validationErrorResult, throw new Error('Invalid parameters');
); });
const response = await executeToolCall( const response = await executeToolCall(
mockConfig, mockConfig,
request, request,
abortController.signal, abortController.signal,
); );
expect(response).toStrictEqual({ expect(response).toStrictEqual({
callId: 'call3', callId: 'call3',
error: new Error('Invalid parameters'), error: new Error('Invalid parameters'),
@ -152,7 +142,7 @@ describe('executeToolCall', () => {
id: 'call3', id: 'call3',
name: 'testTool', name: 'testTool',
response: { response: {
output: 'Error: Invalid parameters', error: 'Invalid parameters',
}, },
}, },
}, },
@ -177,9 +167,7 @@ describe('executeToolCall', () => {
}, },
}; };
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool); vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
vi.spyOn(mockTool, 'validateBuildAndExecute').mockResolvedValue( mockTool.executeFn.mockReturnValue(executionErrorResult);
executionErrorResult,
);
const response = await executeToolCall( const response = await executeToolCall(
mockConfig, mockConfig,
@ -195,7 +183,7 @@ describe('executeToolCall', () => {
id: 'call4', id: 'call4',
name: 'testTool', name: 'testTool',
response: { response: {
output: 'Error: Execution failed', error: 'Execution failed',
}, },
}, },
}, },
@ -211,11 +199,10 @@ describe('executeToolCall', () => {
isClientInitiated: false, isClientInitiated: false,
prompt_id: 'prompt-id-5', prompt_id: 'prompt-id-5',
}; };
const executionError = new Error('Something went very wrong');
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool); vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
vi.spyOn(mockTool, 'validateBuildAndExecute').mockRejectedValue( mockTool.executeFn.mockImplementation(() => {
executionError, throw new Error('Something went very wrong');
); });
const response = await executeToolCall( const response = await executeToolCall(
mockConfig, mockConfig,
@ -223,19 +210,19 @@ describe('executeToolCall', () => {
abortController.signal, abortController.signal,
); );
expect(response.callId).toBe('call5'); expect(response).toStrictEqual({
expect(response.error).toBe(executionError); callId: 'call5',
expect(response.errorType).toBe(ToolErrorType.UNHANDLED_EXCEPTION); error: new Error('Something went very wrong'),
expect(response.resultDisplay).toBe('Something went very wrong'); errorType: ToolErrorType.UNHANDLED_EXCEPTION,
expect(response.responseParts).toEqual([ resultDisplay: 'Something went very wrong',
{ responseParts: {
functionResponse: { functionResponse: {
name: 'testTool', name: 'testTool',
id: 'call5', id: 'call5',
response: { error: 'Something went very wrong' }, response: { error: 'Something went very wrong' },
}, },
}, },
]); });
}); });
it('should correctly format llmContent with inlineData', async () => { it('should correctly format llmContent with inlineData', async () => {
@ -254,7 +241,7 @@ describe('executeToolCall', () => {
returnDisplay: 'Image processed', returnDisplay: 'Image processed',
}; };
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool); vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
vi.spyOn(mockTool, 'validateBuildAndExecute').mockResolvedValue(toolResult); mockTool.executeFn.mockReturnValue(toolResult);
const response = await executeToolCall( const response = await executeToolCall(
mockConfig, mockConfig,
@ -262,18 +249,23 @@ describe('executeToolCall', () => {
abortController.signal, abortController.signal,
); );
expect(response.resultDisplay).toBe('Image processed'); expect(response).toStrictEqual({
expect(response.responseParts).toEqual([ callId: 'call6',
{ error: undefined,
functionResponse: { errorType: undefined,
name: 'testTool', resultDisplay: 'Image processed',
id: 'call6', responseParts: [
response: { {
output: 'Binary content of type image/png was processed.', functionResponse: {
name: 'testTool',
id: 'call6',
response: {
output: 'Binary content of type image/png was processed.',
},
}, },
}, },
}, imageDataPart,
imageDataPart, ],
]); });
}); });
}); });

View File

@ -4,166 +4,27 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { import { ToolCallRequestInfo, ToolCallResponseInfo, Config } from '../index.js';
FileDiff, import { CoreToolScheduler } from './coreToolScheduler.js';
logToolCall,
ToolCallRequestInfo,
ToolCallResponseInfo,
ToolErrorType,
ToolResult,
} from '../index.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
import { Config } from '../config/config.js';
import { convertToFunctionResponse } from './coreToolScheduler.js';
import { ToolCallDecision } from '../telemetry/tool-call-decision.js';
/** /**
* Executes a single tool call non-interactively. * Executes a single tool call non-interactively by leveraging the CoreToolScheduler.
* It does not handle confirmations, multiple calls, or live updates.
*/ */
export async function executeToolCall( export async function executeToolCall(
config: Config, config: Config,
toolCallRequest: ToolCallRequestInfo, toolCallRequest: ToolCallRequestInfo,
abortSignal?: AbortSignal, abortSignal: AbortSignal,
): Promise<ToolCallResponseInfo> { ): Promise<ToolCallResponseInfo> {
const tool = config.getToolRegistry().getTool(toolCallRequest.name); return new Promise<ToolCallResponseInfo>((resolve, reject) => {
new CoreToolScheduler({
const startTime = Date.now(); config,
if (!tool) { getPreferredEditor: () => undefined,
const error = new Error( onEditorClose: () => {},
`Tool "${toolCallRequest.name}" not found in registry.`, onAllToolCallsComplete: async (completedToolCalls) => {
); resolve(completedToolCalls[0].response);
const durationMs = Date.now() - startTime; },
logToolCall(config, { })
'event.name': 'tool_call', .schedule(toolCallRequest, abortSignal)
'event.timestamp': new Date().toISOString(), .catch(reject);
function_name: toolCallRequest.name, });
function_args: toolCallRequest.args,
duration_ms: durationMs,
success: false,
error: error.message,
prompt_id: toolCallRequest.prompt_id,
tool_type: 'native',
});
// Ensure the response structure matches what the API expects for an error
return {
callId: toolCallRequest.callId,
responseParts: [
{
functionResponse: {
id: toolCallRequest.callId,
name: toolCallRequest.name,
response: { error: error.message },
},
},
],
resultDisplay: error.message,
error,
errorType: ToolErrorType.TOOL_NOT_REGISTERED,
};
}
try {
// Directly execute without confirmation or live output handling
const effectiveAbortSignal = abortSignal ?? new AbortController().signal;
const toolResult: ToolResult = await tool.validateBuildAndExecute(
toolCallRequest.args,
effectiveAbortSignal,
// No live output callback for non-interactive mode
);
const tool_output = toolResult.llmContent;
const tool_display = toolResult.returnDisplay;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let metadata: { [key: string]: any } = {};
if (
toolResult.error === undefined &&
typeof tool_display === 'object' &&
tool_display !== null &&
'diffStat' in tool_display
) {
const diffStat = (tool_display as FileDiff).diffStat;
if (diffStat) {
metadata = {
ai_added_lines: diffStat.ai_added_lines,
ai_removed_lines: diffStat.ai_removed_lines,
user_added_lines: diffStat.user_added_lines,
user_removed_lines: diffStat.user_removed_lines,
};
}
}
const durationMs = Date.now() - startTime;
logToolCall(config, {
'event.name': 'tool_call',
'event.timestamp': new Date().toISOString(),
function_name: toolCallRequest.name,
function_args: toolCallRequest.args,
duration_ms: durationMs,
success: toolResult.error === undefined,
error:
toolResult.error === undefined ? undefined : toolResult.error.message,
error_type:
toolResult.error === undefined ? undefined : toolResult.error.type,
prompt_id: toolCallRequest.prompt_id,
metadata,
decision: ToolCallDecision.AUTO_ACCEPT,
tool_type:
typeof tool !== 'undefined' && tool instanceof DiscoveredMCPTool
? 'mcp'
: 'native',
});
const response = convertToFunctionResponse(
toolCallRequest.name,
toolCallRequest.callId,
tool_output,
);
return {
callId: toolCallRequest.callId,
responseParts: response,
resultDisplay: tool_display,
error:
toolResult.error === undefined
? undefined
: new Error(toolResult.error.message),
errorType:
toolResult.error === undefined ? undefined : toolResult.error.type,
};
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
const durationMs = Date.now() - startTime;
logToolCall(config, {
'event.name': 'tool_call',
'event.timestamp': new Date().toISOString(),
function_name: toolCallRequest.name,
function_args: toolCallRequest.args,
duration_ms: durationMs,
success: false,
error: error.message,
error_type: ToolErrorType.UNHANDLED_EXCEPTION,
prompt_id: toolCallRequest.prompt_id,
tool_type:
typeof tool !== 'undefined' && tool instanceof DiscoveredMCPTool
? 'mcp'
: 'native',
});
return {
callId: toolCallRequest.callId,
responseParts: [
{
functionResponse: {
id: toolCallRequest.callId,
name: toolCallRequest.name,
response: { error: error.message },
},
},
],
resultDisplay: error.message,
error,
errorType: ToolErrorType.UNHANDLED_EXCEPTION,
};
}
} }