feat(core): Migrate read_many_files, shell, and web_fetch. (#6167)

This commit is contained in:
joshualitt 2025-08-13 12:27:09 -07:00 committed by GitHub
parent 904f4623b6
commit c0c0e9b7a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 503 additions and 464 deletions

View file

@ -6,14 +6,16 @@
import { SchemaValidator } from '../utils/schemaValidator.js';
import {
BaseTool,
ToolResult,
BaseDeclarativeTool,
BaseToolInvocation,
Kind,
ToolCallConfirmationDetails,
ToolConfirmationOutcome,
Kind,
ToolInvocation,
ToolResult,
} from './tools.js';
import { getErrorMessage } from '../utils/errors.js';
import { Config, ApprovalMode } from '../config/config.js';
import { ApprovalMode, Config } from '../config/config.js';
import { getResponseText } from '../utils/generateContentResponseUtilities.js';
import { fetchWithTimeout, isPrivateIp } from '../utils/fetch.js';
import { convert } from 'html-to-text';
@ -59,41 +61,19 @@ export interface WebFetchToolParams {
prompt: string;
}
/**
* Implementation of the WebFetch tool logic
*/
export class WebFetchTool extends BaseTool<WebFetchToolParams, ToolResult> {
static readonly Name: string = 'web_fetch';
constructor(private readonly config: Config) {
super(
WebFetchTool.Name,
'WebFetch',
"Processes content from URL(s), including local and private network addresses (e.g., localhost), embedded in a prompt. Include up to 20 URLs and instructions (e.g., summarize, extract specific data) directly in the 'prompt' parameter.",
Kind.Fetch,
{
properties: {
prompt: {
description:
'A comprehensive prompt that includes the URL(s) (up to 20) to fetch and specific instructions on how to process their content (e.g., "Summarize https://example.com/article and extract key points from https://another.com/data"). Must contain as least one URL starting with http:// or https://.',
type: 'string',
},
},
required: ['prompt'],
type: 'object',
},
);
const proxy = config.getProxy();
if (proxy) {
setGlobalDispatcher(new ProxyAgent(proxy as string));
}
class WebFetchToolInvocation extends BaseToolInvocation<
WebFetchToolParams,
ToolResult
> {
constructor(
private readonly config: Config,
params: WebFetchToolParams,
) {
super(params);
}
private async executeFallback(
params: WebFetchToolParams,
signal: AbortSignal,
): Promise<ToolResult> {
const urls = extractUrls(params.prompt);
private async executeFallback(signal: AbortSignal): Promise<ToolResult> {
const urls = extractUrls(this.params.prompt);
if (urls.length === 0) {
return {
llmContent: 'Error: No URL found in the prompt for fallback.',
@ -127,13 +107,14 @@ export class WebFetchTool extends BaseTool<WebFetchToolParams, ToolResult> {
}).substring(0, MAX_CONTENT_LENGTH);
const geminiClient = this.config.getGeminiClient();
const fallbackPrompt = `The user requested the following: "${params.prompt}".
const fallbackPrompt = `The user requested the following: "${this.params.prompt}".
I was unable to access the URL directly. Instead, I have fetched the raw content of the page. Please use the following content to answer the user's request. Do not attempt to access the URL again.
I was unable to access the URL directly. Instead, I have fetched the raw content of the page. Please use the following content to answer the request. Do not attempt to access the URL again.
---
${textContent}
---`;
---
`;
const result = await geminiClient.generateContent(
[{ role: 'user', parts: [{ text: fallbackPrompt }] }],
{},
@ -154,49 +135,22 @@ ${textContent}
}
}
validateParams(params: WebFetchToolParams): string | null {
const errors = SchemaValidator.validate(
this.schema.parametersJsonSchema,
params,
);
if (errors) {
return errors;
}
if (!params.prompt || params.prompt.trim() === '') {
return "The 'prompt' parameter cannot be empty and must contain URL(s) and instructions.";
}
if (
!params.prompt.includes('http://') &&
!params.prompt.includes('https://')
) {
return "The 'prompt' must contain at least one valid URL (starting with http:// or https://).";
}
return null;
}
getDescription(params: WebFetchToolParams): string {
getDescription(): string {
const displayPrompt =
params.prompt.length > 100
? params.prompt.substring(0, 97) + '...'
: params.prompt;
this.params.prompt.length > 100
? this.params.prompt.substring(0, 97) + '...'
: this.params.prompt;
return `Processing URLs and instructions from prompt: "${displayPrompt}"`;
}
async shouldConfirmExecute(
params: WebFetchToolParams,
): Promise<ToolCallConfirmationDetails | false> {
async shouldConfirmExecute(): Promise<ToolCallConfirmationDetails | false> {
if (this.config.getApprovalMode() === ApprovalMode.AUTO_EDIT) {
return false;
}
const validationError = this.validateParams(params);
if (validationError) {
return false;
}
// Perform GitHub URL conversion here to differentiate between user-provided
// URL and the actual URL to be fetched.
const urls = extractUrls(params.prompt).map((url) => {
const urls = extractUrls(this.params.prompt).map((url) => {
if (url.includes('github.com') && url.includes('/blob/')) {
return url
.replace('github.com', 'raw.githubusercontent.com')
@ -208,7 +162,7 @@ ${textContent}
const confirmationDetails: ToolCallConfirmationDetails = {
type: 'info',
title: `Confirm Web Fetch`,
prompt: params.prompt,
prompt: this.params.prompt,
urls,
onConfirm: async (outcome: ToolConfirmationOutcome) => {
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
@ -219,25 +173,14 @@ ${textContent}
return confirmationDetails;
}
async execute(
params: WebFetchToolParams,
signal: AbortSignal,
): Promise<ToolResult> {
const validationError = this.validateParams(params);
if (validationError) {
return {
llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`,
returnDisplay: validationError,
};
}
const userPrompt = params.prompt;
async execute(signal: AbortSignal): Promise<ToolResult> {
const userPrompt = this.params.prompt;
const urls = extractUrls(userPrompt);
const url = urls[0];
const isPrivate = isPrivateIp(url);
if (isPrivate) {
return this.executeFallback(params, signal);
return this.executeFallback(signal);
}
const geminiClient = this.config.getGeminiClient();
@ -295,7 +238,7 @@ ${textContent}
}
if (processingError) {
return this.executeFallback(params, signal);
return this.executeFallback(signal);
}
const sourceListFormatted: string[] = [];
@ -360,3 +303,63 @@ ${sourceListFormatted.join('\n')}`;
}
}
}
/**
* Implementation of the WebFetch tool logic
*/
export class WebFetchTool extends BaseDeclarativeTool<
WebFetchToolParams,
ToolResult
> {
static readonly Name: string = 'web_fetch';
constructor(private readonly config: Config) {
super(
WebFetchTool.Name,
'WebFetch',
"Processes content from URL(s), including local and private network addresses (e.g., localhost), embedded in a prompt. Include up to 20 URLs and instructions (e.g., summarize, extract specific data) directly in the 'prompt' parameter.",
Kind.Fetch,
{
properties: {
prompt: {
description:
'A comprehensive prompt that includes the URL(s) (up to 20) to fetch and specific instructions on how to process their content (e.g., "Summarize https://example.com/article and extract key points from https://another.com/data"). Must contain as least one URL starting with http:// or https://.',
type: 'string',
},
},
required: ['prompt'],
type: 'object',
},
);
const proxy = config.getProxy();
if (proxy) {
setGlobalDispatcher(new ProxyAgent(proxy as string));
}
}
protected validateToolParams(params: WebFetchToolParams): string | null {
const errors = SchemaValidator.validate(
this.schema.parametersJsonSchema,
params,
);
if (errors) {
return errors;
}
if (!params.prompt || params.prompt.trim() === '') {
return "The 'prompt' parameter cannot be empty and must contain URL(s) and instructions.";
}
if (
!params.prompt.includes('http://') &&
!params.prompt.includes('https://')
) {
return "The 'prompt' must contain at least one valid URL (starting with http:// or https://).";
}
return null;
}
protected createInvocation(
params: WebFetchToolParams,
): ToolInvocation<WebFetchToolParams, ToolResult> {
return new WebFetchToolInvocation(this.config, params);
}
}