From 6bdad1328e342b3c624efa4db8ec1db0145c0543 Mon Sep 17 00:00:00 2001 From: qer Date: Wed, 8 Jul 2026 20:40:31 +0800 Subject: [PATCH] chore: webSearch & FetchUrl Sync #1260 --- .gitignore | 1 + .../providers/moonshot-web-search.ts | 10 +----- .../app/auth/webSearch/tools/web-search.ts | 34 +------------------ .../src/app/web/tools/fetch-url.ts | 17 ++++++---- .../test/app/web/fetch-url.test.ts | 29 ++++++++++++++++ packages/agent-core-v2/test/auth/auth.test.ts | 3 +- 6 files changed, 45 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index eea4de894..4bd389b82 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ plugins/cdn/ .worktrees/ .kimi-code/local.toml .kimi-sandbox/ +.vscode/ Dockerfile docker-compose.yml diff --git a/packages/agent-core-v2/src/app/auth/webSearch/providers/moonshot-web-search.ts b/packages/agent-core-v2/src/app/auth/webSearch/providers/moonshot-web-search.ts index 44dcefb53..9edb19fee 100644 --- a/packages/agent-core-v2/src/app/auth/webSearch/providers/moonshot-web-search.ts +++ b/packages/agent-core-v2/src/app/auth/webSearch/providers/moonshot-web-search.ts @@ -48,18 +48,11 @@ export class MoonshotWebSearchProvider implements WebSearchProvider { async search( query: string, options?: { - limit?: number; - includeContent?: boolean; toolCallId?: string; signal?: AbortSignal; }, ): Promise { - const body = { - text_query: query, - limit: options?.limit ?? 5, - enable_page_crawling: options?.includeContent ?? false, - timeout_seconds: 30, - }; + const body = { text_query: query }; const bodyJson = JSON.stringify(body); const toolCallId = options?.toolCallId; @@ -90,7 +83,6 @@ export class MoonshotWebSearchProvider implements WebSearchProvider { }; if (typeof r.date === 'string' && r.date.length > 0) out.date = r.date; if (typeof r.site_name === 'string' && r.site_name.length > 0) out.siteName = r.site_name; - if (typeof r.content === 'string' && r.content.length > 0) out.content = r.content; return out; }); } diff --git a/packages/agent-core-v2/src/app/auth/webSearch/tools/web-search.ts b/packages/agent-core-v2/src/app/auth/webSearch/tools/web-search.ts index 7b1e2e2c6..54dd2602d 100644 --- a/packages/agent-core-v2/src/app/auth/webSearch/tools/web-search.ts +++ b/packages/agent-core-v2/src/app/auth/webSearch/tools/web-search.ts @@ -36,15 +36,12 @@ export interface WebSearchResult { snippet: string; date?: string; siteName?: string; - content?: string; } export interface WebSearchProvider { search( query: string, options?: { - limit?: number; - includeContent?: boolean; toolCallId?: string; signal?: AbortSignal; }, @@ -55,23 +52,6 @@ export interface WebSearchProvider { export const WebSearchInputSchema = z.object({ query: z.string().describe('The query text to search for.'), - limit: z - .number() - .int() - .min(1) - .max(20) - .default(5) - .describe( - 'The number of results to return. Typically you do not need to set this value. When the results do not contain what you need, you probably want to give a more concrete query.', - ) - .optional(), - include_content: z - .boolean() - .default(false) - .describe( - 'Whether to include the content of the web pages in the results. It can consume a large amount of tokens when this is set to true. You should avoid enabling this when `limit` is set to a large value.', - ) - .optional(), }); export type WebSearchInput = z.infer; @@ -102,18 +82,7 @@ export class WebSearchTool implements BuiltinTool { { toolCallId, signal }: ExecutableToolContext, ): Promise { try { - const opts: { - limit?: number; - includeContent?: boolean; - toolCallId?: string; - signal?: AbortSignal; - } = { - toolCallId, - signal, - }; - if (args.limit !== undefined) opts.limit = args.limit; - if (args.include_content !== undefined) opts.includeContent = args.include_content; - const results = await this.provider.search(args.query, opts); + const results = await this.provider.search(args.query, { toolCallId, signal }); const builder = new ToolResultBuilder({ maxLineLength: null }); if (results.length === 0) { @@ -131,7 +100,6 @@ export class WebSearchTool implements BuiltinTool { if (result.date) builder.write(`Date: ${result.date}\n`); builder.write(`URL: ${result.url}\n`); builder.write(`Snippet: ${result.snippet}\n\n`); - if (result.content) builder.write(`${result.content}\n\n`); } // Keep the citation reminder next to the data (not just in the static tool diff --git a/packages/agent-core-v2/src/app/web/tools/fetch-url.ts b/packages/agent-core-v2/src/app/web/tools/fetch-url.ts index 0c67db348..88c1b3207 100644 --- a/packages/agent-core-v2/src/app/web/tools/fetch-url.ts +++ b/packages/agent-core-v2/src/app/web/tools/fetch-url.ts @@ -70,15 +70,20 @@ export class FetchURLTool implements BuiltinTool { } const builder = new ToolResultBuilder({ maxLineLength: null }); - builder.write(content); - // Tell the LLM whether it received the whole body or only the - // extracted article text, so it can judge how complete the - // content is. - const message = + // Tell the LLM whether it received the whole body or only the extracted + // article text, so it can judge how complete the content is, and remind it + // to cite this page when it uses the content. Both notes must ride in + // `output`: the result's `message` field is dropped from the transcript, so + // `output` is the only place the model can read them. Put them at the front + // so they survive any downstream truncation of the body. + const note = kind === 'passthrough' ? 'The returned content is the full response body, returned verbatim.' : 'The returned content is the main text extracted from the page.'; - return builder.ok(message); + const citeReminder = + 'If you use it in your answer, cite this page as a markdown link, e.g. [title](url).'; + builder.write(`${note} ${citeReminder}\n\n${content}`); + return builder.ok(); } catch (error) { // An in-flight abort rejects the signal-aware fetch promptly. Re-throw // so the executor can classify it (including user cancellation) and diff --git a/packages/agent-core-v2/test/app/web/fetch-url.test.ts b/packages/agent-core-v2/test/app/web/fetch-url.test.ts index 9d85d2b81..a311068b8 100644 --- a/packages/agent-core-v2/test/app/web/fetch-url.test.ts +++ b/packages/agent-core-v2/test/app/web/fetch-url.test.ts @@ -78,6 +78,35 @@ describe('FetchURLTool abort signal', () => { }); }); +describe('FetchURLTool output note', () => { + async function runKind(kind: UrlFetchResult['kind']): Promise { + const fetch = vi + .fn() + .mockResolvedValue({ content: 'BODY', kind } satisfies UrlFetchResult); + const tool = new FetchURLTool({ fetch }); + const result = await execute(tool, 'https://example.com', new AbortController().signal); + expect(result.isError).toBe(false); + if (typeof result.output !== 'string') throw new Error('expected string output'); + return result.output; + } + + it('puts the passthrough note and citation reminder at the front of output', async () => { + const output = await runKind('passthrough'); + expect(output).toBe( + 'The returned content is the full response body, returned verbatim. ' + + 'If you use it in your answer, cite this page as a markdown link, e.g. [title](url).\n\nBODY', + ); + }); + + it('puts the extracted note and citation reminder at the front of output', async () => { + const output = await runKind('extracted'); + expect(output).toBe( + 'The returned content is the main text extracted from the page. ' + + 'If you use it in your answer, cite this page as a markdown link, e.g. [title](url).\n\nBODY', + ); + }); +}); + describe('LocalFetchURLProvider abort signal', () => { it('passes the signal through to fetchImpl', async () => { const controller = new AbortController(); diff --git a/packages/agent-core-v2/test/auth/auth.test.ts b/packages/agent-core-v2/test/auth/auth.test.ts index d432f0e2a..5dcba0210 100644 --- a/packages/agent-core-v2/test/auth/auth.test.ts +++ b/packages/agent-core-v2/test/auth/auth.test.ts @@ -678,7 +678,7 @@ describe('WebSearchProviderService', () => { const provider = createService().getWebSearchProvider(); expect(provider).not.toBeUndefined(); - const results = await provider!.search('hello', { limit: 2 }); + const results = await provider!.search('hello'); expect(results).toEqual([ { title: 'Title', url: 'https://example.com', snippet: 'Snippet' }, @@ -689,6 +689,7 @@ describe('WebSearchProviderService', () => { const headers = init.headers as Record; expect(headers['Authorization']).toBe('Bearer access-token'); expect(headers['X-Custom']).toBe('yes'); + expect(JSON.parse(init.body as string)).toEqual({ text_query: 'hello' }); }); });