From d8384a488ed3575dc89477a8f517842e6f7d1b42 Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 4 Mar 2026 02:34:47 +0100 Subject: [PATCH] feat(fetch): added optional Kagi summarizer toggle to fetch configuration - Added `fetch.useKagiSummarizer` configuration setting to toggle Kagi Universal Summarizer usage in fetch tool. - Updated fetch tool to conditionally apply Kagi summarization based on configuration setting. - Added comprehensive test coverage for Kagi summarizer toggle behavior with mocked dependencies. --- packages/coding-agent/CHANGELOG.md | 3 + .../src/config/settings-schema.ts | 9 ++ packages/coding-agent/src/tools/fetch.ts | 32 ++++--- .../test/tools/fetch-kagi-toggle.test.ts | 96 +++++++++++++++++++ 4 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 packages/coding-agent/test/tools/fetch-kagi-toggle.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ff433e9e8..0e8936ef1 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] +### Added +- Added `fetch.useKagiSummarizer` setting to toggle Kagi Universal Summarizer usage in the fetch tool. + ### Fixed - Fixed incorrect message history reference in session title generation that could cause missing or stale titles on first message diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 0cbe2de52..74caa27ed 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -490,6 +490,15 @@ export const SETTINGS_SCHEMA = { default: true, ui: { tab: "tools", label: "Enable Fetch", description: "Enable the fetch tool for URL fetching" }, }, + "fetch.useKagiSummarizer": { + type: "boolean", + default: true, + ui: { + tab: "tools", + label: "Use Kagi in Fetch", + description: "Use Kagi Universal Summarizer when rendering HTML in fetch", + }, + }, "web_search.enabled": { type: "boolean", default: true, diff --git a/packages/coding-agent/src/tools/fetch.ts b/packages/coding-agent/src/tools/fetch.ts index 0ad467484..6460a84bf 100644 --- a/packages/coding-agent/src/tools/fetch.ts +++ b/packages/coding-agent/src/tools/fetch.ts @@ -429,6 +429,7 @@ async function renderHtmlToText( url: string, html: string, timeout: number, + useKagiSummarizer: boolean, userSignal?: AbortSignal, ): Promise<{ content: string; ok: boolean; method: string }> { const signal = ptree.combineSignals(userSignal, timeout * 1000); @@ -440,15 +441,17 @@ async function renderHtmlToText( signal, }; - // Try Kagi Universal Summarizer first (if KAGI_API_KEY is configured) - try { - const kagiSummary = await summarizeUrlWithKagi(url, { signal }); - if (kagiSummary && kagiSummary.length > 100 && !isLowQualityOutput(kagiSummary)) { - return { content: kagiSummary, ok: true, method: "kagi" }; + // Try Kagi Universal Summarizer first (if enabled and KAGI_API_KEY is configured) + if (useKagiSummarizer) { + try { + const kagiSummary = await summarizeUrlWithKagi(url, { signal }); + if (kagiSummary && kagiSummary.length > 100 && !isLowQualityOutput(kagiSummary)) { + return { content: kagiSummary, ok: true, method: "kagi" }; + } + } catch { + // Kagi failed, continue to next method + signal?.throwIfAborted(); } - } catch { - // Kagi failed, continue to next method - signal?.throwIfAborted(); } // Try jina next (reader API) @@ -564,7 +567,13 @@ async function handleSpecialUrls(url: string, timeout: number, signal?: AbortSig /** * Main render function implementing the full pipeline */ -async function renderUrl(url: string, timeout: number, raw: boolean, signal?: AbortSignal): Promise { +async function renderUrl( + url: string, + timeout: number, + raw: boolean, + useKagiSummarizer: boolean, + signal?: AbortSignal, +): Promise { const notes: string[] = []; const fetchedAt = new Date().toISOString(); if (signal?.aborted) { @@ -803,7 +812,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean, signal?: Ab } // Step 6: Render HTML with lynx or html2text - const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, signal); + const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, useKagiSummarizer, signal); if (!htmlResult.ok) { notes.push("html rendering failed (lynx/html2text unavailable)"); const output = finalizeOutput(rawContent); @@ -926,7 +935,8 @@ export class FetchTool implements AgentTool { + let testDir: string; + + beforeEach(() => { + testDir = path.join(os.tmpdir(), `fetch-kagi-toggle-${Snowflake.next()}`); + fs.mkdirSync(testDir, { recursive: true }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + fs.rmSync(testDir, { recursive: true, force: true }); + }); + + const createSession = (overrides: Partial> = {}): ToolSession => { + const sessionFile = path.join(testDir, "session.jsonl"); + return { + cwd: testDir, + hasUI: false, + getSessionFile: () => sessionFile, + getSessionSpawns: () => null, + settings: Settings.isolated({ + "fetch.enabled": true, + ...overrides, + }), + }; + }; + + const mockLoadPage = () => { + const pageResponse = (url: string): LoadPageResult => { + if (url === "https://example.com") { + return { + ok: true, + status: 200, + contentType: "text/html", + finalUrl: "https://example.com", + content: "

Example Domain

Short sample content.

", + }; + } + + return { + ok: false, + status: 404, + contentType: "", + finalUrl: url, + content: "", + }; + }; + + return vi + .spyOn(scrapers, "loadPage") + .mockImplementation(async (url: string, _options?: unknown) => pageResponse(url)); + }; + + it("uses Kagi summarizer when enabled", async () => { + const session = createSession({ "fetch.useKagiSummarizer": true }); + const tool = new FetchTool(session); + const loadPageSpy = mockLoadPage(); + const summarizeSpy = vi.spyOn(kagi, "summarizeUrlWithKagi").mockResolvedValue("x".repeat(150)); + vi.spyOn(toolsManager, "ensureTool").mockResolvedValue(undefined); + vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("blocked", { status: 500, statusText: "Blocked" })); + + const result = await tool.execute("fetch-1", { url: "https://example.com" }); + + expect(loadPageSpy).toHaveBeenCalled(); + expect(summarizeSpy).toHaveBeenCalledTimes(1); + expect(result.details?.method).toBe("kagi"); + }); + + it("skips Kagi summarizer when disabled", async () => { + const session = createSession({ "fetch.useKagiSummarizer": false }); + const tool = new FetchTool(session); + const loadPageSpy = mockLoadPage(); + const summarizeSpy = vi.spyOn(kagi, "summarizeUrlWithKagi").mockResolvedValue("x".repeat(150)); + vi.spyOn(toolsManager, "ensureTool").mockResolvedValue(undefined); + vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("blocked", { status: 500, statusText: "Blocked" })); + + const result = await tool.execute("fetch-2", { url: "https://example.com" }); + + expect(loadPageSpy).toHaveBeenCalled(); + expect(summarizeSpy).not.toHaveBeenCalled(); + expect(result.details?.method).not.toBe("kagi"); + }); +});