Files
learnharness/src/core/search.ts
T
amosblomqvist f1373d5de5 feat(search): Tavily + Brave behind a search-provider port; Google CSE retires
Search port + model slots wave, chunk A (owner design, 2026-09-20):
- core/search.ts: SearchHit + SearchProvider port + SearchStatus —
  pure, browser-safe; adapters arrive as injected dependencies.
- server/search.ts: Tavily + Brave adapters (wire shapes VERIFIED
  against docs.tavily.com's OpenAPI + Brave's dashboard docs — Tavily
  auth is the Bearer header, not the legacy body api_key; Brave is
  X-Subscription-Token) + engine resolution per owner decisions: a key
  ALONE activates its engine (no engine choice needed), explicit
  agent.search.engine pins the preference (both keys: Tavily first),
  a pinned engine with a missing key is honestly unavailable — no
  silent fallback, no default engine.
- Curated keys catalog: GOOGLE_SEARCH_API_KEY/GOOGLE_CSE_ID retire
  (CSE closed to new customers, dies Jan 2027 — stored values become
  preserved unknown vars); TAVILY_API_KEY (tvly- shape) + BRAVE_API_KEY
  (length guard) join. UI keys-section TOOL_IDS now derives from the
  catalog — cannot drift.
- GET /api/search: {available, engine, reason} — the honest status for
  the settings gray-out; researcher-disable semantics ride the same
  signal (Teacher stream rewires the tool onto the port).
- agent.search settings field (schema + sanitizer + strict parser);
  web.ts's WebSearchResult is now the core SearchHit (re-exported).
2026-09-22 00:02:07 +03:00

54 lines
2.3 KiB
TypeScript

/**
* The search-provider port (search port + model slots wave, 2026-09-20).
*
* Search engines are pluggable behind ONE seam: the settings record names a
* preferred engine (agent.search.engine — optional), keys live in the auth
* store (TAVILY_API_KEY / BRAVE_API_KEY — curated catalog), and the server
* composes the live adapter. The Google CSE engine retired with this wave
* (closed to new customers; dies Jan 2027) — replaced by Tavily + Brave.
*
* Owner decisions baked here:
* - NO default engine in the factory record: an engine is live iff its
* key is present. With a key, the engine auto-activates (the user never
* chooses an engine to make search work — setting the key is enough);
* agent.search.engine is only an explicit preference.
* - Nothing silently falls back: no engine live = search is honestly
* unavailable (the tool says so; the researcher sub-agent is disabled
* by the agent layer on the same signal — that rewiring is the Teacher
* stream's, on this port).
*
* Pure types + protocol docs only (browser-safe, no node imports): the
* adapters are server-side (src/server/search.ts) and arrive as injected
* dependencies — the agent-layer tool depends on THIS port only.
*/
/** One search result row — the shape every adapter must produce (the
* researcher's tool contract; same fields the old Google adapter emitted). */
export interface SearchHit {
title: string;
url: string;
snippet: string;
}
/** The engine ids this wave ships (the catalog: curated keys + adapters). */
export type SearchEngineId = "tavily" | "brave";
/** A live search engine: an id (for status/labels) + one operation. The
* adapters own their wire protocol (fetch, timeouts, error mapping) —
* callers see only this. `search` throws on failure (adapters map HTTP
* errors to clear Error messages); the tool layer catches and reports. */
export interface SearchProvider {
id: SearchEngineId;
search(query: string, count: number): Promise<SearchHit[]>;
}
/** Why search is (un)available — the honest status the UI and the tool
* layer explain from. `engine` is the ACTIVE engine when live. */
export interface SearchStatus {
available: boolean;
/** The active engine (null when unavailable). */
engine: SearchEngineId | null;
/** The one-line reason for the state (status text for the UI). */
reason: string;
}