summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/cli/src/ui/hooks/useToolScheduler.test.ts178
-rw-r--r--packages/core/src/core/coreToolScheduler.test.ts176
-rw-r--r--packages/core/src/core/coreToolScheduler.ts110
-rw-r--r--packages/core/src/core/nonInteractiveToolExecutor.test.ts16
-rw-r--r--packages/core/src/core/nonInteractiveToolExecutor.ts20
-rw-r--r--packages/core/src/tools/mcp-tool.test.ts7
-rw-r--r--packages/core/src/tools/mcp-tool.ts9
-rw-r--r--packages/core/src/utils/generateContentResponseUtilities.ts6
8 files changed, 300 insertions, 222 deletions
diff --git a/packages/cli/src/ui/hooks/useToolScheduler.test.ts b/packages/cli/src/ui/hooks/useToolScheduler.test.ts
index 30880ba6..e49039f8 100644
--- a/packages/cli/src/ui/hooks/useToolScheduler.test.ts
+++ b/packages/cli/src/ui/hooks/useToolScheduler.test.ts
@@ -11,12 +11,7 @@ import {
useReactToolScheduler,
mapToDisplay,
} from './useReactToolScheduler.js';
-import {
- Part,
- PartListUnion,
- PartUnion,
- FunctionResponse,
-} from '@google/genai';
+import { PartUnion, FunctionResponse } from '@google/genai';
import {
Config,
ToolCallRequestInfo,
@@ -26,7 +21,6 @@ import {
ToolCallConfirmationDetails,
ToolConfirmationOutcome,
ToolCallResponseInfo,
- formatLlmContentForFunctionResponse, // Import from core
ToolCall, // Import from core
Status as ToolCallStatusType,
ApprovalMode, // Import from core
@@ -93,120 +87,6 @@ const mockToolRequiresConfirmation: Tool = {
),
};
-describe('formatLlmContentForFunctionResponse', () => {
- it('should handle simple string llmContent', () => {
- const llmContent = 'Simple text output';
- const { functionResponseJson, additionalParts } =
- formatLlmContentForFunctionResponse(llmContent);
- expect(functionResponseJson).toEqual({ output: 'Simple text output' });
- expect(additionalParts).toEqual([]);
- });
-
- it('should handle llmContent as a single Part with text', () => {
- const llmContent: Part = { text: 'Text from Part object' };
- const { functionResponseJson, additionalParts } =
- formatLlmContentForFunctionResponse(llmContent);
- expect(functionResponseJson).toEqual({ output: 'Text from Part object' });
- expect(additionalParts).toEqual([]);
- });
-
- it('should handle llmContent as a PartListUnion array with a single text Part', () => {
- const llmContent: PartListUnion = [{ text: 'Text from array' }];
- const { functionResponseJson, additionalParts } =
- formatLlmContentForFunctionResponse(llmContent);
- expect(functionResponseJson).toEqual({ output: 'Text from array' });
- expect(additionalParts).toEqual([]);
- });
-
- it('should handle llmContent with inlineData', () => {
- const llmContent: Part = {
- inlineData: { mimeType: 'image/png', data: 'base64...' },
- };
- const { functionResponseJson, additionalParts } =
- formatLlmContentForFunctionResponse(llmContent);
- expect(functionResponseJson).toEqual({
- status: 'Binary content of type image/png was processed.',
- });
- expect(additionalParts).toEqual([llmContent]);
- });
-
- it('should handle llmContent with fileData', () => {
- const llmContent: Part = {
- fileData: { mimeType: 'application/pdf', fileUri: 'gs://...' },
- };
- const { functionResponseJson, additionalParts } =
- formatLlmContentForFunctionResponse(llmContent);
- expect(functionResponseJson).toEqual({
- status: 'Binary content of type application/pdf was processed.',
- });
- expect(additionalParts).toEqual([llmContent]);
- });
-
- it('should handle llmContent as an array of multiple Parts (text and inlineData)', () => {
- const llmContent: PartListUnion = [
- { text: 'Some textual description' },
- { inlineData: { mimeType: 'image/jpeg', data: 'base64data...' } },
- { text: 'Another text part' },
- ];
- const { functionResponseJson, additionalParts } =
- formatLlmContentForFunctionResponse(llmContent);
- expect(functionResponseJson).toEqual({
- status: 'Tool execution succeeded.',
- });
- expect(additionalParts).toEqual(llmContent);
- });
-
- it('should handle llmContent as an array with a single inlineData Part', () => {
- const llmContent: PartListUnion = [
- { inlineData: { mimeType: 'image/gif', data: 'gifdata...' } },
- ];
- const { functionResponseJson, additionalParts } =
- formatLlmContentForFunctionResponse(llmContent);
- expect(functionResponseJson).toEqual({
- status: 'Binary content of type image/gif was processed.',
- });
- expect(additionalParts).toEqual(llmContent);
- });
-
- it('should handle llmContent as a generic Part (not text, inlineData, or fileData)', () => {
- const llmContent: Part = { functionCall: { name: 'test', args: {} } };
- const { functionResponseJson, additionalParts } =
- formatLlmContentForFunctionResponse(llmContent);
- expect(functionResponseJson).toEqual({
- status: 'Tool execution succeeded.',
- });
- expect(additionalParts).toEqual([llmContent]);
- });
-
- it('should handle empty string llmContent', () => {
- const llmContent = '';
- const { functionResponseJson, additionalParts } =
- formatLlmContentForFunctionResponse(llmContent);
- expect(functionResponseJson).toEqual({ output: '' });
- expect(additionalParts).toEqual([]);
- });
-
- it('should handle llmContent as an empty array', () => {
- const llmContent: PartListUnion = [];
- const { functionResponseJson, additionalParts } =
- formatLlmContentForFunctionResponse(llmContent);
- expect(functionResponseJson).toEqual({
- status: 'Tool execution succeeded.',
- });
- expect(additionalParts).toEqual([]);
- });
-
- it('should handle llmContent as a Part with undefined inlineData/fileData/text', () => {
- const llmContent: Part = {}; // An empty part object
- const { functionResponseJson, additionalParts } =
- formatLlmContentForFunctionResponse(llmContent);
- expect(functionResponseJson).toEqual({
- status: 'Tool execution succeeded.',
- });
- expect(additionalParts).toEqual([llmContent]);
- });
-});
-
describe('useReactToolScheduler in YOLO Mode', () => {
let onComplete: Mock;
let setPendingHistoryItem: Mock;
@@ -289,13 +169,13 @@ describe('useReactToolScheduler in YOLO Mode', () => {
request,
response: expect.objectContaining({
resultDisplay: 'YOLO Formatted tool output',
- responseParts: expect.arrayContaining([
- expect.objectContaining({
- functionResponse: expect.objectContaining({
- response: { output: expectedOutput },
- }),
- }),
- ]),
+ responseParts: {
+ functionResponse: {
+ id: 'yoloCall',
+ name: 'mockToolRequiresConfirmation',
+ response: { output: expectedOutput },
+ },
+ },
}),
}),
]);
@@ -433,13 +313,13 @@ describe('useReactToolScheduler', () => {
request,
response: expect.objectContaining({
resultDisplay: 'Formatted tool output',
- responseParts: expect.arrayContaining([
- expect.objectContaining({
- functionResponse: expect.objectContaining({
- response: { output: 'Tool output' },
- }),
- }),
- ]),
+ responseParts: {
+ functionResponse: {
+ id: 'call1',
+ name: 'mockTool',
+ response: { output: 'Tool output' },
+ },
+ },
}),
}),
]);
@@ -917,13 +797,13 @@ describe('useReactToolScheduler', () => {
request: requests[0],
response: expect.objectContaining({
resultDisplay: 'Display 1',
- responseParts: expect.arrayContaining([
- expect.objectContaining({
- functionResponse: expect.objectContaining({
- response: { output: 'Output 1' },
- }),
- }),
- ]),
+ responseParts: {
+ functionResponse: {
+ id: 'multi1',
+ name: 'tool1',
+ response: { output: 'Output 1' },
+ },
+ },
}),
});
expect(call2Result).toMatchObject({
@@ -931,13 +811,13 @@ describe('useReactToolScheduler', () => {
request: requests[1],
response: expect.objectContaining({
resultDisplay: 'Display 2',
- responseParts: expect.arrayContaining([
- expect.objectContaining({
- functionResponse: expect.objectContaining({
- response: { output: 'Output 2' },
- }),
- }),
- ]),
+ responseParts: {
+ functionResponse: {
+ id: 'multi2',
+ name: 'tool2',
+ response: { output: 'Output 2' },
+ },
+ },
}),
});
expect(result.current[0]).toEqual([]);
diff --git a/packages/core/src/core/coreToolScheduler.test.ts b/packages/core/src/core/coreToolScheduler.test.ts
new file mode 100644
index 00000000..be42bb24
--- /dev/null
+++ b/packages/core/src/core/coreToolScheduler.test.ts
@@ -0,0 +1,176 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect } from 'vitest';
+import { convertToFunctionResponse } from './coreToolScheduler.js';
+import { Part, PartListUnion } from '@google/genai';
+
+describe('convertToFunctionResponse', () => {
+ const toolName = 'testTool';
+ const callId = 'call1';
+
+ it('should handle simple string llmContent', () => {
+ const llmContent = 'Simple text output';
+ const result = convertToFunctionResponse(toolName, callId, llmContent);
+ expect(result).toEqual({
+ functionResponse: {
+ name: toolName,
+ id: callId,
+ response: { output: 'Simple text output' },
+ },
+ });
+ });
+
+ it('should handle llmContent as a single Part with text', () => {
+ const llmContent: Part = { text: 'Text from Part object' };
+ const result = convertToFunctionResponse(toolName, callId, llmContent);
+ expect(result).toEqual({
+ functionResponse: {
+ name: toolName,
+ id: callId,
+ response: { output: 'Text from Part object' },
+ },
+ });
+ });
+
+ it('should handle llmContent as a PartListUnion array with a single text Part', () => {
+ const llmContent: PartListUnion = [{ text: 'Text from array' }];
+ const result = convertToFunctionResponse(toolName, callId, llmContent);
+ expect(result).toEqual({
+ functionResponse: {
+ name: toolName,
+ id: callId,
+ response: { output: 'Text from array' },
+ },
+ });
+ });
+
+ it('should handle llmContent with inlineData', () => {
+ const llmContent: Part = {
+ inlineData: { mimeType: 'image/png', data: 'base64...' },
+ };
+ const result = convertToFunctionResponse(toolName, callId, llmContent);
+ expect(result).toEqual([
+ {
+ functionResponse: {
+ name: toolName,
+ id: callId,
+ response: {
+ output: 'Binary content of type image/png was processed.',
+ },
+ },
+ },
+ llmContent,
+ ]);
+ });
+
+ it('should handle llmContent with fileData', () => {
+ const llmContent: Part = {
+ fileData: { mimeType: 'application/pdf', fileUri: 'gs://...' },
+ };
+ const result = convertToFunctionResponse(toolName, callId, llmContent);
+ expect(result).toEqual([
+ {
+ functionResponse: {
+ name: toolName,
+ id: callId,
+ response: {
+ output: 'Binary content of type application/pdf was processed.',
+ },
+ },
+ },
+ llmContent,
+ ]);
+ });
+
+ it('should handle llmContent as an array of multiple Parts (text and inlineData)', () => {
+ const llmContent: PartListUnion = [
+ { text: 'Some textual description' },
+ { inlineData: { mimeType: 'image/jpeg', data: 'base64data...' } },
+ { text: 'Another text part' },
+ ];
+ const result = convertToFunctionResponse(toolName, callId, llmContent);
+ expect(result).toEqual([
+ {
+ functionResponse: {
+ name: toolName,
+ id: callId,
+ response: { output: 'Tool execution succeeded.' },
+ },
+ },
+ ...llmContent,
+ ]);
+ });
+
+ it('should handle llmContent as an array with a single inlineData Part', () => {
+ const llmContent: PartListUnion = [
+ { inlineData: { mimeType: 'image/gif', data: 'gifdata...' } },
+ ];
+ const result = convertToFunctionResponse(toolName, callId, llmContent);
+ expect(result).toEqual([
+ {
+ functionResponse: {
+ name: toolName,
+ id: callId,
+ response: {
+ output: 'Binary content of type image/gif was processed.',
+ },
+ },
+ },
+ ...llmContent,
+ ]);
+ });
+
+ it('should handle llmContent as a generic Part (not text, inlineData, or fileData)', () => {
+ const llmContent: Part = { functionCall: { name: 'test', args: {} } };
+ const result = convertToFunctionResponse(toolName, callId, llmContent);
+ expect(result).toEqual({
+ functionResponse: {
+ name: toolName,
+ id: callId,
+ response: { output: 'Tool execution succeeded.' },
+ },
+ });
+ });
+
+ it('should handle empty string llmContent', () => {
+ const llmContent = '';
+ const result = convertToFunctionResponse(toolName, callId, llmContent);
+ expect(result).toEqual({
+ functionResponse: {
+ name: toolName,
+ id: callId,
+ response: { output: '' },
+ },
+ });
+ });
+
+ it('should handle llmContent as an empty array', () => {
+ const llmContent: PartListUnion = [];
+ const result = convertToFunctionResponse(toolName, callId, llmContent);
+ expect(result).toEqual([
+ {
+ functionResponse: {
+ name: toolName,
+ id: callId,
+ response: { output: 'Tool execution succeeded.' },
+ },
+ },
+ ]);
+ });
+
+ it('should handle llmContent as a Part with undefined inlineData/fileData/text', () => {
+ const llmContent: Part = {}; // An empty part object
+ const result = convertToFunctionResponse(toolName, callId, llmContent);
+ expect(result).toEqual({
+ functionResponse: {
+ name: toolName,
+ id: callId,
+ response: { output: 'Tool execution succeeded.' },
+ },
+ });
+ });
+});
diff --git a/packages/core/src/core/coreToolScheduler.ts b/packages/core/src/core/coreToolScheduler.ts
index c3735366..f82676f1 100644
--- a/packages/core/src/core/coreToolScheduler.ts
+++ b/packages/core/src/core/coreToolScheduler.ts
@@ -14,7 +14,8 @@ import {
ToolRegistry,
ApprovalMode,
} from '../index.js';
-import { Part, PartUnion, PartListUnion } from '@google/genai';
+import { Part, PartListUnion } from '@google/genai';
+import { getResponseTextFromParts } from '../utils/generateContentResponseUtilities.js';
export type ValidatingToolCall = {
status: 'validating';
@@ -96,51 +97,79 @@ export type ToolCallsUpdateHandler = (toolCalls: ToolCall[]) => void;
/**
* Formats tool output for a Gemini FunctionResponse.
*/
-export function formatLlmContentForFunctionResponse(
- llmContent: PartListUnion,
-): {
- functionResponseJson: Record<string, string>;
- additionalParts: PartUnion[];
-} {
- const additionalParts: PartUnion[] = [];
- let functionResponseJson: Record<string, string>;
+function createFunctionResponsePart(
+ callId: string,
+ toolName: string,
+ output: string,
+): Part {
+ return {
+ functionResponse: {
+ id: callId,
+ name: toolName,
+ response: { output },
+ },
+ };
+}
+export function convertToFunctionResponse(
+ toolName: string,
+ callId: string,
+ llmContent: PartListUnion,
+): PartListUnion {
const contentToProcess =
Array.isArray(llmContent) && llmContent.length === 1
? llmContent[0]
: llmContent;
if (typeof contentToProcess === 'string') {
- functionResponseJson = { output: contentToProcess };
- } else if (Array.isArray(contentToProcess)) {
- functionResponseJson = {
- status: 'Tool execution succeeded.',
- };
- additionalParts.push(...contentToProcess);
- } else if (contentToProcess.inlineData || contentToProcess.fileData) {
+ return createFunctionResponsePart(callId, toolName, contentToProcess);
+ }
+
+ if (Array.isArray(contentToProcess)) {
+ const functionResponse = createFunctionResponsePart(
+ callId,
+ toolName,
+ 'Tool execution succeeded.',
+ );
+ return [functionResponse, ...contentToProcess];
+ }
+
+ // After this point, contentToProcess is a single Part object.
+ if (contentToProcess.functionResponse) {
+ if (contentToProcess.functionResponse.response?.content) {
+ const stringifiedOutput =
+ getResponseTextFromParts(
+ contentToProcess.functionResponse.response.content as Part[],
+ ) || '';
+ return createFunctionResponsePart(callId, toolName, stringifiedOutput);
+ }
+ // It's a functionResponse that we should pass through as is.
+ return contentToProcess;
+ }
+
+ if (contentToProcess.inlineData || contentToProcess.fileData) {
const mimeType =
contentToProcess.inlineData?.mimeType ||
contentToProcess.fileData?.mimeType ||
'unknown';
- functionResponseJson = {
- status: `Binary content of type ${mimeType} was processed.`,
- };
- additionalParts.push(contentToProcess);
- } else if (contentToProcess.text !== undefined) {
- functionResponseJson = { output: contentToProcess.text };
- } else if (contentToProcess.functionResponse) {
- functionResponseJson = JSON.parse(
- JSON.stringify(contentToProcess.functionResponse),
+ const functionResponse = createFunctionResponsePart(
+ callId,
+ toolName,
+ `Binary content of type ${mimeType} was processed.`,
);
- } else {
- functionResponseJson = { status: 'Tool execution succeeded.' };
- additionalParts.push(contentToProcess);
+ return [functionResponse, contentToProcess];
}
- return {
- functionResponseJson,
- additionalParts,
- };
+ if (contentToProcess.text !== undefined) {
+ return createFunctionResponsePart(callId, toolName, contentToProcess.text);
+ }
+
+ // Default case for other kinds of parts.
+ return createFunctionResponsePart(
+ callId,
+ toolName,
+ 'Tool execution succeeded.',
+ );
}
const createErrorResponse = (
@@ -452,20 +481,15 @@ export class CoreToolScheduler {
return;
}
- const { functionResponseJson, additionalParts } =
- formatLlmContentForFunctionResponse(toolResult.llmContent);
-
- const functionResponsePart: Part = {
- functionResponse: {
- name: toolName,
- id: callId,
- response: functionResponseJson,
- },
- };
+ const response = convertToFunctionResponse(
+ toolName,
+ callId,
+ toolResult.llmContent,
+ );
const successResponse: ToolCallResponseInfo = {
callId,
- responseParts: [functionResponsePart, ...additionalParts],
+ responseParts: response,
resultDisplay: toolResult.returnDisplay,
error: undefined,
};
diff --git a/packages/core/src/core/nonInteractiveToolExecutor.test.ts b/packages/core/src/core/nonInteractiveToolExecutor.test.ts
index 3d7dc1a2..be9294a8 100644
--- a/packages/core/src/core/nonInteractiveToolExecutor.test.ts
+++ b/packages/core/src/core/nonInteractiveToolExecutor.test.ts
@@ -81,15 +81,13 @@ describe('executeToolCall', () => {
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' },
- },
+ 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 () => {
@@ -225,7 +223,7 @@ describe('executeToolCall', () => {
name: 'testTool',
id: 'call5',
response: {
- status: 'Binary content of type image/png was processed.',
+ output: 'Binary content of type image/png was processed.',
},
},
},
diff --git a/packages/core/src/core/nonInteractiveToolExecutor.ts b/packages/core/src/core/nonInteractiveToolExecutor.ts
index 5b5c9a13..87d17c2b 100644
--- a/packages/core/src/core/nonInteractiveToolExecutor.ts
+++ b/packages/core/src/core/nonInteractiveToolExecutor.ts
@@ -4,14 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { Part } from '@google/genai';
import {
ToolCallRequestInfo,
ToolCallResponseInfo,
ToolRegistry,
ToolResult,
} from '../index.js';
-import { formatLlmContentForFunctionResponse } from './coreToolScheduler.js';
+import { convertToFunctionResponse } from './coreToolScheduler.js';
/**
* Executes a single tool call non-interactively.
@@ -54,20 +53,15 @@ export async function executeToolCall(
// 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,
- },
- };
+ const response = convertToFunctionResponse(
+ toolCallRequest.name,
+ toolCallRequest.callId,
+ toolResult.llmContent,
+ );
return {
callId: toolCallRequest.callId,
- responseParts: [functionResponsePart, ...additionalParts],
+ responseParts: response,
resultDisplay: toolResult.returnDisplay,
error: undefined,
};
diff --git a/packages/core/src/tools/mcp-tool.test.ts b/packages/core/src/tools/mcp-tool.test.ts
index 86968b3d..fc6ce6be 100644
--- a/packages/core/src/tools/mcp-tool.test.ts
+++ b/packages/core/src/tools/mcp-tool.test.ts
@@ -138,12 +138,7 @@ describe('DiscoveredMCPTool', () => {
const stringifiedResponseContent = JSON.stringify(
mockToolSuccessResultObject,
);
- // getStringifiedResultForDisplay joins text parts, then wraps the array of processed parts in JSON
- const expectedDisplayOutput =
- '```json\n' +
- JSON.stringify([stringifiedResponseContent], null, 2) +
- '\n```';
- expect(toolResult.returnDisplay).toBe(expectedDisplayOutput);
+ expect(toolResult.returnDisplay).toBe(stringifiedResponseContent);
});
it('should handle empty result from getStringifiedResultForDisplay', async () => {
diff --git a/packages/core/src/tools/mcp-tool.ts b/packages/core/src/tools/mcp-tool.ts
index 65c0cae8..8a7694d8 100644
--- a/packages/core/src/tools/mcp-tool.ts
+++ b/packages/core/src/tools/mcp-tool.ts
@@ -149,6 +149,13 @@ function getStringifiedResultForDisplay(result: Part[]) {
return part; // Fallback for unexpected structure or non-FunctionResponsePart
};
- const processedResults = result.map(processFunctionResponse);
+ const processedResults =
+ result.length === 1
+ ? processFunctionResponse(result[0])
+ : result.map(processFunctionResponse);
+ if (typeof processedResults === 'string') {
+ return processedResults;
+ }
+
return '```json\n' + JSON.stringify(processedResults, null, 2) + '\n```';
}
diff --git a/packages/core/src/utils/generateContentResponseUtilities.ts b/packages/core/src/utils/generateContentResponseUtilities.ts
index a1d62124..d575bca8 100644
--- a/packages/core/src/utils/generateContentResponseUtilities.ts
+++ b/packages/core/src/utils/generateContentResponseUtilities.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { GenerateContentResponse } from '@google/genai';
+import { GenerateContentResponse, Part } from '@google/genai';
export function getResponseText(
response: GenerateContentResponse,
@@ -15,3 +15,7 @@ export function getResponseText(
.join('') || undefined
);
}
+
+export function getResponseTextFromParts(parts: Part[]): string | undefined {
+ return parts?.map((part) => part.text).join('') || undefined;
+}