summaryrefslogtreecommitdiff
path: root/packages/core/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/core/src')
-rw-r--r--packages/core/src/core/nonInteractiveToolExecutor.test.ts235
-rw-r--r--packages/core/src/core/nonInteractiveToolExecutor.ts91
-rw-r--r--packages/core/src/index.ts4
3 files changed, 330 insertions, 0 deletions
diff --git a/packages/core/src/core/nonInteractiveToolExecutor.test.ts b/packages/core/src/core/nonInteractiveToolExecutor.test.ts
new file mode 100644
index 00000000..3d7dc1a2
--- /dev/null
+++ b/packages/core/src/core/nonInteractiveToolExecutor.test.ts
@@ -0,0 +1,235 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { executeToolCall } from './nonInteractiveToolExecutor.js';
+import {
+ ToolRegistry,
+ ToolCallRequestInfo,
+ ToolResult,
+ Tool,
+ ToolCallConfirmationDetails,
+} from '../index.js';
+import { Part, Type } from '@google/genai';
+
+describe('executeToolCall', () => {
+ let mockToolRegistry: ToolRegistry;
+ let mockTool: Tool;
+ let abortController: AbortController;
+
+ beforeEach(() => {
+ mockTool = {
+ name: 'testTool',
+ displayName: 'Test Tool',
+ description: 'A tool for testing',
+ schema: {
+ name: 'testTool',
+ description: 'A tool for testing',
+ parameters: {
+ type: Type.OBJECT,
+ properties: {
+ param1: { type: Type.STRING },
+ },
+ required: ['param1'],
+ },
+ },
+ execute: vi.fn(),
+ validateToolParams: vi.fn(() => null),
+ shouldConfirmExecute: vi.fn(() =>
+ Promise.resolve(false as false | ToolCallConfirmationDetails),
+ ),
+ isOutputMarkdown: false,
+ canUpdateOutput: false,
+ getDescription: vi.fn(),
+ };
+
+ mockToolRegistry = {
+ getTool: vi.fn(),
+ // Add other ToolRegistry methods if needed, or use a more complete mock
+ } as unknown as ToolRegistry;
+
+ abortController = new AbortController();
+ });
+
+ it('should execute a tool successfully', async () => {
+ const request: ToolCallRequestInfo = {
+ callId: 'call1',
+ name: 'testTool',
+ args: { param1: 'value1' },
+ };
+ const toolResult: ToolResult = {
+ llmContent: 'Tool executed successfully',
+ returnDisplay: 'Success!',
+ };
+ vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
+ vi.mocked(mockTool.execute).mockResolvedValue(toolResult);
+
+ const response = await executeToolCall(
+ request,
+ mockToolRegistry,
+ abortController.signal,
+ );
+
+ expect(mockToolRegistry.getTool).toHaveBeenCalledWith('testTool');
+ expect(mockTool.execute).toHaveBeenCalledWith(
+ request.args,
+ abortController.signal,
+ );
+ expect(response.callId).toBe('call1');
+ expect(response.error).toBeUndefined();
+ expect(response.resultDisplay).toBe('Success!');
+ expect(response.responseParts).toEqual([
+ {
+ functionResponse: {
+ name: 'testTool',
+ id: 'call1',
+ response: { output: 'Tool executed successfully' },
+ },
+ },
+ ]);
+ });
+
+ it('should return an error if tool is not found', async () => {
+ const request: ToolCallRequestInfo = {
+ callId: 'call2',
+ name: 'nonExistentTool',
+ args: {},
+ };
+ vi.mocked(mockToolRegistry.getTool).mockReturnValue(undefined);
+
+ const response = await executeToolCall(
+ request,
+ mockToolRegistry,
+ abortController.signal,
+ );
+
+ expect(response.callId).toBe('call2');
+ expect(response.error).toBeInstanceOf(Error);
+ expect(response.error?.message).toBe(
+ 'Tool "nonExistentTool" not found in registry.',
+ );
+ expect(response.resultDisplay).toBe(
+ 'Tool "nonExistentTool" not found in registry.',
+ );
+ expect(response.responseParts).toEqual([
+ {
+ functionResponse: {
+ name: 'nonExistentTool',
+ id: 'call2',
+ response: { error: 'Tool "nonExistentTool" not found in registry.' },
+ },
+ },
+ ]);
+ });
+
+ it('should return an error if tool execution fails', async () => {
+ const request: ToolCallRequestInfo = {
+ callId: 'call3',
+ name: 'testTool',
+ args: { param1: 'value1' },
+ };
+ const executionError = new Error('Tool execution failed');
+ vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
+ vi.mocked(mockTool.execute).mockRejectedValue(executionError);
+
+ const response = await executeToolCall(
+ request,
+ mockToolRegistry,
+ abortController.signal,
+ );
+
+ expect(response.callId).toBe('call3');
+ expect(response.error).toBe(executionError);
+ expect(response.resultDisplay).toBe('Tool execution failed');
+ expect(response.responseParts).toEqual([
+ {
+ functionResponse: {
+ name: 'testTool',
+ id: 'call3',
+ response: { error: 'Tool execution failed' },
+ },
+ },
+ ]);
+ });
+
+ it('should handle cancellation during tool execution', async () => {
+ const request: ToolCallRequestInfo = {
+ callId: 'call4',
+ name: 'testTool',
+ args: { param1: 'value1' },
+ };
+ const cancellationError = new Error('Operation cancelled');
+ vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
+
+ vi.mocked(mockTool.execute).mockImplementation(async (_args, signal) => {
+ if (signal?.aborted) {
+ return Promise.reject(cancellationError);
+ }
+ return new Promise((_resolve, reject) => {
+ signal?.addEventListener('abort', () => {
+ reject(cancellationError);
+ });
+ // Simulate work that might happen if not aborted immediately
+ const timeoutId = setTimeout(
+ () =>
+ reject(
+ new Error('Should have been cancelled if not aborted prior'),
+ ),
+ 100,
+ );
+ signal?.addEventListener('abort', () => clearTimeout(timeoutId));
+ });
+ });
+
+ abortController.abort(); // Abort before calling
+ const response = await executeToolCall(
+ request,
+ mockToolRegistry,
+ abortController.signal,
+ );
+
+ expect(response.callId).toBe('call4');
+ expect(response.error?.message).toBe(cancellationError.message);
+ expect(response.resultDisplay).toBe('Operation cancelled');
+ });
+
+ it('should correctly format llmContent with inlineData', async () => {
+ const request: ToolCallRequestInfo = {
+ callId: 'call5',
+ name: 'testTool',
+ args: {},
+ };
+ const imageDataPart: Part = {
+ inlineData: { mimeType: 'image/png', data: 'base64data' },
+ };
+ const toolResult: ToolResult = {
+ llmContent: [imageDataPart],
+ returnDisplay: 'Image processed',
+ };
+ vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
+ vi.mocked(mockTool.execute).mockResolvedValue(toolResult);
+
+ const response = await executeToolCall(
+ request,
+ mockToolRegistry,
+ abortController.signal,
+ );
+
+ expect(response.resultDisplay).toBe('Image processed');
+ expect(response.responseParts).toEqual([
+ {
+ functionResponse: {
+ name: 'testTool',
+ id: 'call5',
+ response: {
+ status: 'Binary content of type image/png was processed.',
+ },
+ },
+ },
+ imageDataPart,
+ ]);
+ });
+});
diff --git a/packages/core/src/core/nonInteractiveToolExecutor.ts b/packages/core/src/core/nonInteractiveToolExecutor.ts
new file mode 100644
index 00000000..5b5c9a13
--- /dev/null
+++ b/packages/core/src/core/nonInteractiveToolExecutor.ts
@@ -0,0 +1,91 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Part } from '@google/genai';
+import {
+ ToolCallRequestInfo,
+ ToolCallResponseInfo,
+ ToolRegistry,
+ ToolResult,
+} from '../index.js';
+import { formatLlmContentForFunctionResponse } from './coreToolScheduler.js';
+
+/**
+ * Executes a single tool call non-interactively.
+ * It does not handle confirmations, multiple calls, or live updates.
+ */
+export async function executeToolCall(
+ toolCallRequest: ToolCallRequestInfo,
+ toolRegistry: ToolRegistry,
+ abortSignal?: AbortSignal,
+): Promise<ToolCallResponseInfo> {
+ const tool = toolRegistry.getTool(toolCallRequest.name);
+
+ if (!tool) {
+ const error = new Error(
+ `Tool "${toolCallRequest.name}" not found in registry.`,
+ );
+ // 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,
+ };
+ }
+
+ try {
+ // Directly execute without confirmation or live output handling
+ const effectiveAbortSignal = abortSignal ?? new AbortController().signal;
+ const toolResult: ToolResult = await tool.execute(
+ toolCallRequest.args,
+ effectiveAbortSignal,
+ // No live output callback for non-interactive mode
+ );
+
+ const { functionResponseJson, additionalParts } =
+ formatLlmContentForFunctionResponse(toolResult.llmContent);
+
+ const functionResponsePart: Part = {
+ functionResponse: {
+ name: toolCallRequest.name,
+ id: toolCallRequest.callId,
+ response: functionResponseJson,
+ },
+ };
+
+ return {
+ callId: toolCallRequest.callId,
+ responseParts: [functionResponsePart, ...additionalParts],
+ resultDisplay: toolResult.returnDisplay,
+ error: undefined,
+ };
+ } catch (e) {
+ const error = e instanceof Error ? e : new Error(String(e));
+ return {
+ callId: toolCallRequest.callId,
+ responseParts: [
+ {
+ functionResponse: {
+ id: toolCallRequest.callId,
+ name: toolCallRequest.name,
+ response: { error: error.message },
+ },
+ },
+ ],
+ resultDisplay: error.message,
+ error,
+ };
+ }
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index f8c42336..bd28c864 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -14,6 +14,7 @@ export * from './core/prompts.js';
export * from './core/turn.js';
export * from './core/geminiRequest.js';
export * from './core/coreToolScheduler.js';
+export * from './core/nonInteractiveToolExecutor.js';
// Export utilities
export * from './utils/paths.js';
@@ -35,3 +36,6 @@ export * from './tools/edit.js';
export * from './tools/write-file.js';
export * from './tools/web-fetch.js';
export * from './tools/memoryTool.js';
+export * from './tools/shell.js';
+export * from './tools/web-search.js';
+export * from './tools/read-many-files.js';