fix(cli): record tool calls in non-interactive mode (#10951)

This commit is contained in:
bl-ue
2025-10-14 09:51:00 -06:00
committed by GitHub
parent 3ba4ba79fa
commit 9e8c767694
8 changed files with 316 additions and 98 deletions

View File

@@ -9,6 +9,8 @@ import type {
ToolRegistry,
ServerGeminiStreamEvent,
SessionMetrics,
AnyDeclarativeTool,
AnyToolInvocation,
} from '@google/gemini-cli-core';
import {
executeToolCall,
@@ -204,7 +206,25 @@ describe('runNonInteractive', () => {
},
};
const toolResponse: Part[] = [{ text: 'Tool response' }];
mockCoreExecuteToolCall.mockResolvedValue({ responseParts: toolResponse });
mockCoreExecuteToolCall.mockResolvedValue({
status: 'success',
request: {
callId: 'tool-1',
name: 'testTool',
args: { arg1: 'value1' },
isClientInitiated: false,
prompt_id: 'prompt-id-2',
},
tool: {} as AnyDeclarativeTool,
invocation: {} as AnyToolInvocation,
response: {
responseParts: toolResponse,
callId: 'tool-1',
error: undefined,
errorType: undefined,
contentLength: undefined,
},
});
const firstCallEvents: ServerGeminiStreamEvent[] = [toolCallEvent];
const secondCallEvents: ServerGeminiStreamEvent[] = [
@@ -254,19 +274,32 @@ describe('runNonInteractive', () => {
},
};
mockCoreExecuteToolCall.mockResolvedValue({
error: new Error('Execution failed'),
errorType: ToolErrorType.EXECUTION_FAILED,
responseParts: [
{
functionResponse: {
name: 'errorTool',
response: {
output: 'Error: Execution failed',
status: 'error',
request: {
callId: 'tool-1',
name: 'errorTool',
args: {},
isClientInitiated: false,
prompt_id: 'prompt-id-3',
},
tool: {} as AnyDeclarativeTool,
response: {
callId: 'tool-1',
error: new Error('Execution failed'),
errorType: ToolErrorType.EXECUTION_FAILED,
responseParts: [
{
functionResponse: {
name: 'errorTool',
response: {
output: 'Error: Execution failed',
},
},
},
},
],
resultDisplay: 'Execution failed',
],
resultDisplay: 'Execution failed',
contentLength: undefined,
},
});
const finalResponse: ServerGeminiStreamEvent[] = [
{
@@ -340,9 +373,22 @@ describe('runNonInteractive', () => {
},
};
mockCoreExecuteToolCall.mockResolvedValue({
error: new Error('Tool "nonexistentTool" not found in registry.'),
resultDisplay: 'Tool "nonexistentTool" not found in registry.',
responseParts: [],
status: 'error',
request: {
callId: 'tool-1',
name: 'nonexistentTool',
args: {},
isClientInitiated: false,
prompt_id: 'prompt-id-5',
},
response: {
callId: 'tool-1',
error: new Error('Tool "nonexistentTool" not found in registry.'),
resultDisplay: 'Tool "nonexistentTool" not found in registry.',
responseParts: [],
errorType: undefined,
contentLength: undefined,
},
});
const finalResponse: ServerGeminiStreamEvent[] = [
{
@@ -501,7 +547,25 @@ describe('runNonInteractive', () => {
},
};
const toolResponse: Part[] = [{ text: 'Tool executed successfully' }];
mockCoreExecuteToolCall.mockResolvedValue({ responseParts: toolResponse });
mockCoreExecuteToolCall.mockResolvedValue({
status: 'success',
request: {
callId: 'tool-1',
name: 'testTool',
args: { arg1: 'value1' },
isClientInitiated: false,
prompt_id: 'prompt-id-tool-only',
},
tool: {} as AnyDeclarativeTool,
invocation: {} as AnyToolInvocation,
response: {
responseParts: toolResponse,
callId: 'tool-1',
error: undefined,
errorType: undefined,
contentLength: undefined,
},
});
// First call returns only tool call, no content
const firstCallEvents: ServerGeminiStreamEvent[] = [
@@ -897,7 +961,25 @@ describe('runNonInteractive', () => {
},
};
const toolResponse: Part[] = [{ text: 'file.txt' }];
mockCoreExecuteToolCall.mockResolvedValue({ responseParts: toolResponse });
mockCoreExecuteToolCall.mockResolvedValue({
status: 'success',
request: {
callId: 'tool-shell-1',
name: 'ShellTool',
args: { command: 'ls' },
isClientInitiated: false,
prompt_id: 'prompt-id-allowed',
},
tool: {} as AnyDeclarativeTool,
invocation: {} as AnyToolInvocation,
response: {
responseParts: toolResponse,
callId: 'tool-shell-1',
error: undefined,
errorType: undefined,
contentLength: undefined,
},
});
const firstCallEvents: ServerGeminiStreamEvent[] = [toolCallEvent];
const secondCallEvents: ServerGeminiStreamEvent[] = [

View File

@@ -4,7 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config, ToolCallRequestInfo } from '@google/gemini-cli-core';
import type {
Config,
ToolCallRequestInfo,
CompletedToolCall,
} from '@google/gemini-cli-core';
import { isSlashCommand } from './ui/utils/commandUtils.js';
import type { LoadedSettings } from './config/settings.js';
import {
@@ -132,12 +136,17 @@ export async function runNonInteractive(
if (toolCallRequests.length > 0) {
const toolResponseParts: Part[] = [];
const completedToolCalls: CompletedToolCall[] = [];
for (const requestInfo of toolCallRequests) {
const toolResponse = await executeToolCall(
const completedToolCall = await executeToolCall(
config,
requestInfo,
abortController.signal,
);
const toolResponse = completedToolCall.response;
completedToolCalls.push(completedToolCall);
if (toolResponse.error) {
handleToolError(
@@ -155,6 +164,20 @@ export async function runNonInteractive(
toolResponseParts.push(...toolResponse.responseParts);
}
}
// Record tool calls with full metadata before sending responses to Gemini
try {
const currentModel =
geminiClient.getCurrentSequenceModel() ?? config.getModel();
geminiClient
.getChat()
.recordCompletedToolCalls(currentModel, completedToolCalls);
} catch (error) {
console.error(
`Error recording completed tool call information: ${error}`,
);
}
currentMessages = [{ role: 'user', parts: toolResponseParts }];
} else {
if (config.getOutputFormat() === OutputFormat.JSON) {