Skip to content

Proposal: Search Mode

Status: Superseded by four-modes.md · Last updated: 2026-07-15

2026-07-15 — Search is no longer a mode. It was two unrelated things bolted together: a research agent (which is just Chat with tools) and a news surface (which is just Read with a different content type). The agent folded into Chat behind a Search popover; the news surface folded into Read as Collections. This doc is frozen as the record of what Search was and why its pieces are shaped the way they are — the provider layer it built (firecrawl.ts, nip50.ts, the tool definitions, the source-recording seam) survives in $lib/providers/.

Summary

An in-browser research agent: ask a question, the agent searches and reads sources, and returns a cited answer in a chat UI. Successor to SvelteReader's "Deep Research" mode — but running the pi agent loop client-side instead of LangGraph on a server.

Motivation

SvelteReader's deepresearch mode proved the product shape (research chat with citations) but its LangGraph backend violates the no-server principle. PiChessBrowser and socratic-seminar prove the replacement: pi's ReAct loop runs fine in the browser against a user-configured, CORS-enabled LLM endpoint.

Search is also the cheapest vehicle for the shared agent runtime that Synthesize and Debate need: a single chat surface, simple self-contained tools, no workspace layout or approval gates. The runtime lands mode-agnostic in $lib/ai/ with Search as its first consumer.

Decisions

  • Web search/fetch backend: Firecrawl (decided 2026-07-13). api.firecrawl.dev serves access-control-allow-origin: * (verified by preflight), so browser calls work with a user-supplied key — no proxy needed. /v2/search covers web_search; /v2/scrape (markdown, onlyMainContent) covers fetch_page, including pages that block cross-origin fetch, since Firecrawl does the fetching. More providers (Brave, Tavily, self-hosted SearXNG) can slot in later behind the same tool interface — the settings UI keys the provider choice.
  • nostr_search transport: one-shot raw-WebSocket REQ against a NIP-50 relay (default wss://relay.nostr.band). Cyphertap's pool doesn't include a search relay, and a ~40-line raw-WS client avoids coupling the app to NDK internals (precedent: cyphertap's own raw-WS negentropy sync). Works logged-out-of-Firecrawl — the mode is usable with zero third-party keys.
  • Sources are recorded by the tools, not the model: every web_search result and every fetched/searched page lands in the thread's source list with a stable id (s1, s2, …) as a side effect of tool execution. The prompt instructs the agent to cite [s1]-style markers; the UI links markers to the sources panel. The app-authoritative record is the tool-built source list (PiChessBrowser's verify-by-state lesson) — prose citations are presentation only.
  • Threads are device-local in the per-npub IndexedDB (DB v2 adds searchThreads), matching Read mode's chat-thread stance: transcripts stay outside the sync schema.
  • Agent runtime is mode-agnostic ($lib/ai/model.ts, $lib/ai/runner.svelte.ts): model builder for openai-completions / anthropic-messages flavors, per-thread runner with a reactive $state mirror, tool helper types. Search-specific code (tools, prompt, stores, UI) lives in $lib/search/.
  • Settings: AiSettings gains api (wire-protocol flavor, default openai-completions); a new search.firecrawlApiKey field gates the Firecrawl tools (absent key ⇒ agent runs with nostr_search only and says so). All localStorage, never synced, .env-prefillable in dev like the existing AI fields.

