feat(search): web_search onto the injected port — Google CSE retired, research honestly disabled

Chunk A of the search port wave (2026-09-20), per master's assignment:

- web.ts: the Google Custom Search backend is gone — web_search now runs
  on the core search port (SearchProvider), injected as a dependency
  (import law: the tool never imports server code). createWebTools(search)
  resolves the LIVE provider fresh per call; null = the honest
  unavailability string (no silent fallback, owner decision). web_fetch
  unchanged (needs no engine).
- runner: JobRunnerDeps.searchProvider — the server composition threaded
  into the researcher; jobs already in flight when a key disappears still
  fail honestly through the same seam, never crash.
- jobTools: the research dispatch tool DROPS from the teacher's registry
  when ctx.searchAvailable() is false (owner decision: registry-level
  drop — the model never sees the tool, no doomed dispatch, no error
  card). Absent seam = available (tests). chat-handler injects
  server/searchAvailable + searchProvider.
- tests: stubFetch (the Google wire stub) retired; jobs.test.ts threads
  a recording stub provider through all four research runners and asserts
  the scripted query hit the port; new web-search.test.ts covers the
  factory contract (null → honest string, hits, error strings, fresh
  resolution, web_fetch independence) + the registry drop matrix.

Gates: typecheck, 793/793 tests, lint, build — all green.
This commit is contained in:
amosblomqvist
2026-09-22 13:59:25 +03:00
parent 48fbd988f1
commit ed264c936d
8 changed files with 385 additions and 232 deletions
+19 -5
View File
@@ -22,6 +22,12 @@ export interface JobToolContext {
* input rides along because the spawn happens MID-STREAM, before
* onFinish persists the tool part — a transcript scan would race it. */
spawn: (job: JobRecord, input: ResearchInput | DemoInput | VisualInput | PdfInput) => void;
/** Search availability (search port wave, 2026-09-20): the honest
* disable signal for the research dispatch tool — no live engine
* means NO research tool in the registry (owner decision: registry-
* level drop, no doomed dispatch, no silent fallback). Absent =
* available (tests + any seam without search wiring). */
searchAvailable?: () => boolean;
}
export const researchInputSchema = z.object({
@@ -121,14 +127,22 @@ const PDF_DESCRIPTION =
* Per-request job tool registry: each tool closes over the composition root's
* context (chatId + jobStore + the spawn seam), so concurrent chats never
* share state. chat-handler wires it each turn; tests wire their own.
*
* The research tool drops from the registry when ctx.searchAvailable reports
* no live engine (2026-09-20) — the teacher never sees it, so a keyless
* server can't dispatch doomed research jobs. Jobs ALREADY in flight when
* a key disappears still run to their honest failure (runner backstop).
*/
export function jobTools(ctx: JobToolContext): Record<string, LhTool<any, any>> {
const searchOn = ctx.searchAvailable?.() ?? true;
return {
research: lhTool({
name: "research",
description: RESEARCH_DESCRIPTION,
schema: researchInputSchema,
execute: async (input, { toolCallId }) => dispatch(ctx, "research", toolCallId, RESEARCH_ACK, input),
...(searchOn && {
research: lhTool({
name: "research",
description: RESEARCH_DESCRIPTION,
schema: researchInputSchema,
execute: async (input, { toolCallId }) => dispatch(ctx, "research", toolCallId, RESEARCH_ACK, input),
}),
}),
demo: lhTool({
name: "demo",