feat(core): restore MessageBus optionality for soft migration (Phase 1) (#15774)

This commit is contained in:
Abhi
2026-01-04 14:59:35 -05:00
committed by GitHub
parent d3c206c677
commit eec5d5ebf8
16 changed files with 105 additions and 82 deletions
+2 -1
View File
@@ -35,7 +35,8 @@ import {
createStreamMessageRequest, createStreamMessageRequest,
createMockConfig, createMockConfig,
} from '../utils/testing_utils.js'; } from '../utils/testing_utils.js';
import { MockTool } from '@google/gemini-cli-core'; // Import MockTool from specific path to avoid vitest dependency in main core bundle
import { MockTool } from '@google/gemini-cli-core/src/test-utils/mock-tool.js';
import type { Command, CommandContext } from '../commands/types.js'; import type { Command, CommandContext } from '../commands/types.js';
const mockToolConfirmationFn = async () => const mockToolConfirmationFn = async () =>
@@ -31,10 +31,10 @@ import {
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
ToolConfirmationOutcome, ToolConfirmationOutcome,
ApprovalMode, ApprovalMode,
MockTool,
HookSystem, HookSystem,
PREVIEW_GEMINI_MODEL, PREVIEW_GEMINI_MODEL,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { MockTool } from '@google/gemini-cli-core/src/test-utils/mock-tool.js';
import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js'; import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js';
import { ToolCallStatus } from '../types.js'; import { ToolCallStatus } from '../types.js';
-3
View File
@@ -157,9 +157,6 @@ export { Storage } from './config/storage.js';
// Export hooks system // Export hooks system
export * from './hooks/index.js'; export * from './hooks/index.js';
// Export test utils
export * from './test-utils/index.js';
// Export hook types // Export hook types
export * from './hooks/types.js'; export * from './hooks/types.js';
+28 -11
View File
@@ -18,6 +18,8 @@ import {
BaseToolInvocation, BaseToolInvocation,
Kind, Kind,
} from '../tools/tools.js'; } from '../tools/tools.js';
import { createMockMessageBus } from './mock-message-bus.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
interface MockToolOptions { interface MockToolOptions {
name: string; name: string;
@@ -35,6 +37,7 @@ interface MockToolOptions {
updateOutput?: (output: string) => void, updateOutput?: (output: string) => void,
) => Promise<ToolResult>; ) => Promise<ToolResult>;
params?: object; params?: object;
messageBus?: MessageBus;
} }
class MockToolInvocation extends BaseToolInvocation< class MockToolInvocation extends BaseToolInvocation<
@@ -44,8 +47,9 @@ class MockToolInvocation extends BaseToolInvocation<
constructor( constructor(
private readonly tool: MockTool, private readonly tool: MockTool,
params: { [key: string]: unknown }, params: { [key: string]: unknown },
messageBus: MessageBus,
) { ) {
super(params); super(params, messageBus, tool.name, tool.displayName);
} }
execute( execute(
@@ -96,6 +100,7 @@ export class MockTool extends BaseDeclarativeTool<
options.params, options.params,
options.isOutputMarkdown ?? false, options.isOutputMarkdown ?? false,
options.canUpdateOutput ?? false, options.canUpdateOutput ?? false,
options.messageBus ?? createMockMessageBus(),
); );
if (options.shouldConfirmExecute) { if (options.shouldConfirmExecute) {
@@ -115,10 +120,11 @@ export class MockTool extends BaseDeclarativeTool<
} }
} }
protected createInvocation(params: { protected createInvocation(
[key: string]: unknown; params: { [key: string]: unknown },
}): ToolInvocation<{ [key: string]: unknown }, ToolResult> { messageBus: MessageBus,
return new MockToolInvocation(this, params); ): ToolInvocation<{ [key: string]: unknown }, ToolResult> {
return new MockToolInvocation(this, params, messageBus);
} }
} }
@@ -138,8 +144,9 @@ export class MockModifiableToolInvocation extends BaseToolInvocation<
constructor( constructor(
private readonly tool: MockModifiableTool, private readonly tool: MockModifiableTool,
params: Record<string, unknown>, params: Record<string, unknown>,
messageBus: MessageBus,
) { ) {
super(params); super(params, messageBus, tool.name, tool.displayName);
} }
async execute(_abortSignal: AbortSignal): Promise<ToolResult> { async execute(_abortSignal: AbortSignal): Promise<ToolResult> {
@@ -189,10 +196,19 @@ export class MockModifiableTool
shouldConfirm = true; shouldConfirm = true;
constructor(name = 'mockModifiableTool') { constructor(name = 'mockModifiableTool') {
super(name, name, 'A mock modifiable tool for testing.', Kind.Other, { super(
type: 'object', name,
properties: { param: { type: 'string' } }, name,
}); 'A mock modifiable tool for testing.',
Kind.Other,
{
type: 'object',
properties: { param: { type: 'string' } },
},
true,
false,
createMockMessageBus(),
);
} }
getModifyContext( getModifyContext(
@@ -212,7 +228,8 @@ export class MockModifiableTool
protected createInvocation( protected createInvocation(
params: Record<string, unknown>, params: Record<string, unknown>,
messageBus: MessageBus,
): ToolInvocation<Record<string, unknown>, ToolResult> { ): ToolInvocation<Record<string, unknown>, ToolResult> {
return new MockModifiableToolInvocation(this, params); return new MockModifiableToolInvocation(this, params, messageBus);
} }
} }
+15 -3
View File
@@ -59,6 +59,10 @@ import { ApprovalMode } from '../policy/types.js';
import type { Content, Part, SchemaUnion } from '@google/genai'; import type { Content, Part, SchemaUnion } from '@google/genai';
import { StandardFileSystemService } from '../services/fileSystemService.js'; import { StandardFileSystemService } from '../services/fileSystemService.js';
import { WorkspaceContext } from '../utils/workspaceContext.js'; import { WorkspaceContext } from '../utils/workspaceContext.js';
import {
createMockMessageBus,
getMockMessageBusInstance,
} from '../test-utils/mock-message-bus.js';
describe('EditTool', () => { describe('EditTool', () => {
let tool: EditTool; let tool: EditTool;
@@ -177,7 +181,9 @@ describe('EditTool', () => {
}, },
); );
tool = new EditTool(mockConfig); const bus = createMockMessageBus();
getMockMessageBusInstance(bus).defaultToolDecision = 'ask_user';
tool = new EditTool(mockConfig, bus);
}); });
afterEach(() => { afterEach(() => {
@@ -1029,7 +1035,10 @@ describe('EditTool', () => {
it('should use windows-style path examples on windows', () => { it('should use windows-style path examples on windows', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32'); vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
const tool = new EditTool({} as unknown as Config); const tool = new EditTool(
{} as unknown as Config,
createMockMessageBus(),
);
const schema = tool.schema; const schema = tool.schema;
expect( expect(
(schema.parametersJsonSchema as EditFileParameterSchema).properties (schema.parametersJsonSchema as EditFileParameterSchema).properties
@@ -1040,7 +1049,10 @@ describe('EditTool', () => {
it('should use unix-style path examples on non-windows platforms', () => { it('should use unix-style path examples on non-windows platforms', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('linux'); vi.spyOn(process, 'platform', 'get').mockReturnValue('linux');
const tool = new EditTool({} as unknown as Config); const tool = new EditTool(
{} as unknown as Config,
createMockMessageBus(),
);
const schema = tool.schema; const schema = tool.schema;
expect( expect(
(schema.parametersJsonSchema as EditFileParameterSchema).properties (schema.parametersJsonSchema as EditFileParameterSchema).properties
+2 -1
View File
@@ -16,6 +16,7 @@ import type { Config } from '../config/config.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js'; import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { ToolErrorType } from './tool-error.js'; import { ToolErrorType } from './tool-error.js';
import * as glob from 'glob'; import * as glob from 'glob';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
vi.mock('glob', { spy: true }); vi.mock('glob', { spy: true });
@@ -43,7 +44,7 @@ describe('GlobTool', () => {
// Create a unique root directory for each test run // Create a unique root directory for each test run
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'glob-tool-root-')); tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'glob-tool-root-'));
await fs.writeFile(path.join(tempRootDir, '.git'), ''); // Fake git repo await fs.writeFile(path.join(tempRootDir, '.git'), ''); // Fake git repo
globTool = new GlobTool(mockConfig); globTool = new GlobTool(mockConfig, createMockMessageBus());
// Create some test files and directories within this root // Create some test files and directories within this root
// Top-level files // Top-level files
+14 -4
View File
@@ -14,6 +14,7 @@ import type { Config } from '../config/config.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js'; import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { ToolErrorType } from './tool-error.js'; import { ToolErrorType } from './tool-error.js';
import * as glob from 'glob'; import * as glob from 'glob';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
vi.mock('glob', { spy: true }); vi.mock('glob', { spy: true });
@@ -47,7 +48,7 @@ describe('GrepTool', () => {
beforeEach(async () => { beforeEach(async () => {
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-root-')); tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-root-'));
grepTool = new GrepTool(mockConfig); grepTool = new GrepTool(mockConfig, createMockMessageBus());
// Create some test files and directories // Create some test files and directories
await fs.writeFile( await fs.writeFile(
@@ -270,7 +271,10 @@ describe('GrepTool', () => {
}), }),
} as unknown as Config; } as unknown as Config;
const multiDirGrepTool = new GrepTool(multiDirConfig); const multiDirGrepTool = new GrepTool(
multiDirConfig,
createMockMessageBus(),
);
const params: GrepToolParams = { pattern: 'world' }; const params: GrepToolParams = { pattern: 'world' };
const invocation = multiDirGrepTool.build(params); const invocation = multiDirGrepTool.build(params);
const result = await invocation.execute(abortSignal); const result = await invocation.execute(abortSignal);
@@ -323,7 +327,10 @@ describe('GrepTool', () => {
}), }),
} as unknown as Config; } as unknown as Config;
const multiDirGrepTool = new GrepTool(multiDirConfig); const multiDirGrepTool = new GrepTool(
multiDirConfig,
createMockMessageBus(),
);
// Search only in the 'sub' directory of the first workspace // Search only in the 'sub' directory of the first workspace
const params: GrepToolParams = { pattern: 'world', dir_path: 'sub' }; const params: GrepToolParams = { pattern: 'world', dir_path: 'sub' };
@@ -385,7 +392,10 @@ describe('GrepTool', () => {
}), }),
} as unknown as Config; } as unknown as Config;
const multiDirGrepTool = new GrepTool(multiDirConfig); const multiDirGrepTool = new GrepTool(
multiDirConfig,
createMockMessageBus(),
);
const params: GrepToolParams = { pattern: 'testPattern' }; const params: GrepToolParams = { pattern: 'testPattern' };
const invocation = multiDirGrepTool.build(params); const invocation = multiDirGrepTool.build(params);
expect(invocation.getDescription()).toBe( expect(invocation.getDescription()).toBe(
+2 -1
View File
@@ -13,6 +13,7 @@ import type { Config } from '../config/config.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { ToolErrorType } from './tool-error.js'; import { ToolErrorType } from './tool-error.js';
import { WorkspaceContext } from '../utils/workspaceContext.js'; import { WorkspaceContext } from '../utils/workspaceContext.js';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
describe('LSTool', () => { describe('LSTool', () => {
let lsTool: LSTool; let lsTool: LSTool;
@@ -39,7 +40,7 @@ describe('LSTool', () => {
}), }),
} as unknown as Config; } as unknown as Config;
lsTool = new LSTool(mockConfig); lsTool = new LSTool(mockConfig, createMockMessageBus());
}); });
afterEach(async () => { afterEach(async () => {
@@ -56,22 +56,19 @@ class TestToolInvocation extends BaseToolInvocation<TestParams, TestResult> {
override async shouldConfirmExecute( override async shouldConfirmExecute(
abortSignal: AbortSignal, abortSignal: AbortSignal,
): Promise<false> { ): Promise<false> {
// This conditional is here to allow testing of the case where there is no message bus. const decision = await this.getMessageBusDecision(abortSignal);
if (this.messageBus) { if (decision === 'ALLOW') {
const decision = await this.getMessageBusDecision(abortSignal); return false;
if (decision === 'ALLOW') { }
return false; if (decision === 'DENY') {
} throw new Error('Tool execution denied by policy');
if (decision === 'DENY') {
throw new Error('Tool execution denied by policy');
}
} }
return false; return false;
} }
} }
class TestTool extends BaseDeclarativeTool<TestParams, TestResult> { class TestTool extends BaseDeclarativeTool<TestParams, TestResult> {
constructor(messageBus?: MessageBus) { constructor(messageBus: MessageBus) {
super( super(
'test-tool', 'test-tool',
'Test Tool', 'Test Tool',
@@ -90,7 +87,7 @@ class TestTool extends BaseDeclarativeTool<TestParams, TestResult> {
); );
} }
protected createInvocation(params: TestParams, messageBus?: MessageBus) { protected createInvocation(params: TestParams, messageBus: MessageBus) {
return new TestToolInvocation(params, messageBus); return new TestToolInvocation(params, messageBus);
} }
} }
@@ -220,16 +217,6 @@ describe('Message Bus Integration', () => {
); );
}); });
it('should fall back to default behavior when no message bus', async () => {
const tool = new TestTool(); // No message bus
const invocation = tool.build({ testParam: 'test-value' });
const result = await invocation.shouldConfirmExecute(
new AbortController().signal,
);
expect(result).toBe(false);
});
it('should ignore responses with wrong correlation ID', async () => { it('should ignore responses with wrong correlation ID', async () => {
vi.useFakeTimers(); vi.useFakeTimers();
@@ -260,28 +247,6 @@ describe('Message Bus Integration', () => {
}); });
}); });
describe('Backward Compatibility', () => {
it('should work with existing tools that do not use message bus', async () => {
const tool = new TestTool(); // No message bus
const invocation = tool.build({ testParam: 'test-value' });
// Should execute normally
const result = await invocation.execute(new AbortController().signal);
expect(result.testValue).toBe('test-value');
expect(result.llmContent).toBe('Executed with test-value');
});
it('should work with tools that have message bus but use default confirmation', async () => {
const tool = new TestTool(messageBus);
const invocation = tool.build({ testParam: 'test-value' });
// Should execute normally even with message bus available
const result = await invocation.execute(new AbortController().signal);
expect(result.testValue).toBe('test-value');
expect(result.llmContent).toBe('Executed with test-value');
});
});
describe('Error Handling', () => { describe('Error Handling', () => {
it('should handle message bus publish errors gracefully', async () => { it('should handle message bus publish errors gracefully', async () => {
const tool = new TestTool(messageBus); const tool = new TestTool(messageBus);
+3 -2
View File
@@ -17,6 +17,7 @@ import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { StandardFileSystemService } from '../services/fileSystemService.js'; import { StandardFileSystemService } from '../services/fileSystemService.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js'; import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { WorkspaceContext } from '../utils/workspaceContext.js'; import { WorkspaceContext } from '../utils/workspaceContext.js';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
vi.mock('../telemetry/loggers.js', () => ({ vi.mock('../telemetry/loggers.js', () => ({
logFileOperation: vi.fn(), logFileOperation: vi.fn(),
@@ -46,7 +47,7 @@ describe('ReadFileTool', () => {
}, },
isInteractive: () => false, isInteractive: () => false,
} as unknown as Config; } as unknown as Config;
tool = new ReadFileTool(mockConfigInstance); tool = new ReadFileTool(mockConfigInstance, createMockMessageBus());
}); });
afterEach(async () => { afterEach(async () => {
@@ -438,7 +439,7 @@ describe('ReadFileTool', () => {
getProjectTempDir: () => path.join(tempRootDir, '.temp'), getProjectTempDir: () => path.join(tempRootDir, '.temp'),
}, },
} as unknown as Config; } as unknown as Config;
tool = new ReadFileTool(mockConfigInstance); tool = new ReadFileTool(mockConfigInstance, createMockMessageBus());
}); });
it('should throw error if path is ignored by a .geminiignore pattern', async () => { it('should throw error if path is ignored by a .geminiignore pattern', async () => {
@@ -21,6 +21,7 @@ import {
DEFAULT_FILE_EXCLUDES, DEFAULT_FILE_EXCLUDES,
} from '../utils/ignorePatterns.js'; } from '../utils/ignorePatterns.js';
import * as glob from 'glob'; import * as glob from 'glob';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
vi.mock('glob', { spy: true }); vi.mock('glob', { spy: true });
@@ -90,7 +91,7 @@ describe('ReadManyFilesTool', () => {
}), }),
isInteractive: () => false, isInteractive: () => false,
} as Partial<Config> as Config; } as Partial<Config> as Config;
tool = new ReadManyFilesTool(mockConfig); tool = new ReadManyFilesTool(mockConfig, createMockMessageBus());
mockReadFileFn = mockControl.mockReadFile; mockReadFileFn = mockControl.mockReadFile;
mockReadFileFn.mockReset(); mockReadFileFn.mockReset();
@@ -505,7 +506,7 @@ describe('ReadManyFilesTool', () => {
}), }),
isInteractive: () => false, isInteractive: () => false,
} as Partial<Config> as Config; } as Partial<Config> as Config;
tool = new ReadManyFilesTool(mockConfig); tool = new ReadManyFilesTool(mockConfig, createMockMessageBus());
fs.writeFileSync(path.join(tempDir1, 'file1.txt'), 'Content1'); fs.writeFileSync(path.join(tempDir1, 'file1.txt'), 'Content1');
fs.writeFileSync(path.join(tempDir2, 'file2.txt'), 'Content2'); fs.writeFileSync(path.join(tempDir2, 'file2.txt'), 'Content2');
+12 -6
View File
@@ -172,7 +172,7 @@ Signal: Signal number or \`(none)\` if no signal was received.
protected createInvocation( protected createInvocation(
params: ToolParams, params: ToolParams,
_messageBus?: MessageBus, messageBus?: MessageBus,
_toolName?: string, _toolName?: string,
_displayName?: string, _displayName?: string,
): ToolInvocation<ToolParams, ToolResult> { ): ToolInvocation<ToolParams, ToolResult> {
@@ -181,7 +181,7 @@ Signal: Signal number or \`(none)\` if no signal was received.
this.originalName, this.originalName,
this.name, this.name,
params, params,
_messageBus, messageBus,
); );
} }
} }
@@ -194,11 +194,8 @@ export class ToolRegistry {
private config: Config; private config: Config;
private messageBus?: MessageBus; private messageBus?: MessageBus;
constructor(config: Config) { constructor(config: Config, messageBus?: MessageBus) {
this.config = config; this.config = config;
}
setMessageBus(messageBus: MessageBus): void {
this.messageBus = messageBus; this.messageBus = messageBus;
} }
@@ -206,6 +203,15 @@ export class ToolRegistry {
return this.messageBus; return this.messageBus;
} }
/**
* @deprecated migration only - will be removed in PR 3 (Enforcement)
* TODO: DELETE ME in PR 3. This is a temporary shim to allow for soft migration
* of tools while the core infrastructure is updated to require a MessageBus at birth.
*/
setMessageBus(messageBus: MessageBus): void {
this.messageBus = messageBus;
}
/** /**
* Registers a tool definition. * Registers a tool definition.
* *
+7 -1
View File
@@ -41,6 +41,10 @@ import { StandardFileSystemService } from '../services/fileSystemService.js';
import type { DiffUpdateResult } from '../ide/ide-client.js'; import type { DiffUpdateResult } from '../ide/ide-client.js';
import { IdeClient } from '../ide/ide-client.js'; import { IdeClient } from '../ide/ide-client.js';
import { WorkspaceContext } from '../utils/workspaceContext.js'; import { WorkspaceContext } from '../utils/workspaceContext.js';
import {
createMockMessageBus,
getMockMessageBusInstance,
} from '../test-utils/mock-message-bus.js';
const rootDir = path.resolve(os.tmpdir(), 'gemini-cli-test-root'); const rootDir = path.resolve(os.tmpdir(), 'gemini-cli-test-root');
@@ -150,7 +154,9 @@ describe('WriteFileTool', () => {
mockBaseLlmClientInstance, mockBaseLlmClientInstance,
); );
tool = new WriteFileTool(mockConfig); const bus = createMockMessageBus();
getMockMessageBusInstance(bus).defaultToolDecision = 'ask_user';
tool = new WriteFileTool(mockConfig, bus);
// Reset mocks before each test // Reset mocks before each test
mockConfigInternal.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); mockConfigInternal.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
+2 -1
View File
@@ -475,11 +475,12 @@ export class WriteFileTool
protected createInvocation( protected createInvocation(
params: WriteFileToolParams, params: WriteFileToolParams,
messageBus?: MessageBus,
): ToolInvocation<WriteFileToolParams, ToolResult> { ): ToolInvocation<WriteFileToolParams, ToolResult> {
return new WriteFileToolInvocation( return new WriteFileToolInvocation(
this.config, this.config,
params, params,
this.messageBus, messageBus ?? this.messageBus,
this.name, this.name,
this.displayName, this.displayName,
); );
@@ -164,7 +164,10 @@ describe('editCorrector', () => {
const abortSignal = new AbortController().signal; const abortSignal = new AbortController().signal;
beforeEach(() => { beforeEach(() => {
mockToolRegistry = new ToolRegistry({} as Config) as Mocked<ToolRegistry>; mockToolRegistry = new ToolRegistry(
{} as Config,
{} as any,
) as Mocked<ToolRegistry>;
const configParams = { const configParams = {
apiKey: 'test-api-key', apiKey: 'test-api-key',
model: 'test-model', model: 'test-model',
+2 -1
View File
@@ -8,6 +8,7 @@ import { expect, describe, it } from 'vitest';
import { doesToolInvocationMatch, getToolSuggestion } from './tool-utils.js'; import { doesToolInvocationMatch, getToolSuggestion } from './tool-utils.js';
import type { AnyToolInvocation, Config } from '../index.js'; import type { AnyToolInvocation, Config } from '../index.js';
import { ReadFileTool } from '../tools/read-file.js'; import { ReadFileTool } from '../tools/read-file.js';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
describe('getToolSuggestion', () => { describe('getToolSuggestion', () => {
it('should suggest the top N closest tool names for a typo', () => { it('should suggest the top N closest tool names for a typo', () => {
@@ -83,7 +84,7 @@ describe('doesToolInvocationMatch', () => {
}); });
describe('for non-shell tools', () => { describe('for non-shell tools', () => {
const readFileTool = new ReadFileTool({} as Config); const readFileTool = new ReadFileTool({} as Config, createMockMessageBus());
const invocation = { const invocation = {
params: { file: 'test.txt' }, params: { file: 'test.txt' },
} as AnyToolInvocation; } as AnyToolInvocation;