diff --git a/.pi/agent/extensions/web-search/src/format.test.ts b/.pi/agent/extensions/web-search/src/format.test.ts new file mode 100644 index 0000000..7ea273e --- /dev/null +++ b/.pi/agent/extensions/web-search/src/format.test.ts @@ -0,0 +1,53 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { formatFetchOutput, formatSearchOutput, truncateText } from "./format.ts"; + +test("formatSearchOutput renders a compact metadata-only list", () => { + const output = formatSearchOutput({ + providerName: "exa-main", + results: [ + { + title: "Exa Docs", + url: "https://exa.ai/docs", + publishedDate: "2026-04-09", + author: "Exa", + score: 0.98, + }, + ], + }); + + assert.match(output, /Found 1 web result via exa-main:/); + assert.match(output, /Exa Docs/); + assert.match(output, /https:\/\/exa.ai\/docs/); +}); + +test("truncateText shortens long fetch bodies with an ellipsis", () => { + assert.equal(truncateText("abcdef", 4), "abc…"); + assert.equal(truncateText("abc", 10), "abc"); +}); + +test("formatFetchOutput includes both successful and failed URLs", () => { + const output = formatFetchOutput( + { + providerName: "exa-main", + results: [ + { + url: "https://good.example", + title: "Good", + text: "This is a very long body that should be truncated in the final output.", + }, + { + url: "https://bad.example", + title: null, + error: "429 rate limited", + }, + ], + }, + { maxCharactersPerResult: 20 }, + ); + + assert.match(output, /Status: ok/); + assert.match(output, /Status: failed/); + assert.match(output, /429 rate limited/); + assert.match(output, /This is a very long…/); +}); diff --git a/.pi/agent/extensions/web-search/src/format.ts b/.pi/agent/extensions/web-search/src/format.ts new file mode 100644 index 0000000..3f48046 --- /dev/null +++ b/.pi/agent/extensions/web-search/src/format.ts @@ -0,0 +1,76 @@ +import type { NormalizedFetchResponse, NormalizedSearchResponse } from "./providers/types.ts"; + +export function truncateText(text: string, maxCharacters = 4000) { + if (text.length <= maxCharacters) { + return text; + } + return `${text.slice(0, Math.max(0, maxCharacters - 1))}…`; +} + +export function formatSearchOutput(response: NormalizedSearchResponse) { + if (response.results.length === 0) { + return `No web results via ${response.providerName}.`; + } + + const lines = [ + `Found ${response.results.length} web result${response.results.length === 1 ? "" : "s"} via ${response.providerName}:`, + ]; + + for (const [index, result] of response.results.entries()) { + lines.push(`${index + 1}. ${result.title ?? "(untitled)"}`); + lines.push(` URL: ${result.url}`); + + const meta = [result.publishedDate, result.author].filter(Boolean); + if (meta.length > 0) { + lines.push(` Meta: ${meta.join(" • ")}`); + } + + if (typeof result.score === "number") { + lines.push(` Score: ${result.score}`); + } + } + + return lines.join("\n"); +} + +export interface FetchFormatOptions { + maxCharactersPerResult?: number; +} + +export function formatFetchOutput(response: NormalizedFetchResponse, options: FetchFormatOptions = {}) { + const maxCharactersPerResult = options.maxCharactersPerResult ?? 4000; + const lines = [ + `Fetched ${response.results.length} URL${response.results.length === 1 ? "" : "s"} via ${response.providerName}:`, + ]; + + for (const result of response.results) { + lines.push(""); + lines.push(`URL: ${result.url}`); + + if (result.error) { + lines.push("Status: failed"); + lines.push(`Error: ${result.error}`); + continue; + } + + lines.push("Status: ok"); + if (result.title) { + lines.push(`Title: ${result.title}`); + } + if (result.summary) { + lines.push(`Summary: ${result.summary}`); + } + if (result.highlights?.length) { + lines.push("Highlights:"); + for (const highlight of result.highlights) { + lines.push(`- ${highlight}`); + } + } + if (result.text) { + lines.push("Text:"); + lines.push(truncateText(result.text, maxCharactersPerResult)); + } + } + + return lines.join("\n"); +} diff --git a/docs/superpowers/plans/2026-04-09-web-search-tools.md b/docs/superpowers/plans/2026-04-09-web-search-tools.md index 4330d4f..ec2730f 100644 --- a/docs/superpowers/plans/2026-04-09-web-search-tools.md +++ b/docs/superpowers/plans/2026-04-09-web-search-tools.md @@ -737,7 +737,7 @@ git -C /home/alex/dotfiles commit -m "test: add exa web provider adapter" - Create: `/home/alex/dotfiles/.pi/agent/extensions/web-search/src/format.ts` - Test: `/home/alex/dotfiles/.pi/agent/extensions/web-search/src/format.test.ts` -- [ ] **Step 1: Write the failing formatter tests** +- [x] **Step 1: Write the failing formatter tests** Create `/home/alex/dotfiles/.pi/agent/extensions/web-search/src/format.test.ts` with this content: @@ -797,7 +797,7 @@ test("formatFetchOutput includes both successful and failed URLs", () => { }); ``` -- [ ] **Step 2: Run the formatter tests to verify they fail** +- [x] **Step 2: Run the formatter tests to verify they fail** Run: @@ -808,7 +808,7 @@ npx tsx --test src/format.test.ts Expected: `FAIL` with an `ERR_MODULE_NOT_FOUND` error for `./format.ts`. -- [ ] **Step 3: Write the minimal formatter implementation** +- [x] **Step 3: Write the minimal formatter implementation** Create `/home/alex/dotfiles/.pi/agent/extensions/web-search/src/format.ts` with this content: @@ -891,7 +891,7 @@ export function formatFetchOutput(response: NormalizedFetchResponse, options: Fe } ``` -- [ ] **Step 4: Run the formatter tests to verify they pass** +- [x] **Step 4: Run the formatter tests to verify they pass** Run: @@ -902,7 +902,7 @@ npx tsx --test src/format.test.ts Expected: `PASS` for all three tests. -- [ ] **Step 5: Commit the formatter layer** +- [x] **Step 5: Commit the formatter layer** Run: