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:
@@ -1,6 +1,7 @@
|
||||
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
||||
import type { Usage } from "@earendil-works/pi-ai";
|
||||
import { webTools } from "../tools/web.ts";
|
||||
import { createWebTools } from "../tools/web.ts";
|
||||
import type { SearchProvider } from "../../core/search.ts";
|
||||
import { toPiTools } from "../lh-tool.ts";
|
||||
import type { PiEngine } from "../models.ts";
|
||||
import { runSubagent } from "./pi-run.ts";
|
||||
@@ -62,6 +63,12 @@ What couldn't be answered. Suggested next steps.`;
|
||||
|
||||
export interface ResearcherRunOptions {
|
||||
engine?: PiEngine;
|
||||
/** The search seam (search port wave, 2026-09-20): resolves the LIVE
|
||||
* provider per call, null = honestly unavailable (the web_search tool
|
||||
* reports that in its output — no crash, no silent fallback). Absent
|
||||
* seam reads as unavailable: the server wiring always injects one; the
|
||||
* dispatch-level drop (jobTools) keeps doomed jobs from even starting. */
|
||||
search?: () => SearchProvider | null;
|
||||
abortSignal?: AbortSignal;
|
||||
onUsage?: (usage: Usage) => void;
|
||||
onToolStart?: (toolName: string, args: unknown) => void;
|
||||
@@ -69,10 +76,11 @@ export interface ResearcherRunOptions {
|
||||
|
||||
/**
|
||||
* Run the researcher to completion: isolated context, web tools only, final
|
||||
* text = the brief. The engine is injectable so tests run the loop scripted.
|
||||
* text = the brief. The engine and search seam are injectable so tests run
|
||||
* the loop scripted.
|
||||
*/
|
||||
export async function runResearcherAgent(prompt: string, opts: ResearcherRunOptions = {}): Promise<string> {
|
||||
const { text } = await runSubagent(toPiTools(webTools) as AgentTool<any, any>[], {
|
||||
const { text } = await runSubagent(toPiTools(createWebTools(opts.search ?? (() => null))) as AgentTool<any, any>[], {
|
||||
role: "researcher",
|
||||
prompt,
|
||||
engine: opts.engine,
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user