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
+9
View File
@@ -1,5 +1,6 @@
import { isToolUIPart, type UIMessage } from "ai";
import type { PiEngine } from "../models.ts";
import type { SearchProvider } from "../../core/search.ts";
import {
accumulateJobUsage,
buildSubagentSteer,
@@ -76,6 +77,13 @@ export interface JobRunnerDeps {
* Test overrides (below) still win; absent → config pins.
*/
engineFor?: (role: ModelRole) => PiEngine;
/** The search seam (search port wave, 2026-09-20): resolves the LIVE
* provider fresh per job — server/search.ts composition injected by
* the wiring; null = honestly unavailable (the researcher's web_search
* reports it). Absent seam (tests) reads the same. The dispatch-level
* drop (jobTools' searchAvailable) uses the same signal — jobs already
* in flight when a key disappears still fail honestly, never crash. */
searchProvider?: () => SearchProvider | null;
}
export interface JobRunner {
@@ -308,6 +316,7 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
jobStore.updateJob(jobId, { activity: "researching" });
const brief = await runResearcherAgent(prompt, {
engine: model,
search: deps.searchProvider,
abortSignal: loop.abortSignal,
// Money channel: each agent step is one model call — fold its
// usage into the record as it completes (progress write).