diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index af957616a..c921f50ee 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 \ No newline at end of file diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index a8bc928d0..6cf4399d4 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -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, diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 428956f86..2507004b5 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -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) { diff --git a/packages/coding-agent/src/tools/fetch.ts b/packages/coding-agent/src/tools/fetch.ts index b9cac9454..4cfed4d67 100644 --- a/packages/coding-agent/src/tools/fetch.ts +++ b/packages/coding-agent/src/tools/fetch.ts @@ -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 { +async function renderUrl(url: string, timeout: number, raw: boolean, signal?: AbortSignal): Promise { 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; -} - 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 { }; } -export async function summarizeUrlWithKagi(url: string, options: KagiSummarizeOptions = {}): Promise { - 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 { const apiKey = await findKagiApiKey(); if (!apiKey) { diff --git a/packages/coding-agent/src/web/scrapers/youtube.ts b/packages/coding-agent/src/web/scrapers/youtube.ts index 12a7577ff..fffd58050 100644 --- a/packages/coding-agent/src/web/scrapers/youtube.ts +++ b/packages/coding-agent/src/web/scrapers/youtube.ts @@ -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) { diff --git a/packages/coding-agent/src/web/search/providers/exa.ts b/packages/coding-agent/src/web/search/providers/exa.ts index b62c8e25d..ed5677460 100644 --- a/packages/coding-agent/src/web/search/providers/exa.ts +++ b/packages/coding-agent/src/web/search/providers/exa.ts @@ -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"; diff --git a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts index 8e1991db9..93bceabe6 100644 --- a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts +++ b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts @@ -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); }); - }); diff --git a/packages/coding-agent/test/tools/fetch-kagi-toggle.test.ts b/packages/coding-agent/test/tools/fetch-kagi-toggle.test.ts index 233ed85ec..f11f96cd5 100644 --- a/packages/coding-agent/test/tools/fetch-kagi-toggle.test.ts +++ b/packages/coding-agent/test/tools/fetch-kagi-toggle.test.ts @@ -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: "

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); - 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("gateway error"); }); 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 = "

UnixSocketOptions

Page-specific docs.

"; @@ -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 = "

Widget

";