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.devservesaccess-control-allow-origin: *(verified by preflight), so browser calls work with a user-supplied key — no proxy needed./v2/searchcoversweb_search;/v2/scrape(markdown,onlyMainContent) coversfetch_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_searchresult 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 foropenai-completions/anthropic-messagesflavors, per-thread runner with a reactive$statemirror, tool helper types. Search-specific code (tools, prompt, stores, UI) lives in$lib/search/. - Settings:
AiSettingsgainsapi(wire-protocol flavor, defaultopenai-completions); a newsearch.firecrawlApiKeyfield gates the Firecrawl tools (absent key ⇒ agent runs withnostr_searchonly 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'sagent/model.ts+ PiChessBrowser'sai/settings.tspattern. - Tools:
web_search(query, limit?)— Firecrawl/v2/search, returns titles/urls/descriptions, records sourcesfetch_page(url)— Firecrawl/v2/scrape→ markdown (truncated to a token budget), records the sourcenostr_search(query, kinds?, limit?)— NIP-50 REQ over raw WS, returns matching events (notes, long-form), recordsnostr: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
#ttopic 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 viafirecrawlCredits.ensureLoaded(), click to refresh) + detailed block in Settings → Web research (Check usageruns 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_sourcepulling 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/gainsmodel.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.apiflavor,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-gatedprompt.ts. - Phase 3 — Search UI: DB v2 (
searchThreadsstore), 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 onLIVE=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.