mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 12:04:56 -07:00
fix(cli): record tool calls in non-interactive mode (#10951)
This commit is contained in:
@@ -35,6 +35,7 @@ import type {
|
||||
OutputConfig,
|
||||
} from './types.js';
|
||||
import { AgentTerminateMode } from './types.js';
|
||||
import type { AnyDeclarativeTool, AnyToolInvocation } from '../tools/tools.js';
|
||||
|
||||
const { mockSendMessageStream, mockExecuteToolCall } = vi.hoisted(() => ({
|
||||
mockSendMessageStream: vi.fn(),
|
||||
@@ -318,18 +319,32 @@ describe('AgentExecutor', () => {
|
||||
'T1: Listing',
|
||||
);
|
||||
mockExecuteToolCall.mockResolvedValueOnce({
|
||||
callId: 'call1',
|
||||
resultDisplay: 'file1.txt',
|
||||
responseParts: [
|
||||
{
|
||||
functionResponse: {
|
||||
name: LSTool.Name,
|
||||
response: { result: 'file1.txt' },
|
||||
id: 'call1',
|
||||
status: 'success',
|
||||
request: {
|
||||
callId: 'call1',
|
||||
name: LSTool.Name,
|
||||
args: { path: '.' },
|
||||
isClientInitiated: false,
|
||||
prompt_id: 'test-prompt',
|
||||
},
|
||||
tool: {} as AnyDeclarativeTool,
|
||||
invocation: {} as AnyToolInvocation,
|
||||
response: {
|
||||
callId: 'call1',
|
||||
resultDisplay: 'file1.txt',
|
||||
responseParts: [
|
||||
{
|
||||
functionResponse: {
|
||||
name: LSTool.Name,
|
||||
response: { result: 'file1.txt' },
|
||||
id: 'call1',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
error: undefined,
|
||||
],
|
||||
error: undefined,
|
||||
errorType: undefined,
|
||||
contentLength: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// Turn 2: Model calls complete_task with required output
|
||||
@@ -451,13 +466,32 @@ describe('AgentExecutor', () => {
|
||||
{ name: LSTool.Name, args: { path: '.' }, id: 'call1' },
|
||||
]);
|
||||
mockExecuteToolCall.mockResolvedValueOnce({
|
||||
callId: 'call1',
|
||||
resultDisplay: 'ok',
|
||||
responseParts: [
|
||||
{
|
||||
functionResponse: { name: LSTool.Name, response: {}, id: 'call1' },
|
||||
},
|
||||
],
|
||||
status: 'success',
|
||||
request: {
|
||||
callId: 'call1',
|
||||
name: LSTool.Name,
|
||||
args: { path: '.' },
|
||||
isClientInitiated: false,
|
||||
prompt_id: 'test-prompt',
|
||||
},
|
||||
tool: {} as AnyDeclarativeTool,
|
||||
invocation: {} as AnyToolInvocation,
|
||||
response: {
|
||||
callId: 'call1',
|
||||
resultDisplay: 'ok',
|
||||
responseParts: [
|
||||
{
|
||||
functionResponse: {
|
||||
name: LSTool.Name,
|
||||
response: {},
|
||||
id: 'call1',
|
||||
},
|
||||
},
|
||||
],
|
||||
error: undefined,
|
||||
errorType: undefined,
|
||||
contentLength: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
mockModelResponse(
|
||||
@@ -504,13 +538,32 @@ describe('AgentExecutor', () => {
|
||||
{ name: LSTool.Name, args: { path: '.' }, id: 'call1' },
|
||||
]);
|
||||
mockExecuteToolCall.mockResolvedValueOnce({
|
||||
callId: 'call1',
|
||||
resultDisplay: 'ok',
|
||||
responseParts: [
|
||||
{
|
||||
functionResponse: { name: LSTool.Name, response: {}, id: 'call1' },
|
||||
},
|
||||
],
|
||||
status: 'success',
|
||||
request: {
|
||||
callId: 'call1',
|
||||
name: LSTool.Name,
|
||||
args: { path: '.' },
|
||||
isClientInitiated: false,
|
||||
prompt_id: 'test-prompt',
|
||||
},
|
||||
tool: {} as AnyDeclarativeTool,
|
||||
invocation: {} as AnyToolInvocation,
|
||||
response: {
|
||||
callId: 'call1',
|
||||
resultDisplay: 'ok',
|
||||
responseParts: [
|
||||
{
|
||||
functionResponse: {
|
||||
name: LSTool.Name,
|
||||
response: {},
|
||||
id: 'call1',
|
||||
},
|
||||
},
|
||||
],
|
||||
error: undefined,
|
||||
errorType: undefined,
|
||||
contentLength: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
mockModelResponse([], 'I think I am done.');
|
||||
@@ -675,17 +728,26 @@ describe('AgentExecutor', () => {
|
||||
if (callsStarted === 2) resolveCalls();
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
return {
|
||||
callId: reqInfo.callId,
|
||||
resultDisplay: 'ok',
|
||||
responseParts: [
|
||||
{
|
||||
functionResponse: {
|
||||
name: reqInfo.name,
|
||||
response: {},
|
||||
id: reqInfo.callId,
|
||||
status: 'success',
|
||||
request: reqInfo,
|
||||
tool: {} as AnyDeclarativeTool,
|
||||
invocation: {} as AnyToolInvocation,
|
||||
response: {
|
||||
callId: reqInfo.callId,
|
||||
resultDisplay: 'ok',
|
||||
responseParts: [
|
||||
{
|
||||
functionResponse: {
|
||||
name: reqInfo.name,
|
||||
response: {},
|
||||
id: reqInfo.callId,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
error: undefined,
|
||||
errorType: undefined,
|
||||
contentLength: undefined,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -802,11 +864,26 @@ describe('AgentExecutor', () => {
|
||||
const mockWorkResponse = (id: string) => {
|
||||
mockModelResponse([{ name: LSTool.Name, args: { path: '.' }, id }]);
|
||||
mockExecuteToolCall.mockResolvedValueOnce({
|
||||
callId: id,
|
||||
resultDisplay: 'ok',
|
||||
responseParts: [
|
||||
{ functionResponse: { name: LSTool.Name, response: {}, id } },
|
||||
],
|
||||
status: 'success',
|
||||
request: {
|
||||
callId: id,
|
||||
name: LSTool.Name,
|
||||
args: { path: '.' },
|
||||
isClientInitiated: false,
|
||||
prompt_id: 'test-prompt',
|
||||
},
|
||||
tool: {} as AnyDeclarativeTool,
|
||||
invocation: {} as AnyToolInvocation,
|
||||
response: {
|
||||
callId: id,
|
||||
resultDisplay: 'ok',
|
||||
responseParts: [
|
||||
{ functionResponse: { name: LSTool.Name, response: {}, id } },
|
||||
],
|
||||
error: undefined,
|
||||
errorType: undefined,
|
||||
contentLength: undefined,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -835,12 +912,21 @@ describe('AgentExecutor', () => {
|
||||
mockModelResponse([{ name: LSTool.Name, args: { path: '.' }, id: 't1' }]);
|
||||
|
||||
// Long running tool
|
||||
mockExecuteToolCall.mockImplementationOnce(async () => {
|
||||
mockExecuteToolCall.mockImplementationOnce(async (_ctx, reqInfo) => {
|
||||
await vi.advanceTimersByTimeAsync(61 * 1000);
|
||||
return {
|
||||
callId: 't1',
|
||||
resultDisplay: 'ok',
|
||||
responseParts: [],
|
||||
status: 'success',
|
||||
request: reqInfo,
|
||||
tool: {} as AnyDeclarativeTool,
|
||||
invocation: {} as AnyToolInvocation,
|
||||
response: {
|
||||
callId: 't1',
|
||||
resultDisplay: 'ok',
|
||||
responseParts: [],
|
||||
error: undefined,
|
||||
errorType: undefined,
|
||||
contentLength: undefined,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -534,7 +534,7 @@ export class AgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
|
||||
// Create a promise for the tool execution
|
||||
const executionPromise = (async () => {
|
||||
const toolResponse = await executeToolCall(
|
||||
const { response: toolResponse } = await executeToolCall(
|
||||
this.runtimeContext,
|
||||
requestInfo,
|
||||
signal,
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('executeToolCall', () => {
|
||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
|
||||
executeFn.mockResolvedValue(toolResult);
|
||||
|
||||
const response = await executeToolCall(
|
||||
const { response } = await executeToolCall(
|
||||
mockConfig,
|
||||
request,
|
||||
abortController.signal,
|
||||
@@ -126,7 +126,7 @@ describe('executeToolCall', () => {
|
||||
'anotherTool',
|
||||
]);
|
||||
|
||||
const response = await executeToolCall(
|
||||
const { response } = await executeToolCall(
|
||||
mockConfig,
|
||||
request,
|
||||
abortController.signal,
|
||||
@@ -167,7 +167,7 @@ describe('executeToolCall', () => {
|
||||
throw new Error('Invalid parameters');
|
||||
});
|
||||
|
||||
const response = await executeToolCall(
|
||||
const { response } = await executeToolCall(
|
||||
mockConfig,
|
||||
request,
|
||||
abortController.signal,
|
||||
@@ -212,7 +212,7 @@ describe('executeToolCall', () => {
|
||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
|
||||
executeFn.mockResolvedValue(executionErrorResult);
|
||||
|
||||
const response = await executeToolCall(
|
||||
const { response } = await executeToolCall(
|
||||
mockConfig,
|
||||
request,
|
||||
abortController.signal,
|
||||
@@ -248,7 +248,7 @@ describe('executeToolCall', () => {
|
||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
|
||||
executeFn.mockRejectedValue(new Error('Something went very wrong'));
|
||||
|
||||
const response = await executeToolCall(
|
||||
const { response } = await executeToolCall(
|
||||
mockConfig,
|
||||
request,
|
||||
abortController.signal,
|
||||
@@ -290,7 +290,7 @@ describe('executeToolCall', () => {
|
||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
|
||||
executeFn.mockResolvedValue(toolResult);
|
||||
|
||||
const response = await executeToolCall(
|
||||
const { response } = await executeToolCall(
|
||||
mockConfig,
|
||||
request,
|
||||
abortController.signal,
|
||||
@@ -333,7 +333,7 @@ describe('executeToolCall', () => {
|
||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
|
||||
executeFn.mockResolvedValue(toolResult);
|
||||
|
||||
const response = await executeToolCall(
|
||||
const { response } = await executeToolCall(
|
||||
mockConfig,
|
||||
request,
|
||||
abortController.signal,
|
||||
@@ -361,7 +361,7 @@ describe('executeToolCall', () => {
|
||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);
|
||||
executeFn.mockResolvedValue(toolResult);
|
||||
|
||||
const response = await executeToolCall(
|
||||
const { response } = await executeToolCall(
|
||||
mockConfig,
|
||||
request,
|
||||
abortController.signal,
|
||||
|
||||
@@ -4,12 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {
|
||||
ToolCallRequestInfo,
|
||||
ToolCallResponseInfo,
|
||||
Config,
|
||||
} from '../index.js';
|
||||
import { CoreToolScheduler } from './coreToolScheduler.js';
|
||||
import type { ToolCallRequestInfo, Config } from '../index.js';
|
||||
import {
|
||||
CoreToolScheduler,
|
||||
type CompletedToolCall,
|
||||
} from './coreToolScheduler.js';
|
||||
|
||||
/**
|
||||
* Executes a single tool call non-interactively by leveraging the CoreToolScheduler.
|
||||
@@ -18,14 +17,14 @@ export async function executeToolCall(
|
||||
config: Config,
|
||||
toolCallRequest: ToolCallRequestInfo,
|
||||
abortSignal: AbortSignal,
|
||||
): Promise<ToolCallResponseInfo> {
|
||||
return new Promise<ToolCallResponseInfo>((resolve, reject) => {
|
||||
): Promise<CompletedToolCall> {
|
||||
return new Promise<CompletedToolCall>((resolve, reject) => {
|
||||
new CoreToolScheduler({
|
||||
config,
|
||||
getPreferredEditor: () => undefined,
|
||||
onEditorClose: () => {},
|
||||
onAllToolCallsComplete: async (completedToolCalls) => {
|
||||
resolve(completedToolCalls[0].response);
|
||||
resolve(completedToolCalls[0]);
|
||||
},
|
||||
})
|
||||
.schedule(toolCallRequest, abortSignal)
|
||||
|
||||
@@ -36,6 +36,7 @@ import type {
|
||||
GenerateContentResponse,
|
||||
} from '@google/genai';
|
||||
import { ToolErrorType } from '../tools/tool-error.js';
|
||||
import type { AnyDeclarativeTool, AnyToolInvocation } from '../tools/tools.js';
|
||||
|
||||
vi.mock('./geminiChat.js');
|
||||
vi.mock('./contentGenerator.js');
|
||||
@@ -580,13 +581,26 @@ describe('subagent.ts', () => {
|
||||
]),
|
||||
);
|
||||
|
||||
// Mock the tool execution result
|
||||
// Mock the tool execution result - must return CompletedToolCall
|
||||
vi.mocked(executeToolCall).mockResolvedValue({
|
||||
callId: 'call_1',
|
||||
responseParts: [{ text: 'file1.txt\nfile2.ts' }],
|
||||
resultDisplay: 'Listed 2 files',
|
||||
error: undefined,
|
||||
errorType: undefined, // Or ToolErrorType.NONE if available and appropriate
|
||||
status: 'success',
|
||||
request: {
|
||||
callId: 'call_1',
|
||||
name: 'list_files',
|
||||
args: { path: '.' },
|
||||
isClientInitiated: false,
|
||||
prompt_id: 'prompt-id-1',
|
||||
},
|
||||
tool: {} as AnyDeclarativeTool,
|
||||
invocation: {} as AnyToolInvocation,
|
||||
response: {
|
||||
callId: 'call_1',
|
||||
responseParts: [{ text: 'file1.txt\nfile2.ts' }],
|
||||
resultDisplay: 'Listed 2 files',
|
||||
error: undefined,
|
||||
errorType: undefined,
|
||||
contentLength: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const scope = await SubAgentScope.create(
|
||||
@@ -635,13 +649,25 @@ describe('subagent.ts', () => {
|
||||
]),
|
||||
);
|
||||
|
||||
// Mock the tool execution failure.
|
||||
// Mock the tool execution failure - must return CompletedToolCall
|
||||
vi.mocked(executeToolCall).mockResolvedValue({
|
||||
callId: 'call_fail',
|
||||
responseParts: [{ text: 'ERROR: Tool failed catastrophically' }], // This should be sent to the model
|
||||
resultDisplay: 'Tool failed catastrophically',
|
||||
error: new Error('Failure'),
|
||||
errorType: ToolErrorType.INVALID_TOOL_PARAMS,
|
||||
status: 'error',
|
||||
request: {
|
||||
callId: 'call_fail',
|
||||
name: 'failing_tool',
|
||||
args: {},
|
||||
isClientInitiated: false,
|
||||
prompt_id: 'prompt-id-fail',
|
||||
},
|
||||
tool: {} as AnyDeclarativeTool,
|
||||
response: {
|
||||
callId: 'call_fail',
|
||||
responseParts: [{ text: 'ERROR: Tool failed catastrophically' }], // This should be sent to the model
|
||||
resultDisplay: 'Tool failed catastrophically',
|
||||
error: new Error('Failure'),
|
||||
errorType: ToolErrorType.INVALID_TOOL_PARAMS,
|
||||
contentLength: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const scope = await SubAgentScope.create(
|
||||
|
||||
@@ -580,11 +580,13 @@ export class SubAgentScope {
|
||||
error: undefined,
|
||||
};
|
||||
} else {
|
||||
toolResponse = await executeToolCall(
|
||||
this.runtimeContext,
|
||||
requestInfo,
|
||||
abortController.signal,
|
||||
);
|
||||
toolResponse = (
|
||||
await executeToolCall(
|
||||
this.runtimeContext,
|
||||
requestInfo,
|
||||
abortController.signal,
|
||||
)
|
||||
).response;
|
||||
}
|
||||
|
||||
if (toolResponse.error) {
|
||||
|
||||
Reference in New Issue
Block a user