feat(coding-agent): removed Kagi summarizer integration from fetch tool

- Removed Kagi Universal Summarizer integration from fetch tool and YouTube scraper.
- Removed `fetch.useKagiSummarizer` configuration setting from settings schema.
- Simplified renderHtmlToText() and renderUrl() functions by removing Kagi summarization fallback logic.
- Fixed indentation inconsistencies in test files from tabs to spaces.
This commit is contained in:
can1357
2026-03-09 15:56:04 +01:00
parent 2ec4401bcd
commit 46be698745
9 changed files with 44 additions and 193 deletions
+11 -4
View File
@@ -1,14 +1,21 @@
# Changelog
## [Unreleased]
### Fixed
- Canonicalized bash executor working directories before handing them to brush so `pwd` stays aligned with canonical Git worktree paths in symlinked workspaces
### Added
- Added `#`-triggered prompt action suggestions in the editor, with keybinding hints for line navigation and prompt copy actions
### Removed
- Removed Kagi Universal Summarizer integration from fetch tool—HTML rendering now uses jina, trafilatura, and lynx only
- Removed `fetch.useKagiSummarizer` setting
- Removed Kagi summarization from YouTube video handling
### Fixed
- Canonicalized bash executor working directories before handing them to brush so `pwd` stays aligned with canonical Git worktree paths in symlinked workspaces
## [13.9.10] - 2026-03-08
### Added
@@ -5792,4 +5799,4 @@ Initial public release.
- Git branch display in footer
- Message queueing during streaming responses
- OAuth integration for Gmail and Google Calendar access
- HTML export with syntax highlighting and collapsible sections
- HTML export with syntax highlighting and collapsible sections
@@ -541,15 +541,6 @@ 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,
@@ -3884,18 +3884,11 @@ export class AgentSession {
let attempt = 0;
while (true) {
try {
compactResult = await compact(
preparation,
candidate,
apiKey,
undefined,
autoCompactionSignal,
{
promptOverride: hookPrompt,
extraContext: hookContext,
remoteInstructions: this.#baseSystemPrompt,
},
);
compactResult = await compact(preparation, candidate, apiKey, undefined, autoCompactionSignal, {
promptOverride: hookPrompt,
extraContext: hookContext,
remoteInstructions: this.#baseSystemPrompt,
});
break;
} catch (error) {
if (autoCompactionSignal.aborted) {
+5 -27
View File
@@ -15,7 +15,6 @@ import { renderStatusLine } from "../tui";
import { CachedOutputBlock } from "../tui/output-block";
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
import { ensureTool } from "../utils/tools-manager";
import { summarizeUrlWithKagi } from "../web/kagi";
import { specialHandlers } from "../web/scrapers";
import type { RenderResult } from "../web/scrapers/types";
import { finalizeOutput, loadPage, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
@@ -466,13 +465,12 @@ function parseFeedToMarkdown(content: string, maxItems = 10): string {
}
/**
* Render HTML to markdown using kagi, jina, trafilatura, lynx (in order of preference)
* Render HTML to markdown using jina, trafilatura, lynx (in order of preference)
*/
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);
@@ -484,20 +482,7 @@ async function renderHtmlToText(
signal,
};
// 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();
}
}
// Try jina next (reader API)
// Try jina first (reader API)
try {
const jinaUrl = `https://r.jina.ai/${url}`;
const response = await fetch(jinaUrl, {
@@ -623,13 +608,7 @@ async function handleSpecialUrls(
/**
* Main render function implementing the full pipeline
*/
async function renderUrl(
url: string,
timeout: number,
raw: boolean,
useKagiSummarizer: boolean,
signal?: AbortSignal,
): Promise<FetchRenderResult> {
async function renderUrl(url: string, timeout: number, raw: boolean, signal?: AbortSignal): Promise<FetchRenderResult> {
const notes: string[] = [];
const fetchedAt = new Date().toISOString();
if (signal?.aborted) {
@@ -973,7 +952,7 @@ async function renderUrl(
}
// 5E: Render HTML with lynx or html2text
const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, useKagiSummarizer, signal);
const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, signal);
if (!htmlResult.ok) {
notes.push("html rendering failed (lynx/html2text unavailable)");
const output = finalizeOutput(rawContent);
@@ -1113,8 +1092,7 @@ export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails
throw new ToolAbortError();
}
const useKagiSummarizer = this.session.settings.get("fetch.useKagiSummarizer");
const result = await renderUrl(url, effectiveTimeout, raw, useKagiSummarizer, signal);
const result = await renderUrl(url, effectiveTimeout, raw, signal);
const truncation = truncateHead(result.content, {
maxBytes: DEFAULT_MAX_BYTES,
maxLines: FETCH_DEFAULT_MAX_LINES,
-42
View File
@@ -1,18 +1,8 @@
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
import { findCredential } from "./search/providers/utils";
const KAGI_SUMMARIZE_URL = "https://kagi.com/api/v0/summarize";
const KAGI_SEARCH_URL = "https://kagi.com/api/v0/search";
interface KagiSummarizeResponse {
data?: {
output?: string;
};
error?: Array<{
msg?: string;
}>;
}
interface KagiSearchResultObject {
t: 0;
url: string;
@@ -105,14 +95,6 @@ function parseKagiErrorResponse(statusCode: number, responseText: string): KagiA
}
}
export interface KagiSummarizeOptions {
engine?: string;
summaryType?: string;
targetLanguage?: string;
cache?: boolean;
signal?: AbortSignal;
}
export interface KagiSearchOptions {
limit?: number;
signal?: AbortSignal;
@@ -142,30 +124,6 @@ function getAuthHeaders(apiKey: string): Record<string, string> {
};
}
export async function summarizeUrlWithKagi(url: string, options: KagiSummarizeOptions = {}): Promise<string | null> {
const apiKey = await findKagiApiKey();
if (!apiKey) return null;
const requestUrl = new URL(KAGI_SUMMARIZE_URL);
requestUrl.searchParams.set("url", url);
requestUrl.searchParams.set("summary_type", options.summaryType ?? "summary");
if (options.engine) requestUrl.searchParams.set("engine", options.engine);
if (options.targetLanguage) requestUrl.searchParams.set("target_language", options.targetLanguage);
if (options.cache !== undefined) requestUrl.searchParams.set("cache", String(options.cache));
const response = await fetch(requestUrl, {
headers: getAuthHeaders(apiKey),
signal: options.signal,
});
if (!response.ok) return null;
const payload = (await response.json()) as KagiSummarizeResponse;
if (payload.error && payload.error.length > 0) return null;
const output = payload.data?.output?.trim();
return output && output.length > 0 ? output : null;
}
export async function searchWithKagi(query: string, options: KagiSearchOptions = {}): Promise<KagiSearchResult> {
const apiKey = await findKagiApiKey();
if (!apiKey) {
@@ -4,7 +4,6 @@ import * as path from "node:path";
import { ptree, Snowflake } from "@oh-my-pi/pi-utils";
import { throwIfAborted } from "../../tools/tool-errors";
import { ensureTool } from "../../utils/tools-manager";
import { summarizeUrlWithKagi } from "../kagi";
import type { RenderResult, SpecialHandler } from "./types";
import { buildResult, formatMediaDuration, formatNumber } from "./types";
@@ -110,22 +109,6 @@ export const handleYouTube: SpecialHandler = async (
const notes: string[] = [];
const videoUrl = `https://www.youtube.com/watch?v=${yt.videoId}`;
// Prefer Kagi Universal Summarizer when credentials are available
try {
const kagiSummary = await summarizeUrlWithKagi(videoUrl, { signal });
if (kagiSummary && kagiSummary.length > 100) {
return buildResult(kagiSummary, {
url,
finalUrl: videoUrl,
method: "kagi",
fetchedAt,
notes: ["Used Kagi Universal Summarizer for YouTube"],
});
}
} catch {
throwIfAborted(signal);
}
// Ensure yt-dlp is available (auto-download if missing)
const ytdlp = await ensureTool("yt-dlp", { signal, silent: true });
if (!ytdlp) {
@@ -7,10 +7,10 @@
* them into a combined `answer` string on the SearchResponse.
*/
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
import { settings } from "../../../config/settings";
import { callExaTool, findApiKey, isSearchResponse } from "../../../exa/mcp-client";
import type { SearchResponse, SearchSource } from "../../../web/search/types";
import { SearchProviderError } from "../../../web/search/types";
import { settings } from "../../../config/settings";
import { dateToAgeSeconds } from "../utils";
import type { SearchParams } from "./base";
import { SearchProvider } from "./base";
@@ -25,8 +25,8 @@ function getRuntimeSignals(): string[] {
}
/**
* Regression test: auto-compaction completion should resume the agent loop when
* there are queued agent-level messages (follow-up/steering/custom).
* Regression test: auto-compaction completion should resume the agent loop when
* there are queued agent-level messages (follow-up/steering/custom).
*/
describe("AgentSession auto-compaction queue resume", () => {
let tempDir: TempDir;
@@ -60,17 +60,17 @@ describe("AgentSession auto-compaction queue resume", () => {
"\t\t};",
"\t});",
'\tpi.on("auto_compaction_start", async (event) => {',
`\t\tconst signals = globalThis.${runtimeSignalStoreKey} ?? (globalThis.${runtimeSignalStoreKey} = []);`,
'\t\tsignals.push("compaction:start:" + event.reason);',
"\t});",
`\t\tconst signals = globalThis.${runtimeSignalStoreKey} ?? (globalThis.${runtimeSignalStoreKey} = []);`,
'\t\tsignals.push("compaction:start:" + event.reason);',
"\t});",
'\tpi.on("auto_compaction_end", async (event) => {',
`\t\tconst signals = globalThis.${runtimeSignalStoreKey} ?? (globalThis.${runtimeSignalStoreKey} = []);`,
'\t\tsignals.push("compaction:end:" + (event.aborted ? "aborted" : "ok"));',
"\t});",
`\t\tconst signals = globalThis.${runtimeSignalStoreKey} ?? (globalThis.${runtimeSignalStoreKey} = []);`,
'\t\tsignals.push("compaction:end:" + (event.aborted ? "aborted" : "ok"));',
"\t});",
'\tpi.on("todo_reminder", async (event) => {',
`\t\tconst signals = globalThis.${runtimeSignalStoreKey} ?? (globalThis.${runtimeSignalStoreKey} = []);`,
'\t\tsignals.push("todo:" + event.attempt + "/" + event.maxAttempts);',
"\t});",
`\t\tconst signals = globalThis.${runtimeSignalStoreKey} ?? (globalThis.${runtimeSignalStoreKey} = []);`,
'\t\tsignals.push("todo:" + event.attempt + "/" + event.maxAttempts);',
"\t});",
"}",
].join("\n"),
);
@@ -239,5 +239,4 @@ describe("AgentSession auto-compaction queue resume", () => {
expect(getRuntimeSignals()).toContain("todo:1/3");
expect(continueSpy).toHaveBeenCalledTimes(1);
});
});
@@ -7,14 +7,12 @@ import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { FetchTool } from "@oh-my-pi/pi-coding-agent/tools/fetch";
import * as imageResize from "@oh-my-pi/pi-coding-agent/utils/image-resize";
import * as toolsManager from "@oh-my-pi/pi-coding-agent/utils/tools-manager";
import * as kagi from "@oh-my-pi/pi-coding-agent/web/kagi";
import type { LoadPageResult } from "@oh-my-pi/pi-coding-agent/web/scrapers/types";
import * as scrapers from "@oh-my-pi/pi-coding-agent/web/scrapers/types";
import * as scraperUtils from "@oh-my-pi/pi-coding-agent/web/scrapers/utils";
import * as natives from "@oh-my-pi/pi-natives";
import { hookFetch, ptree, Snowflake } from "@oh-my-pi/pi-utils";
describe("fetch tool Kagi summarization toggle", () => {
describe("fetch tool", () => {
let testDir: string;
beforeEach(() => {
@@ -41,64 +39,8 @@ describe("fetch tool Kagi summarization toggle", () => {
};
};
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: "<html><body><h1>Example Domain</h1><p>Short sample content.</p></body></html>",
};
}
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);
using _hook = hookFetch(() => 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);
using _hook = hookFetch(() => 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");
});
it("returns an image content block when fetching image URLs", async () => {
const session = createSession({ "fetch.useKagiSummarizer": false });
const session = createSession();
const tool = new FetchTool(session);
const imageBytes = new Uint8Array([137, 80, 78, 71]);
vi.spyOn(scrapers, "loadPage").mockResolvedValue({
@@ -142,7 +84,7 @@ describe("fetch tool Kagi summarization toggle", () => {
});
it("resizes fetched images before emitting image content blocks", async () => {
const session = createSession({ "fetch.useKagiSummarizer": false });
const session = createSession();
const tool = new FetchTool(session);
const resizeSpy = vi.spyOn(imageResize, "resizeImage").mockResolvedValue({
buffer: new Uint8Array([1, 2, 3]),
@@ -188,7 +130,7 @@ describe("fetch tool Kagi summarization toggle", () => {
});
it("keeps markitdown extracted text for image responses", async () => {
const session = createSession({ "fetch.useKagiSummarizer": false });
const session = createSession();
const tool = new FetchTool(session);
const extractedText = "Converted image text content that is definitely longer than fifty characters.";
vi.spyOn(imageResize, "resizeImage").mockResolvedValue({
@@ -232,7 +174,7 @@ describe("fetch tool Kagi summarization toggle", () => {
expect(imageBlock?.data).toBe("aW1hZ2U=");
});
it("falls back to text-only output for unsupported image MIME types", async () => {
const session = createSession({ "fetch.useKagiSummarizer": false });
const session = createSession();
const tool = new FetchTool(session);
const fetchBinarySpy = vi.spyOn(scraperUtils, "fetchBinary");
vi.spyOn(scrapers, "loadPage").mockResolvedValue({
@@ -255,7 +197,7 @@ describe("fetch tool Kagi summarization toggle", () => {
});
it("uses binary conversion fallback for unsupported image MIME when extension is convertible", async () => {
const session = createSession({ "fetch.useKagiSummarizer": false });
const session = createSession();
const tool = new FetchTool(session);
const convertedText = "Converted image text from markitdown fallback with sufficient length to pass threshold.";
const fetchBinarySpy = vi.spyOn(scraperUtils, "fetchBinary").mockResolvedValue({
@@ -288,7 +230,7 @@ describe("fetch tool Kagi summarization toggle", () => {
});
it("does not treat text/html at .png paths as inline images", async () => {
const session = createSession({ "fetch.useKagiSummarizer": false });
const session = createSession();
const tool = new FetchTool(session);
vi.spyOn(scraperUtils, "fetchBinary").mockResolvedValue({ ok: false, error: "not an image" });
vi.spyOn(scrapers, "loadPage").mockResolvedValue({
@@ -310,7 +252,7 @@ describe("fetch tool Kagi summarization toggle", () => {
});
it("falls back to textual output when inline image refetch fails", async () => {
const session = createSession({ "fetch.useKagiSummarizer": false });
const session = createSession();
const tool = new FetchTool(session);
const convertSpy = vi.spyOn(scraperUtils, "convertWithMarkitdown");
vi.spyOn(scrapers, "loadPage").mockResolvedValue({
@@ -336,7 +278,7 @@ describe("fetch tool Kagi summarization toggle", () => {
expect(fetchBinarySpy).toHaveBeenCalledTimes(1);
});
it("falls back to text-only output when image payload bytes are invalid", async () => {
const session = createSession({ "fetch.useKagiSummarizer": false });
const session = createSession();
const tool = new FetchTool(session);
vi.spyOn(scrapers, "loadPage").mockResolvedValue({
ok: true,
@@ -377,7 +319,7 @@ describe("fetch tool Kagi summarization toggle", () => {
expect(textBlock?.text).toContain("<html><body>gateway error</body></html>");
});
it("prefers rendered page content over site-wide llms.txt for deep pages", async () => {
const session = createSession({ "fetch.useKagiSummarizer": false });
const session = createSession();
const tool = new FetchTool(session);
const pageUrl = "https://bun.com/reference/bun/UnixSocketOptions";
const pageHtml = "<html><body><main><h1>UnixSocketOptions</h1><p>Page-specific docs.</p></main></body></html>";
@@ -445,7 +387,7 @@ describe("fetch tool Kagi summarization toggle", () => {
});
it("uses section-scoped llms.txt fallback without requesting the site-wide file", async () => {
const session = createSession({ "fetch.useKagiSummarizer": false });
const session = createSession();
const tool = new FetchTool(session);
const pageUrl = "https://example.com/docs/reference/widget";
const pageHtml = "<html><body><nav>Docs</nav><main><h1>Widget</h1></main></body></html>";