feat(core): Implement parallel FC for read only tools. (#18791)

This commit is contained in:
joshualitt
2026-02-19 16:38:22 -08:00
committed by GitHub
parent f4dfbfbb58
commit b11df9d839
11 changed files with 862 additions and 301 deletions
+11 -1
View File
@@ -247,7 +247,7 @@ export class DiscoveredMCPTool extends BaseDeclarativeTool<
override readonly parameterSchema: unknown,
messageBus: MessageBus,
readonly trust?: boolean,
readonly isReadOnly?: boolean,
isReadOnly?: boolean,
nameOverride?: string,
private readonly cliConfig?: Config,
override readonly extensionName?: string,
@@ -265,6 +265,16 @@ export class DiscoveredMCPTool extends BaseDeclarativeTool<
extensionName,
extensionId,
);
this._isReadOnly = isReadOnly;
}
private readonly _isReadOnly?: boolean;
override get isReadOnly(): boolean {
if (this._isReadOnly !== undefined) {
return this._isReadOnly;
}
return super.isReadOnly;
}
getFullyQualifiedPrefix(): string {
+29
View File
@@ -9,6 +9,8 @@ import type { ToolInvocation, ToolResult } from './tools.js';
import { DeclarativeTool, hasCycleInSchema, Kind } from './tools.js';
import { ToolErrorType } from './tool-error.js';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
import { ReadFileTool } from './read-file.js';
import { makeFakeConfig } from '../test-utils/config.js';
class TestToolInvocation implements ToolInvocation<object, ToolResult> {
constructor(
@@ -238,3 +240,30 @@ describe('hasCycleInSchema', () => {
expect(hasCycleInSchema({})).toBe(false);
});
});
describe('Tools Read-Only property', () => {
it('should have isReadOnly true for ReadFileTool', () => {
const config = makeFakeConfig();
const bus = createMockMessageBus();
const tool = new ReadFileTool(config, bus);
expect(tool.isReadOnly).toBe(true);
});
it('should derive isReadOnly from Kind', () => {
const bus = createMockMessageBus();
class MyTool extends DeclarativeTool<object, ToolResult> {
build(_params: object): ToolInvocation<object, ToolResult> {
throw new Error('Not implemented');
}
}
const mutator = new MyTool('m', 'M', 'd', Kind.Edit, {}, bus);
expect(mutator.isReadOnly).toBe(false);
const reader = new MyTool('r', 'R', 'd', Kind.Read, {}, bus);
expect(reader.isReadOnly).toBe(true);
const searcher = new MyTool('s', 'S', 'd', Kind.Search, {}, bus);
expect(searcher.isReadOnly).toBe(true);
});
});
+16
View File
@@ -333,6 +333,11 @@ export interface ToolBuilder<
*/
canUpdateOutput: boolean;
/**
* Whether the tool is read-only (has no side effects).
*/
isReadOnly: boolean;
/**
* Validates raw parameters and builds a ready-to-execute invocation.
* @param params The raw, untrusted parameters from the model.
@@ -363,6 +368,10 @@ export abstract class DeclarativeTool<
readonly extensionId?: string,
) {}
get isReadOnly(): boolean {
return READ_ONLY_KINDS.includes(this.kind);
}
getSchema(_modelId?: string): FunctionDeclaration {
return {
name: this.name,
@@ -819,6 +828,13 @@ export const MUTATOR_KINDS: Kind[] = [
Kind.Execute,
] as const;
// Function kinds that are safe to run in parallel
export const READ_ONLY_KINDS: Kind[] = [
Kind.Read,
Kind.Search,
Kind.Fetch,
] as const;
export interface ToolLocation {
// Absolute path to the file
path: string;