Design

  • Agent runtime: @earendil-works/pi-agent-core + @earendil-works/pi-ai (0.80.6, pinned like the references), one agent per research thread. BYO endpoint/key/model; settings in localStorage — socratic-seminar's agent/model.ts + PiChessBrowser's ai/settings.ts pattern.
  • Tools:
    • web_search(query, limit?) — Firecrawl /v2/search, returns titles/urls/descriptions, records sources
    • fetch_page(url) — Firecrawl /v2/scrape → markdown (truncated to a token budget), records the source
    • nostr_search(query, kinds?, limit?) — NIP-50 REQ over raw WS, returns matching events (notes, long-form), records nostr: sources
  • UI (SvelteReader deepresearch shape, reworked 2026-07-13): collapsible left thread rail (icon rail ↔ searchable date-grouped history with confirm-delete); hero view — "Research deeply. Get answers." over a rounded input card whose controls row holds the effort selector (Quick/Balanced/Thorough, persisted, wired into the agent prompt), Web/Nostr tool toggles, settings gear, round submit — with Explore Recent Events below (recent kind-30023 articles from a general relay via #t topic pills; card select becomes news-reading mode later); the hero adapts into the chat view once a search starts (assistant bubbles render sanitized markdown with clickable [sN] citations); collapsible right sources rail (count badge when collapsed) whose cards open a source detail modal.
  • Pay-per-use anticipation: unchanged — the // PAYMENTS: seam. Firecrawl metering would hook the same place as inference metering when a payments backend exists.

Reference material

  • SvelteReader frontend/src/lib/components/deepresearch/ + frontend/src/routes/deepresearch/ — target UX
  • socratic-seminar src/lib/agent/ — runner, model builder, tool patterns, persistence/pruning
  • PiChessBrowser src/ai/piPlayer.ts — minimal loop, timeout/repair/verify-by-state-diff patterns
  • Firecrawl API: POST /v2/search ({query, limit, sources}data.web[{title,description,url}]), POST /v2/scrape ({url, formats:["markdown"], onlyMainContent}data.markdown + data.metadata)
  • Spec: nips/50.md (search capability)

Open questions

  • More search providers: Brave / Tavily / self-hosted SearXNG behind a provider picker — deferred; the tool interface is provider-agnostic.
  • News reading: built per news-mode.md, and since 2026-07-13 it lives inside Search — the $lib/news/ surface (Explore/Saved/Feeds tabs, URL bar, article reader) renders below the hero, cards open the reader in place, deep links are /search?a=//search?url=.
  • Firecrawl credit usage (2026-07-14): BYOK-only surface on GET /v2/team/credit-usage (CORS *, verified) — compact flame chip in the input bar (one lazy fetch per session via firecrawlCredits.ensureLoaded(), click to refresh) + detailed block in Settings → Web research (Check usage runs against the draft key, so a pasted key verifies before saving). Store: $lib/search/stores/credits.svelte.ts. Disappears with the key when pay-per-use payments land.
  • Feed relay + quality: the discover feed reads wss://relay.damus.io (hardcoded; the NIP-50 search relay ignores plain REQs) with a one-article-per-author cap against bulk publishers. A configurable feed relay and better spam filtering (WoT?) are open.
  • Cross-mode sources: read_source pulling sources saved by other modes — deferred to the Synthesize shared-core work.
  • Nostr sync for threads/sources: deferred with the same reasoning as Read-mode chats; revisit if research threads prove share-worthy.

Phases

  • Phase 1 — Agent runtime: pi deps pinned at 0.80.6, $lib/ai/ gains model.ts, runner.svelte.ts (reactive mirror, abort, error surface, toolRuns rebuilt from rehydrated transcripts), tools.ts (jsonResult + withArgRepair), pruning.ts, transcript.ts (sanitize + text extraction); settings extension (ai.api flavor, search.firecrawlApiKey, search.searchRelay).
  • Phase 2 — Tools: $lib/search/firecrawl.ts (+ // PAYMENTS: seam), nip50.ts (raw-WS one-shot REQ, timeout resolves with partial results), tools.ts (web_search / fetch_page / nostr_search, source recording with s1/s2 ids, 20k-char page budget), capability-gated prompt.ts.
  • Phase 3 — Search UI: DB v2 (searchThreads store), thread store ($lib/search/stores/threads.svelte.ts, one runner per thread kept across switches) + session hooks, SearchView (thread sidebar / chat / sources panel), ToolCallCard (expandable result), citation [sN] click → source flash, SearchSettingsDialog (AI endpoint + flavor, Firecrawl key, search relay).
  • Phase 4 — Playwright e2e (e2e/search.test.ts, 4 tests): logged-out gate, unconfigured pitch + dialog, full mocked roundtrip (SSE chat chunks + Firecrawl JSON: tool card, 2 sources, cited answer, citation highlight, thread + tool cards + sources survive reload), thread deletion persists. Full suite 30 green.
  • Phase 5 — Live smoke (e2e/search-live.test.ts, gated on LIVE=1 + filled .env): executed 2026-07-13 against api.abvstudio.net (gemma-4-26b via LM Studio, openai-completions), real Firecrawl, relay.nostr.band. The agent ran 5 tool calls (3× web_search, 2× nostr_search), recorded 14 sources, and answered with correct inline [sN] citations in 1.3 min. Re-run the gate after runtime or endpoint changes.

Each phase gated on pnpm check + pnpm build + full Playwright suite.