feat(core): Migrate MockTools to declarative pattern. (#6197)
This commit is contained in:
parent
d2f4e2664e
commit
8f2fa5a537
|
@ -24,7 +24,9 @@ import {
|
||||||
Status as ToolCallStatusType,
|
Status as ToolCallStatusType,
|
||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
Kind,
|
Kind,
|
||||||
BaseTool,
|
BaseDeclarativeTool,
|
||||||
|
BaseToolInvocation,
|
||||||
|
ToolInvocation,
|
||||||
AnyDeclarativeTool,
|
AnyDeclarativeTool,
|
||||||
AnyToolInvocation,
|
AnyToolInvocation,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
|
@ -62,7 +64,41 @@ const mockConfig = {
|
||||||
getDebugMode: () => false,
|
getDebugMode: () => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
class MockTool extends BaseTool<object, ToolResult> {
|
class MockToolInvocation extends BaseToolInvocation<object, ToolResult> {
|
||||||
|
constructor(
|
||||||
|
private readonly tool: MockTool,
|
||||||
|
params: object,
|
||||||
|
) {
|
||||||
|
super(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDescription(): string {
|
||||||
|
return JSON.stringify(this.params);
|
||||||
|
}
|
||||||
|
|
||||||
|
override shouldConfirmExecute(
|
||||||
|
abortSignal: AbortSignal,
|
||||||
|
): Promise<ToolCallConfirmationDetails | false> {
|
||||||
|
return this.tool.shouldConfirmExecute(this.params, abortSignal);
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(
|
||||||
|
signal: AbortSignal,
|
||||||
|
updateOutput?: (output: string) => void,
|
||||||
|
terminalColumns?: number,
|
||||||
|
terminalRows?: number,
|
||||||
|
): Promise<ToolResult> {
|
||||||
|
return this.tool.execute(
|
||||||
|
this.params,
|
||||||
|
signal,
|
||||||
|
updateOutput,
|
||||||
|
terminalColumns,
|
||||||
|
terminalRows,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockTool extends BaseDeclarativeTool<object, ToolResult> {
|
||||||
constructor(
|
constructor(
|
||||||
name: string,
|
name: string,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
|
@ -80,11 +116,12 @@ class MockTool extends BaseTool<object, ToolResult> {
|
||||||
canUpdateOutput,
|
canUpdateOutput,
|
||||||
);
|
);
|
||||||
if (shouldConfirm) {
|
if (shouldConfirm) {
|
||||||
this.shouldConfirmExecute = vi.fn(
|
this.shouldConfirmExecute.mockImplementation(
|
||||||
async (): Promise<ToolCallConfirmationDetails | false> => ({
|
async (): Promise<ToolCallConfirmationDetails | false> => ({
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
title: 'Mock Tool Requires Confirmation',
|
title: 'Mock Tool Requires Confirmation',
|
||||||
onConfirm: mockOnUserConfirmForToolConfirmation,
|
onConfirm: mockOnUserConfirmForToolConfirmation,
|
||||||
|
filePath: 'mock',
|
||||||
fileName: 'mockToolRequiresConfirmation.ts',
|
fileName: 'mockToolRequiresConfirmation.ts',
|
||||||
fileDiff: 'Mock tool requires confirmation',
|
fileDiff: 'Mock tool requires confirmation',
|
||||||
originalContent: 'Original content',
|
originalContent: 'Original content',
|
||||||
|
@ -96,6 +133,12 @@ class MockTool extends BaseTool<object, ToolResult> {
|
||||||
|
|
||||||
execute = vi.fn();
|
execute = vi.fn();
|
||||||
shouldConfirmExecute = vi.fn();
|
shouldConfirmExecute = vi.fn();
|
||||||
|
|
||||||
|
protected createInvocation(
|
||||||
|
params: object,
|
||||||
|
): ToolInvocation<object, ToolResult> {
|
||||||
|
return new MockToolInvocation(this, params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockTool = new MockTool('mockTool', 'Mock Tool');
|
const mockTool = new MockTool('mockTool', 'Mock Tool');
|
||||||
|
@ -142,6 +185,8 @@ describe('useReactToolScheduler in YOLO Mode', () => {
|
||||||
onComplete,
|
onComplete,
|
||||||
mockConfig as unknown as Config,
|
mockConfig as unknown as Config,
|
||||||
setPendingHistoryItem,
|
setPendingHistoryItem,
|
||||||
|
() => undefined,
|
||||||
|
() => {},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -160,7 +205,7 @@ describe('useReactToolScheduler in YOLO Mode', () => {
|
||||||
callId: 'yoloCall',
|
callId: 'yoloCall',
|
||||||
name: 'mockToolRequiresConfirmation',
|
name: 'mockToolRequiresConfirmation',
|
||||||
args: { data: 'any data' },
|
args: { data: 'any data' },
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
schedule(request, new AbortController().signal);
|
schedule(request, new AbortController().signal);
|
||||||
|
@ -270,13 +315,14 @@ describe('useReactToolScheduler', () => {
|
||||||
(
|
(
|
||||||
mockToolRequiresConfirmation.shouldConfirmExecute as Mock
|
mockToolRequiresConfirmation.shouldConfirmExecute as Mock
|
||||||
).mockImplementation(
|
).mockImplementation(
|
||||||
async (): Promise<ToolCallConfirmationDetails | null> => ({
|
async (): Promise<ToolCallConfirmationDetails | null> =>
|
||||||
onConfirm: mockOnUserConfirmForToolConfirmation,
|
({
|
||||||
fileName: 'mockToolRequiresConfirmation.ts',
|
onConfirm: mockOnUserConfirmForToolConfirmation,
|
||||||
fileDiff: 'Mock tool requires confirmation',
|
fileName: 'mockToolRequiresConfirmation.ts',
|
||||||
type: 'edit',
|
fileDiff: 'Mock tool requires confirmation',
|
||||||
title: 'Mock Tool Requires Confirmation',
|
type: 'edit',
|
||||||
}),
|
title: 'Mock Tool Requires Confirmation',
|
||||||
|
}) as any,
|
||||||
);
|
);
|
||||||
|
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
|
@ -293,6 +339,8 @@ describe('useReactToolScheduler', () => {
|
||||||
onComplete,
|
onComplete,
|
||||||
mockConfig as unknown as Config,
|
mockConfig as unknown as Config,
|
||||||
setPendingHistoryItem,
|
setPendingHistoryItem,
|
||||||
|
() => undefined,
|
||||||
|
() => {},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -316,7 +364,7 @@ describe('useReactToolScheduler', () => {
|
||||||
callId: 'call1',
|
callId: 'call1',
|
||||||
name: 'mockTool',
|
name: 'mockTool',
|
||||||
args: { param: 'value' },
|
args: { param: 'value' },
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
schedule(request, new AbortController().signal);
|
schedule(request, new AbortController().signal);
|
||||||
|
@ -365,7 +413,7 @@ describe('useReactToolScheduler', () => {
|
||||||
callId: 'call1',
|
callId: 'call1',
|
||||||
name: 'nonexistentTool',
|
name: 'nonexistentTool',
|
||||||
args: {},
|
args: {},
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
schedule(request, new AbortController().signal);
|
schedule(request, new AbortController().signal);
|
||||||
|
@ -402,7 +450,7 @@ describe('useReactToolScheduler', () => {
|
||||||
callId: 'call1',
|
callId: 'call1',
|
||||||
name: 'mockTool',
|
name: 'mockTool',
|
||||||
args: {},
|
args: {},
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
schedule(request, new AbortController().signal);
|
schedule(request, new AbortController().signal);
|
||||||
|
@ -438,7 +486,7 @@ describe('useReactToolScheduler', () => {
|
||||||
callId: 'call1',
|
callId: 'call1',
|
||||||
name: 'mockTool',
|
name: 'mockTool',
|
||||||
args: {},
|
args: {},
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
schedule(request, new AbortController().signal);
|
schedule(request, new AbortController().signal);
|
||||||
|
@ -480,7 +528,7 @@ describe('useReactToolScheduler', () => {
|
||||||
callId: 'callConfirm',
|
callId: 'callConfirm',
|
||||||
name: 'mockToolRequiresConfirmation',
|
name: 'mockToolRequiresConfirmation',
|
||||||
args: { data: 'sensitive' },
|
args: { data: 'sensitive' },
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
schedule(request, new AbortController().signal);
|
schedule(request, new AbortController().signal);
|
||||||
|
@ -536,7 +584,7 @@ describe('useReactToolScheduler', () => {
|
||||||
callId: 'callConfirmCancel',
|
callId: 'callConfirmCancel',
|
||||||
name: 'mockToolRequiresConfirmation',
|
name: 'mockToolRequiresConfirmation',
|
||||||
args: {},
|
args: {},
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
schedule(request, new AbortController().signal);
|
schedule(request, new AbortController().signal);
|
||||||
|
@ -608,7 +656,7 @@ describe('useReactToolScheduler', () => {
|
||||||
callId: 'liveCall',
|
callId: 'liveCall',
|
||||||
name: 'mockToolWithLiveOutput',
|
name: 'mockToolWithLiveOutput',
|
||||||
args: {},
|
args: {},
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
schedule(request, new AbortController().signal);
|
schedule(request, new AbortController().signal);
|
||||||
|
@ -693,8 +741,8 @@ describe('useReactToolScheduler', () => {
|
||||||
const { result } = renderScheduler();
|
const { result } = renderScheduler();
|
||||||
const schedule = result.current[1];
|
const schedule = result.current[1];
|
||||||
const requests: ToolCallRequestInfo[] = [
|
const requests: ToolCallRequestInfo[] = [
|
||||||
{ callId: 'multi1', name: 'tool1', args: { p: 1 } },
|
{ callId: 'multi1', name: 'tool1', args: { p: 1 } } as any,
|
||||||
{ callId: 'multi2', name: 'tool2', args: { p: 2 } },
|
{ callId: 'multi2', name: 'tool2', args: { p: 2 } } as any,
|
||||||
];
|
];
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
|
@ -777,12 +825,12 @@ describe('useReactToolScheduler', () => {
|
||||||
callId: 'run1',
|
callId: 'run1',
|
||||||
name: 'mockTool',
|
name: 'mockTool',
|
||||||
args: {},
|
args: {},
|
||||||
};
|
} as any;
|
||||||
const request2: ToolCallRequestInfo = {
|
const request2: ToolCallRequestInfo = {
|
||||||
callId: 'run2',
|
callId: 'run2',
|
||||||
name: 'mockTool',
|
name: 'mockTool',
|
||||||
args: {},
|
args: {},
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
schedule(request1, new AbortController().signal);
|
schedule(request1, new AbortController().signal);
|
||||||
|
@ -818,7 +866,7 @@ describe('mapToDisplay', () => {
|
||||||
callId: 'testCallId',
|
callId: 'testCallId',
|
||||||
name: 'testTool',
|
name: 'testTool',
|
||||||
args: { foo: 'bar' },
|
args: { foo: 'bar' },
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
const baseTool = new MockTool('testTool', 'Test Tool Display');
|
const baseTool = new MockTool('testTool', 'Test Tool Display');
|
||||||
|
|
||||||
|
@ -834,9 +882,8 @@ describe('mapToDisplay', () => {
|
||||||
} as PartUnion,
|
} as PartUnion,
|
||||||
],
|
],
|
||||||
resultDisplay: 'Test display output',
|
resultDisplay: 'Test display output',
|
||||||
summary: 'Test summary',
|
|
||||||
error: undefined,
|
error: undefined,
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
// Define a more specific type for extraProps for these tests
|
// Define a more specific type for extraProps for these tests
|
||||||
// This helps ensure that tool and confirmationDetails are only accessed when they are expected to exist.
|
// This helps ensure that tool and confirmationDetails are only accessed when they are expected to exist.
|
||||||
|
@ -882,7 +929,7 @@ describe('mapToDisplay', () => {
|
||||||
extraProps: { tool: baseTool, invocation: baseInvocation },
|
extraProps: { tool: baseTool, invocation: baseInvocation },
|
||||||
expectedStatus: ToolCallStatus.Executing,
|
expectedStatus: ToolCallStatus.Executing,
|
||||||
expectedName: baseTool.displayName,
|
expectedName: baseTool.displayName,
|
||||||
expectedDescription: baseTool.getDescription(baseRequest.args),
|
expectedDescription: baseInvocation.getDescription(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'awaiting_approval',
|
name: 'awaiting_approval',
|
||||||
|
@ -897,6 +944,7 @@ describe('mapToDisplay', () => {
|
||||||
serverName: 'testTool',
|
serverName: 'testTool',
|
||||||
toolName: 'testTool',
|
toolName: 'testTool',
|
||||||
toolDisplayName: 'Test Tool Display',
|
toolDisplayName: 'Test Tool Display',
|
||||||
|
filePath: 'mock',
|
||||||
fileName: 'test.ts',
|
fileName: 'test.ts',
|
||||||
fileDiff: 'Test diff',
|
fileDiff: 'Test diff',
|
||||||
originalContent: 'Original content',
|
originalContent: 'Original content',
|
||||||
|
@ -905,7 +953,7 @@ describe('mapToDisplay', () => {
|
||||||
},
|
},
|
||||||
expectedStatus: ToolCallStatus.Confirming,
|
expectedStatus: ToolCallStatus.Confirming,
|
||||||
expectedName: baseTool.displayName,
|
expectedName: baseTool.displayName,
|
||||||
expectedDescription: baseTool.getDescription(baseRequest.args),
|
expectedDescription: baseInvocation.getDescription(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'scheduled',
|
name: 'scheduled',
|
||||||
|
@ -913,7 +961,7 @@ describe('mapToDisplay', () => {
|
||||||
extraProps: { tool: baseTool, invocation: baseInvocation },
|
extraProps: { tool: baseTool, invocation: baseInvocation },
|
||||||
expectedStatus: ToolCallStatus.Pending,
|
expectedStatus: ToolCallStatus.Pending,
|
||||||
expectedName: baseTool.displayName,
|
expectedName: baseTool.displayName,
|
||||||
expectedDescription: baseTool.getDescription(baseRequest.args),
|
expectedDescription: baseInvocation.getDescription(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'executing no live output',
|
name: 'executing no live output',
|
||||||
|
@ -921,7 +969,7 @@ describe('mapToDisplay', () => {
|
||||||
extraProps: { tool: baseTool, invocation: baseInvocation },
|
extraProps: { tool: baseTool, invocation: baseInvocation },
|
||||||
expectedStatus: ToolCallStatus.Executing,
|
expectedStatus: ToolCallStatus.Executing,
|
||||||
expectedName: baseTool.displayName,
|
expectedName: baseTool.displayName,
|
||||||
expectedDescription: baseTool.getDescription(baseRequest.args),
|
expectedDescription: baseInvocation.getDescription(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'executing with live output',
|
name: 'executing with live output',
|
||||||
|
@ -934,7 +982,7 @@ describe('mapToDisplay', () => {
|
||||||
expectedStatus: ToolCallStatus.Executing,
|
expectedStatus: ToolCallStatus.Executing,
|
||||||
expectedResultDisplay: 'Live test output',
|
expectedResultDisplay: 'Live test output',
|
||||||
expectedName: baseTool.displayName,
|
expectedName: baseTool.displayName,
|
||||||
expectedDescription: baseTool.getDescription(baseRequest.args),
|
expectedDescription: baseInvocation.getDescription(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'success',
|
name: 'success',
|
||||||
|
@ -947,7 +995,7 @@ describe('mapToDisplay', () => {
|
||||||
expectedStatus: ToolCallStatus.Success,
|
expectedStatus: ToolCallStatus.Success,
|
||||||
expectedResultDisplay: baseResponse.resultDisplay as any,
|
expectedResultDisplay: baseResponse.resultDisplay as any,
|
||||||
expectedName: baseTool.displayName,
|
expectedName: baseTool.displayName,
|
||||||
expectedDescription: baseTool.getDescription(baseRequest.args),
|
expectedDescription: baseInvocation.getDescription(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'error tool not found',
|
name: 'error tool not found',
|
||||||
|
@ -978,7 +1026,7 @@ describe('mapToDisplay', () => {
|
||||||
expectedStatus: ToolCallStatus.Error,
|
expectedStatus: ToolCallStatus.Error,
|
||||||
expectedResultDisplay: 'Execution failed display',
|
expectedResultDisplay: 'Execution failed display',
|
||||||
expectedName: baseTool.displayName, // Changed from baseTool.name
|
expectedName: baseTool.displayName, // Changed from baseTool.name
|
||||||
expectedDescription: baseTool.getDescription(baseRequest.args),
|
expectedDescription: baseInvocation.getDescription(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'cancelled',
|
name: 'cancelled',
|
||||||
|
@ -994,7 +1042,7 @@ describe('mapToDisplay', () => {
|
||||||
expectedStatus: ToolCallStatus.Canceled,
|
expectedStatus: ToolCallStatus.Canceled,
|
||||||
expectedResultDisplay: 'Cancelled display',
|
expectedResultDisplay: 'Cancelled display',
|
||||||
expectedName: baseTool.displayName,
|
expectedName: baseTool.displayName,
|
||||||
expectedDescription: baseTool.getDescription(baseRequest.args),
|
expectedDescription: baseInvocation.getDescription(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,6 @@
|
||||||
"src/ui/hooks/useGeminiStream.test.tsx",
|
"src/ui/hooks/useGeminiStream.test.tsx",
|
||||||
"src/ui/hooks/useKeypress.test.ts",
|
"src/ui/hooks/useKeypress.test.ts",
|
||||||
"src/ui/hooks/usePhraseCycler.test.ts",
|
"src/ui/hooks/usePhraseCycler.test.ts",
|
||||||
"src/ui/hooks/useToolScheduler.test.ts",
|
|
||||||
"src/ui/hooks/vim.test.ts",
|
"src/ui/hooks/vim.test.ts",
|
||||||
"src/ui/utils/computeStats.test.ts",
|
"src/ui/utils/computeStats.test.ts",
|
||||||
"src/ui/themes/theme.test.ts",
|
"src/ui/themes/theme.test.ts",
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import {
|
import {
|
||||||
CoreToolScheduler,
|
CoreToolScheduler,
|
||||||
|
@ -12,65 +11,20 @@ import {
|
||||||
convertToFunctionResponse,
|
convertToFunctionResponse,
|
||||||
} from './coreToolScheduler.js';
|
} from './coreToolScheduler.js';
|
||||||
import {
|
import {
|
||||||
BaseTool,
|
BaseDeclarativeTool,
|
||||||
|
BaseToolInvocation,
|
||||||
ToolCallConfirmationDetails,
|
ToolCallConfirmationDetails,
|
||||||
ToolConfirmationOutcome,
|
ToolConfirmationOutcome,
|
||||||
ToolConfirmationPayload,
|
ToolConfirmationPayload,
|
||||||
|
ToolInvocation,
|
||||||
ToolResult,
|
ToolResult,
|
||||||
Config,
|
Config,
|
||||||
Kind,
|
Kind,
|
||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
|
ToolRegistry,
|
||||||
} from '../index.js';
|
} from '../index.js';
|
||||||
import { Part, PartListUnion } from '@google/genai';
|
import { Part, PartListUnion } from '@google/genai';
|
||||||
|
import { MockModifiableTool, MockTool } from '../test-utils/tools.js';
|
||||||
import {
|
|
||||||
ModifiableDeclarativeTool,
|
|
||||||
ModifyContext,
|
|
||||||
} from '../tools/modifiable-tool.js';
|
|
||||||
import { MockTool } from '../test-utils/tools.js';
|
|
||||||
|
|
||||||
class MockModifiableTool
|
|
||||||
extends MockTool
|
|
||||||
implements ModifiableDeclarativeTool<Record<string, unknown>>
|
|
||||||
{
|
|
||||||
constructor(name = 'mockModifiableTool') {
|
|
||||||
super(name);
|
|
||||||
this.shouldConfirm = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
getModifyContext(
|
|
||||||
_abortSignal: AbortSignal,
|
|
||||||
): ModifyContext<Record<string, unknown>> {
|
|
||||||
return {
|
|
||||||
getFilePath: () => 'test.txt',
|
|
||||||
getCurrentContent: async () => 'old content',
|
|
||||||
getProposedContent: async () => 'new content',
|
|
||||||
createUpdatedParams: (
|
|
||||||
_oldContent: string,
|
|
||||||
modifiedProposedContent: string,
|
|
||||||
_originalParams: Record<string, unknown>,
|
|
||||||
) => ({ newContent: modifiedProposedContent }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
override async shouldConfirmExecute(): Promise<
|
|
||||||
ToolCallConfirmationDetails | false
|
|
||||||
> {
|
|
||||||
if (this.shouldConfirm) {
|
|
||||||
return {
|
|
||||||
type: 'edit',
|
|
||||||
title: 'Confirm Mock Tool',
|
|
||||||
fileName: 'test.txt',
|
|
||||||
filePath: 'test.txt',
|
|
||||||
fileDiff: 'diff',
|
|
||||||
originalContent: 'originalContent',
|
|
||||||
newContent: 'newContent',
|
|
||||||
onConfirm: async () => {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('CoreToolScheduler', () => {
|
describe('CoreToolScheduler', () => {
|
||||||
it('should cancel a tool call if the signal is aborted before confirmation', async () => {
|
it('should cancel a tool call if the signal is aborted before confirmation', async () => {
|
||||||
|
@ -81,7 +35,7 @@ describe('CoreToolScheduler', () => {
|
||||||
getTool: () => declarativeTool,
|
getTool: () => declarativeTool,
|
||||||
getFunctionDeclarations: () => [],
|
getFunctionDeclarations: () => [],
|
||||||
tools: new Map(),
|
tools: new Map(),
|
||||||
discovery: {} as any,
|
discovery: {},
|
||||||
registerTool: () => {},
|
registerTool: () => {},
|
||||||
getToolByName: () => declarativeTool,
|
getToolByName: () => declarativeTool,
|
||||||
getToolByDisplayName: () => declarativeTool,
|
getToolByDisplayName: () => declarativeTool,
|
||||||
|
@ -103,7 +57,7 @@ describe('CoreToolScheduler', () => {
|
||||||
|
|
||||||
const scheduler = new CoreToolScheduler({
|
const scheduler = new CoreToolScheduler({
|
||||||
config: mockConfig,
|
config: mockConfig,
|
||||||
toolRegistry: Promise.resolve(toolRegistry as any),
|
toolRegistry: Promise.resolve(toolRegistry as unknown as ToolRegistry),
|
||||||
onAllToolCallsComplete,
|
onAllToolCallsComplete,
|
||||||
onToolCallsUpdate,
|
onToolCallsUpdate,
|
||||||
getPreferredEditor: () => 'vscode',
|
getPreferredEditor: () => 'vscode',
|
||||||
|
@ -123,19 +77,6 @@ describe('CoreToolScheduler', () => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
await scheduler.schedule([request], abortController.signal);
|
await scheduler.schedule([request], abortController.signal);
|
||||||
|
|
||||||
const confirmationDetails = await mockTool.shouldConfirmExecute(
|
|
||||||
{},
|
|
||||||
abortController.signal,
|
|
||||||
);
|
|
||||||
if (confirmationDetails) {
|
|
||||||
await scheduler.handleConfirmationResponse(
|
|
||||||
'1',
|
|
||||||
confirmationDetails.onConfirm,
|
|
||||||
ToolConfirmationOutcome.ProceedOnce,
|
|
||||||
abortController.signal,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(onAllToolCallsComplete).toHaveBeenCalled();
|
expect(onAllToolCallsComplete).toHaveBeenCalled();
|
||||||
const completedCalls = onAllToolCallsComplete.mock
|
const completedCalls = onAllToolCallsComplete.mock
|
||||||
.calls[0][0] as ToolCall[];
|
.calls[0][0] as ToolCall[];
|
||||||
|
@ -151,7 +92,7 @@ describe('CoreToolScheduler with payload', () => {
|
||||||
getTool: () => declarativeTool,
|
getTool: () => declarativeTool,
|
||||||
getFunctionDeclarations: () => [],
|
getFunctionDeclarations: () => [],
|
||||||
tools: new Map(),
|
tools: new Map(),
|
||||||
discovery: {} as any,
|
discovery: {},
|
||||||
registerTool: () => {},
|
registerTool: () => {},
|
||||||
getToolByName: () => declarativeTool,
|
getToolByName: () => declarativeTool,
|
||||||
getToolByDisplayName: () => declarativeTool,
|
getToolByDisplayName: () => declarativeTool,
|
||||||
|
@ -173,7 +114,7 @@ describe('CoreToolScheduler with payload', () => {
|
||||||
|
|
||||||
const scheduler = new CoreToolScheduler({
|
const scheduler = new CoreToolScheduler({
|
||||||
config: mockConfig,
|
config: mockConfig,
|
||||||
toolRegistry: Promise.resolve(toolRegistry as any),
|
toolRegistry: Promise.resolve(toolRegistry as unknown as ToolRegistry),
|
||||||
onAllToolCallsComplete,
|
onAllToolCallsComplete,
|
||||||
onToolCallsUpdate,
|
onToolCallsUpdate,
|
||||||
getPreferredEditor: () => 'vscode',
|
getPreferredEditor: () => 'vscode',
|
||||||
|
@ -192,15 +133,22 @@ describe('CoreToolScheduler with payload', () => {
|
||||||
|
|
||||||
await scheduler.schedule([request], abortController.signal);
|
await scheduler.schedule([request], abortController.signal);
|
||||||
|
|
||||||
const confirmationDetails = await mockTool.shouldConfirmExecute();
|
await vi.waitFor(() => {
|
||||||
|
const awaitingCall = onToolCallsUpdate.mock.calls.find(
|
||||||
|
(call) => call[0][0].status === 'awaiting_approval',
|
||||||
|
)?.[0][0];
|
||||||
|
expect(awaitingCall).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
const awaitingCall = onToolCallsUpdate.mock.calls.find(
|
||||||
|
(call) => call[0][0].status === 'awaiting_approval',
|
||||||
|
)?.[0][0];
|
||||||
|
const confirmationDetails = awaitingCall.confirmationDetails;
|
||||||
|
|
||||||
if (confirmationDetails) {
|
if (confirmationDetails) {
|
||||||
const payload: ToolConfirmationPayload = { newContent: 'final version' };
|
const payload: ToolConfirmationPayload = { newContent: 'final version' };
|
||||||
await scheduler.handleConfirmationResponse(
|
await confirmationDetails.onConfirm(
|
||||||
'1',
|
|
||||||
confirmationDetails.onConfirm,
|
|
||||||
ToolConfirmationOutcome.ProceedOnce,
|
ToolConfirmationOutcome.ProceedOnce,
|
||||||
abortController.signal,
|
|
||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -382,54 +330,66 @@ describe('convertToFunctionResponse', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class MockEditToolInvocation extends BaseToolInvocation<
|
||||||
|
Record<string, unknown>,
|
||||||
|
ToolResult
|
||||||
|
> {
|
||||||
|
constructor(params: Record<string, unknown>) {
|
||||||
|
super(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDescription(): string {
|
||||||
|
return 'A mock edit tool invocation';
|
||||||
|
}
|
||||||
|
|
||||||
|
override async shouldConfirmExecute(
|
||||||
|
_abortSignal: AbortSignal,
|
||||||
|
): Promise<ToolCallConfirmationDetails | false> {
|
||||||
|
return {
|
||||||
|
type: 'edit',
|
||||||
|
title: 'Confirm Edit',
|
||||||
|
fileName: 'test.txt',
|
||||||
|
filePath: 'test.txt',
|
||||||
|
fileDiff:
|
||||||
|
'--- test.txt\n+++ test.txt\n@@ -1,1 +1,1 @@\n-old content\n+new content',
|
||||||
|
originalContent: 'old content',
|
||||||
|
newContent: 'new content',
|
||||||
|
onConfirm: async () => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(_abortSignal: AbortSignal): Promise<ToolResult> {
|
||||||
|
return {
|
||||||
|
llmContent: 'Edited successfully',
|
||||||
|
returnDisplay: 'Edited successfully',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockEditTool extends BaseDeclarativeTool<
|
||||||
|
Record<string, unknown>,
|
||||||
|
ToolResult
|
||||||
|
> {
|
||||||
|
constructor() {
|
||||||
|
super('mockEditTool', 'mockEditTool', 'A mock edit tool', Kind.Edit, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createInvocation(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
): ToolInvocation<Record<string, unknown>, ToolResult> {
|
||||||
|
return new MockEditToolInvocation(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe('CoreToolScheduler edit cancellation', () => {
|
describe('CoreToolScheduler edit cancellation', () => {
|
||||||
it('should preserve diff when an edit is cancelled', async () => {
|
it('should preserve diff when an edit is cancelled', async () => {
|
||||||
class MockEditTool extends BaseTool<Record<string, unknown>, ToolResult> {
|
|
||||||
constructor() {
|
|
||||||
super(
|
|
||||||
'mockEditTool',
|
|
||||||
'mockEditTool',
|
|
||||||
'A mock edit tool',
|
|
||||||
Kind.Edit,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async shouldConfirmExecute(
|
|
||||||
_params: Record<string, unknown>,
|
|
||||||
_abortSignal: AbortSignal,
|
|
||||||
): Promise<ToolCallConfirmationDetails | false> {
|
|
||||||
return {
|
|
||||||
type: 'edit',
|
|
||||||
title: 'Confirm Edit',
|
|
||||||
fileName: 'test.txt',
|
|
||||||
filePath: 'test.txt',
|
|
||||||
fileDiff:
|
|
||||||
'--- test.txt\n+++ test.txt\n@@ -1,1 +1,1 @@\n-old content\n+new content',
|
|
||||||
originalContent: 'old content',
|
|
||||||
newContent: 'new content',
|
|
||||||
onConfirm: async () => {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(
|
|
||||||
_params: Record<string, unknown>,
|
|
||||||
_abortSignal: AbortSignal,
|
|
||||||
): Promise<ToolResult> {
|
|
||||||
return {
|
|
||||||
llmContent: 'Edited successfully',
|
|
||||||
returnDisplay: 'Edited successfully',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockEditTool = new MockEditTool();
|
const mockEditTool = new MockEditTool();
|
||||||
const declarativeTool = mockEditTool;
|
const declarativeTool = mockEditTool;
|
||||||
const toolRegistry = {
|
const toolRegistry = {
|
||||||
getTool: () => declarativeTool,
|
getTool: () => declarativeTool,
|
||||||
getFunctionDeclarations: () => [],
|
getFunctionDeclarations: () => [],
|
||||||
tools: new Map(),
|
tools: new Map(),
|
||||||
discovery: {} as any,
|
discovery: {},
|
||||||
registerTool: () => {},
|
registerTool: () => {},
|
||||||
getToolByName: () => declarativeTool,
|
getToolByName: () => declarativeTool,
|
||||||
getToolByDisplayName: () => declarativeTool,
|
getToolByDisplayName: () => declarativeTool,
|
||||||
|
@ -451,7 +411,7 @@ describe('CoreToolScheduler edit cancellation', () => {
|
||||||
|
|
||||||
const scheduler = new CoreToolScheduler({
|
const scheduler = new CoreToolScheduler({
|
||||||
config: mockConfig,
|
config: mockConfig,
|
||||||
toolRegistry: Promise.resolve(toolRegistry as any),
|
toolRegistry: Promise.resolve(toolRegistry as unknown as ToolRegistry),
|
||||||
onAllToolCallsComplete,
|
onAllToolCallsComplete,
|
||||||
onToolCallsUpdate,
|
onToolCallsUpdate,
|
||||||
getPreferredEditor: () => 'vscode',
|
getPreferredEditor: () => 'vscode',
|
||||||
|
@ -478,17 +438,9 @@ describe('CoreToolScheduler edit cancellation', () => {
|
||||||
expect(awaitingCall).toBeDefined();
|
expect(awaitingCall).toBeDefined();
|
||||||
|
|
||||||
// Cancel the edit
|
// Cancel the edit
|
||||||
const confirmationDetails = await mockEditTool.shouldConfirmExecute(
|
const confirmationDetails = awaitingCall.confirmationDetails;
|
||||||
{},
|
|
||||||
abortController.signal,
|
|
||||||
);
|
|
||||||
if (confirmationDetails) {
|
if (confirmationDetails) {
|
||||||
await scheduler.handleConfirmationResponse(
|
await confirmationDetails.onConfirm(ToolConfirmationOutcome.Cancel);
|
||||||
'1',
|
|
||||||
confirmationDetails.onConfirm,
|
|
||||||
ToolConfirmationOutcome.Cancel,
|
|
||||||
abortController.signal,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(onAllToolCallsComplete).toHaveBeenCalled();
|
expect(onAllToolCallsComplete).toHaveBeenCalled();
|
||||||
|
@ -498,6 +450,7 @@ describe('CoreToolScheduler edit cancellation', () => {
|
||||||
expect(completedCalls[0].status).toBe('cancelled');
|
expect(completedCalls[0].status).toBe('cancelled');
|
||||||
|
|
||||||
// Check that the diff is preserved
|
// Check that the diff is preserved
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const cancelledCall = completedCalls[0] as any;
|
const cancelledCall = completedCalls[0] as any;
|
||||||
expect(cancelledCall.response.resultDisplay).toBeDefined();
|
expect(cancelledCall.response.resultDisplay).toBeDefined();
|
||||||
expect(cancelledCall.response.resultDisplay.fileDiff).toBe(
|
expect(cancelledCall.response.resultDisplay.fileDiff).toBe(
|
||||||
|
@ -525,7 +478,7 @@ describe('CoreToolScheduler YOLO mode', () => {
|
||||||
// Other properties are not needed for this test but are included for type consistency.
|
// Other properties are not needed for this test but are included for type consistency.
|
||||||
getFunctionDeclarations: () => [],
|
getFunctionDeclarations: () => [],
|
||||||
tools: new Map(),
|
tools: new Map(),
|
||||||
discovery: {} as any,
|
discovery: {},
|
||||||
registerTool: () => {},
|
registerTool: () => {},
|
||||||
getToolByDisplayName: () => declarativeTool,
|
getToolByDisplayName: () => declarativeTool,
|
||||||
getTools: () => [],
|
getTools: () => [],
|
||||||
|
@ -547,7 +500,7 @@ describe('CoreToolScheduler YOLO mode', () => {
|
||||||
|
|
||||||
const scheduler = new CoreToolScheduler({
|
const scheduler = new CoreToolScheduler({
|
||||||
config: mockConfig,
|
config: mockConfig,
|
||||||
toolRegistry: Promise.resolve(toolRegistry as any),
|
toolRegistry: Promise.resolve(toolRegistry as unknown as ToolRegistry),
|
||||||
onAllToolCallsComplete,
|
onAllToolCallsComplete,
|
||||||
onToolCallsUpdate,
|
onToolCallsUpdate,
|
||||||
getPreferredEditor: () => 'vscode',
|
getPreferredEditor: () => 'vscode',
|
||||||
|
@ -612,7 +565,7 @@ describe('CoreToolScheduler request queueing', () => {
|
||||||
getToolByName: () => declarativeTool,
|
getToolByName: () => declarativeTool,
|
||||||
getFunctionDeclarations: () => [],
|
getFunctionDeclarations: () => [],
|
||||||
tools: new Map(),
|
tools: new Map(),
|
||||||
discovery: {} as any,
|
discovery: {},
|
||||||
registerTool: () => {},
|
registerTool: () => {},
|
||||||
getToolByDisplayName: () => declarativeTool,
|
getToolByDisplayName: () => declarativeTool,
|
||||||
getTools: () => [],
|
getTools: () => [],
|
||||||
|
@ -633,7 +586,7 @@ describe('CoreToolScheduler request queueing', () => {
|
||||||
|
|
||||||
const scheduler = new CoreToolScheduler({
|
const scheduler = new CoreToolScheduler({
|
||||||
config: mockConfig,
|
config: mockConfig,
|
||||||
toolRegistry: Promise.resolve(toolRegistry as any),
|
toolRegistry: Promise.resolve(toolRegistry as unknown as ToolRegistry),
|
||||||
onAllToolCallsComplete,
|
onAllToolCallsComplete,
|
||||||
onToolCallsUpdate,
|
onToolCallsUpdate,
|
||||||
getPreferredEditor: () => 'vscode',
|
getPreferredEditor: () => 'vscode',
|
||||||
|
@ -722,7 +675,7 @@ describe('CoreToolScheduler request queueing', () => {
|
||||||
getToolByName: () => declarativeTool,
|
getToolByName: () => declarativeTool,
|
||||||
getFunctionDeclarations: () => [],
|
getFunctionDeclarations: () => [],
|
||||||
tools: new Map(),
|
tools: new Map(),
|
||||||
discovery: {} as any,
|
discovery: {},
|
||||||
registerTool: () => {},
|
registerTool: () => {},
|
||||||
getToolByDisplayName: () => declarativeTool,
|
getToolByDisplayName: () => declarativeTool,
|
||||||
getTools: () => [],
|
getTools: () => [],
|
||||||
|
@ -743,7 +696,7 @@ describe('CoreToolScheduler request queueing', () => {
|
||||||
|
|
||||||
const scheduler = new CoreToolScheduler({
|
const scheduler = new CoreToolScheduler({
|
||||||
config: mockConfig,
|
config: mockConfig,
|
||||||
toolRegistry: Promise.resolve(toolRegistry as any),
|
toolRegistry: Promise.resolve(toolRegistry as unknown as ToolRegistry),
|
||||||
onAllToolCallsComplete,
|
onAllToolCallsComplete,
|
||||||
onToolCallsUpdate,
|
onToolCallsUpdate,
|
||||||
getPreferredEditor: () => 'vscode',
|
getPreferredEditor: () => 'vscode',
|
||||||
|
|
|
@ -594,6 +594,14 @@ export class CoreToolScheduler {
|
||||||
const { request: reqInfo, invocation } = toolCall;
|
const { request: reqInfo, invocation } = toolCall;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (signal.aborted) {
|
||||||
|
this.setStatusInternal(
|
||||||
|
reqInfo.callId,
|
||||||
|
'cancelled',
|
||||||
|
'Tool call cancelled by user.',
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (this.config.getApprovalMode() === ApprovalMode.YOLO) {
|
if (this.config.getApprovalMode() === ApprovalMode.YOLO) {
|
||||||
this.setToolCallOutcome(
|
this.setToolCallOutcome(
|
||||||
reqInfo.callId,
|
reqInfo.callId,
|
||||||
|
|
|
@ -6,17 +6,67 @@
|
||||||
|
|
||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
import {
|
import {
|
||||||
BaseTool,
|
BaseDeclarativeTool,
|
||||||
|
BaseToolInvocation,
|
||||||
ToolCallConfirmationDetails,
|
ToolCallConfirmationDetails,
|
||||||
|
ToolInvocation,
|
||||||
ToolResult,
|
ToolResult,
|
||||||
Kind,
|
Kind,
|
||||||
} from '../tools/tools.js';
|
} from '../tools/tools.js';
|
||||||
import { Schema, Type } from '@google/genai';
|
import { Schema, Type } from '@google/genai';
|
||||||
|
import {
|
||||||
|
ModifiableDeclarativeTool,
|
||||||
|
ModifyContext,
|
||||||
|
} from '../tools/modifiable-tool.js';
|
||||||
|
|
||||||
|
class MockToolInvocation extends BaseToolInvocation<
|
||||||
|
{ [key: string]: unknown },
|
||||||
|
ToolResult
|
||||||
|
> {
|
||||||
|
constructor(
|
||||||
|
private readonly tool: MockTool,
|
||||||
|
params: { [key: string]: unknown },
|
||||||
|
) {
|
||||||
|
super(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(_abortSignal: AbortSignal): Promise<ToolResult> {
|
||||||
|
const result = this.tool.executeFn(this.params);
|
||||||
|
return (
|
||||||
|
result ?? {
|
||||||
|
llmContent: `Tool ${this.tool.name} executed successfully.`,
|
||||||
|
returnDisplay: `Tool ${this.tool.name} executed successfully.`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async shouldConfirmExecute(
|
||||||
|
_abortSignal: AbortSignal,
|
||||||
|
): Promise<ToolCallConfirmationDetails | false> {
|
||||||
|
if (this.tool.shouldConfirm) {
|
||||||
|
return {
|
||||||
|
type: 'exec' as const,
|
||||||
|
title: `Confirm ${this.tool.displayName}`,
|
||||||
|
command: this.tool.name,
|
||||||
|
rootCommand: this.tool.name,
|
||||||
|
onConfirm: async () => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDescription(): string {
|
||||||
|
return `A mock tool invocation for ${this.tool.name}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A highly configurable mock tool for testing purposes.
|
* A highly configurable mock tool for testing purposes.
|
||||||
*/
|
*/
|
||||||
export class MockTool extends BaseTool<{ [key: string]: unknown }, ToolResult> {
|
export class MockTool extends BaseDeclarativeTool<
|
||||||
|
{ [key: string]: unknown },
|
||||||
|
ToolResult
|
||||||
|
> {
|
||||||
executeFn = vi.fn();
|
executeFn = vi.fn();
|
||||||
shouldConfirm = false;
|
shouldConfirm = false;
|
||||||
|
|
||||||
|
@ -32,32 +82,87 @@ export class MockTool extends BaseTool<{ [key: string]: unknown }, ToolResult> {
|
||||||
super(name, displayName ?? name, description, Kind.Other, params);
|
super(name, displayName ?? name, description, Kind.Other, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(
|
protected createInvocation(params: {
|
||||||
params: { [key: string]: unknown },
|
[key: string]: unknown;
|
||||||
_abortSignal: AbortSignal,
|
}): ToolInvocation<{ [key: string]: unknown }, ToolResult> {
|
||||||
): Promise<ToolResult> {
|
return new MockToolInvocation(this, params);
|
||||||
const result = this.executeFn(params);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MockModifiableToolInvocation extends BaseToolInvocation<
|
||||||
|
Record<string, unknown>,
|
||||||
|
ToolResult
|
||||||
|
> {
|
||||||
|
constructor(
|
||||||
|
private readonly tool: MockModifiableTool,
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
super(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(_abortSignal: AbortSignal): Promise<ToolResult> {
|
||||||
|
const result = this.tool.executeFn(this.params);
|
||||||
return (
|
return (
|
||||||
result ?? {
|
result ?? {
|
||||||
llmContent: `Tool ${this.name} executed successfully.`,
|
llmContent: `Tool ${this.tool.name} executed successfully.`,
|
||||||
returnDisplay: `Tool ${this.name} executed successfully.`,
|
returnDisplay: `Tool ${this.tool.name} executed successfully.`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
override async shouldConfirmExecute(
|
override async shouldConfirmExecute(
|
||||||
_params: { [key: string]: unknown },
|
|
||||||
_abortSignal: AbortSignal,
|
_abortSignal: AbortSignal,
|
||||||
): Promise<ToolCallConfirmationDetails | false> {
|
): Promise<ToolCallConfirmationDetails | false> {
|
||||||
if (this.shouldConfirm) {
|
if (this.tool.shouldConfirm) {
|
||||||
return {
|
return {
|
||||||
type: 'exec' as const,
|
type: 'edit',
|
||||||
title: `Confirm ${this.displayName}`,
|
title: 'Confirm Mock Tool',
|
||||||
command: this.name,
|
fileName: 'test.txt',
|
||||||
rootCommand: this.name,
|
filePath: 'test.txt',
|
||||||
|
fileDiff: 'diff',
|
||||||
|
originalContent: 'originalContent',
|
||||||
|
newContent: 'newContent',
|
||||||
onConfirm: async () => {},
|
onConfirm: async () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDescription(): string {
|
||||||
|
return `A mock modifiable tool invocation for ${this.tool.name}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configurable mock modifiable tool for testing.
|
||||||
|
*/
|
||||||
|
export class MockModifiableTool
|
||||||
|
extends MockTool
|
||||||
|
implements ModifiableDeclarativeTool<Record<string, unknown>>
|
||||||
|
{
|
||||||
|
constructor(name = 'mockModifiableTool') {
|
||||||
|
super(name);
|
||||||
|
this.shouldConfirm = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getModifyContext(
|
||||||
|
_abortSignal: AbortSignal,
|
||||||
|
): ModifyContext<Record<string, unknown>> {
|
||||||
|
return {
|
||||||
|
getFilePath: () => 'test.txt',
|
||||||
|
getCurrentContent: async () => 'old content',
|
||||||
|
getProposedContent: async () => 'new content',
|
||||||
|
createUpdatedParams: (
|
||||||
|
_oldContent: string,
|
||||||
|
modifiedProposedContent: string,
|
||||||
|
_originalParams: Record<string, unknown>,
|
||||||
|
) => ({ newContent: modifiedProposedContent }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override createInvocation(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
): ToolInvocation<Record<string, unknown>, ToolResult> {
|
||||||
|
return new MockModifiableToolInvocation(this, params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,50 +90,6 @@ export abstract class BaseToolInvocation<
|
||||||
*/
|
*/
|
||||||
export type AnyToolInvocation = ToolInvocation<object, ToolResult>;
|
export type AnyToolInvocation = ToolInvocation<object, ToolResult>;
|
||||||
|
|
||||||
/**
|
|
||||||
* An adapter that wraps the legacy `Tool` interface to make it compatible
|
|
||||||
* with the new `ToolInvocation` pattern.
|
|
||||||
*/
|
|
||||||
export class LegacyToolInvocation<
|
|
||||||
TParams extends object,
|
|
||||||
TResult extends ToolResult,
|
|
||||||
> implements ToolInvocation<TParams, TResult>
|
|
||||||
{
|
|
||||||
constructor(
|
|
||||||
private readonly legacyTool: BaseTool<TParams, TResult>,
|
|
||||||
readonly params: TParams,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
getDescription(): string {
|
|
||||||
return this.legacyTool.getDescription(this.params);
|
|
||||||
}
|
|
||||||
|
|
||||||
toolLocations(): ToolLocation[] {
|
|
||||||
return this.legacyTool.toolLocations(this.params);
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldConfirmExecute(
|
|
||||||
abortSignal: AbortSignal,
|
|
||||||
): Promise<ToolCallConfirmationDetails | false> {
|
|
||||||
return this.legacyTool.shouldConfirmExecute(this.params, abortSignal);
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(
|
|
||||||
signal: AbortSignal,
|
|
||||||
updateOutput?: (output: string) => void,
|
|
||||||
terminalColumns?: number,
|
|
||||||
terminalRows?: number,
|
|
||||||
): Promise<TResult> {
|
|
||||||
return this.legacyTool.execute(
|
|
||||||
this.params,
|
|
||||||
signal,
|
|
||||||
updateOutput,
|
|
||||||
terminalColumns,
|
|
||||||
terminalRows,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for a tool builder that validates parameters and creates invocations.
|
* Interface for a tool builder that validates parameters and creates invocations.
|
||||||
*/
|
*/
|
||||||
|
@ -285,118 +241,6 @@ export abstract class BaseDeclarativeTool<
|
||||||
*/
|
*/
|
||||||
export type AnyDeclarativeTool = DeclarativeTool<object, ToolResult>;
|
export type AnyDeclarativeTool = DeclarativeTool<object, ToolResult>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Base implementation for tools with common functionality
|
|
||||||
* @deprecated Use `DeclarativeTool` for new tools.
|
|
||||||
*/
|
|
||||||
export abstract class BaseTool<
|
|
||||||
TParams extends object,
|
|
||||||
TResult extends ToolResult = ToolResult,
|
|
||||||
> extends DeclarativeTool<TParams, TResult> {
|
|
||||||
/**
|
|
||||||
* Creates a new instance of BaseTool
|
|
||||||
* @param name Internal name of the tool (used for API calls)
|
|
||||||
* @param displayName User-friendly display name of the tool
|
|
||||||
* @param description Description of what the tool does
|
|
||||||
* @param isOutputMarkdown Whether the tool's output should be rendered as markdown
|
|
||||||
* @param canUpdateOutput Whether the tool supports live (streaming) output
|
|
||||||
* @param parameterSchema JSON Schema defining the parameters
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
override readonly name: string,
|
|
||||||
override readonly displayName: string,
|
|
||||||
override readonly description: string,
|
|
||||||
override readonly kind: Kind,
|
|
||||||
override readonly parameterSchema: unknown,
|
|
||||||
override readonly isOutputMarkdown: boolean = true,
|
|
||||||
override readonly canUpdateOutput: boolean = false,
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
name,
|
|
||||||
displayName,
|
|
||||||
description,
|
|
||||||
kind,
|
|
||||||
parameterSchema,
|
|
||||||
isOutputMarkdown,
|
|
||||||
canUpdateOutput,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
build(params: TParams): ToolInvocation<TParams, TResult> {
|
|
||||||
const validationError = this.validateToolParams(params);
|
|
||||||
if (validationError) {
|
|
||||||
throw new Error(validationError);
|
|
||||||
}
|
|
||||||
return new LegacyToolInvocation(this, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the parameters for the tool
|
|
||||||
* This is a placeholder implementation and should be overridden
|
|
||||||
* Should be called from both `shouldConfirmExecute` and `execute`
|
|
||||||
* `shouldConfirmExecute` should return false immediately if invalid
|
|
||||||
* @param params Parameters to validate
|
|
||||||
* @returns An error message string if invalid, null otherwise
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
override validateToolParams(params: TParams): string | null {
|
|
||||||
// Implementation would typically use a JSON Schema validator
|
|
||||||
// This is a placeholder that should be implemented by derived classes
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a pre-execution description of the tool operation
|
|
||||||
* Default implementation that should be overridden by derived classes
|
|
||||||
* @param params Parameters for the tool execution
|
|
||||||
* @returns A markdown string describing what the tool will do
|
|
||||||
*/
|
|
||||||
getDescription(params: TParams): string {
|
|
||||||
return JSON.stringify(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the tool should prompt for confirmation before execution
|
|
||||||
* @param params Parameters for the tool execution
|
|
||||||
* @returns Whether or not execute should be confirmed by the user.
|
|
||||||
*/
|
|
||||||
shouldConfirmExecute(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
params: TParams,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
abortSignal: AbortSignal,
|
|
||||||
): Promise<ToolCallConfirmationDetails | false> {
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines what file system paths the tool will affect
|
|
||||||
* @param params Parameters for the tool execution
|
|
||||||
* @returns A list of such paths
|
|
||||||
*/
|
|
||||||
toolLocations(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
params: TParams,
|
|
||||||
): ToolLocation[] {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract method to execute the tool with the given parameters
|
|
||||||
* Must be implemented by derived classes
|
|
||||||
* @param params Parameters for the tool execution
|
|
||||||
* @param signal AbortSignal for tool cancellation
|
|
||||||
* @returns Result of the tool execution
|
|
||||||
*/
|
|
||||||
abstract execute(
|
|
||||||
params: TParams,
|
|
||||||
signal: AbortSignal,
|
|
||||||
updateOutput?: (output: string) => void,
|
|
||||||
terminalColumns?: number,
|
|
||||||
terminalRows?: number,
|
|
||||||
): Promise<TResult>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolResult {
|
export interface ToolResult {
|
||||||
/**
|
/**
|
||||||
* A short, one-line summary of the tool's action and result.
|
* A short, one-line summary of the tool's action and result.
|
||||||
|
|
Loading…
Reference in New Issue