Reuse CoreToolScheduler for nonInteractiveToolExecutor (#6714)
This commit is contained in:
parent
29699274bb
commit
15c62bade3
|
@ -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}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
],
|
||||||
]);
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue