/** * runResearchJob — deterministic multi-model research runner. * * Flow: * 1. Resolve >=2 distinct provider/model blocks via model-diversity. * 2. Query each block via the injected queryModel dep (no SDK imports here). * 3. Collect labelled contributions. * 4. Serialize into a markdown vault note and write it to disk. * 5. Build one Finding and emit it via the injected findingSink EXACTLY once. * 6. Return { notePath, models, finding }. * * PURE deps-injection contract: * - queryModel — provider-agnostic; CLI binds to createClient().chat() * - findingSink — hub writer; CLI binds to ingestFinding(store, f, {now}) * - now — injected ISO timestamp; never call new Date() in this module * - vaultRoot — target vault dir; must be writable by the caller * * Clock discipline: `now` is always stamped at the CLI .action() boundary. * This module never calls new Date() or Date.now(). */ import type { Finding } from "../hub/finding.js"; import type { RoleProviderBlock } from "../fleet/tier-policy.js"; import type { ResearchJob } from "./types.js"; import { type ModelContribution } from "./note-writer.js"; import { type RetrievalClient } from "./online-retrieval.js"; import type { ResearchEgressGuard } from "./egress.js"; /** Provider-agnostic query function — wraps a single LLM chat call. */ export type QueryModel = (block: RoleProviderBlock, prompt: string) => Promise; /** Hub Finding emitter — called exactly once after the note is written. */ export type FindingSink = (finding: Finding) => Promise; /** Injected dependencies for runResearchJob. */ export interface RunDeps { /** Provider-agnostic query fn — NO SDK import; CLI binds to createClient(). */ queryModel: QueryModel; /** Finding sink — CLI binds to ingestFinding(store, f, { now }). */ findingSink: FindingSink; /** Injected ISO timestamp — stamped at the CLI boundary; never read the clock here. */ now: string; /** Writable vault root directory. */ vaultRoot: string; /** Research egress guard — when absent, retrieval is skipped entirely (zero outbound). */ egress?: ResearchEgressGuard; /** Injected retrieval client — when absent, retrieval is skipped entirely. */ retrievalClient?: RetrievalClient; } /** Result returned by runResearchJob. */ export interface RunResult { /** Absolute path of the written vault note. */ notePath: string; /** Canonical model labels queried (e.g. ["openai-compat/deepseek", "openai-compat/grok"]). */ models: string[]; /** The Finding emitted to the hub. */ finding: Finding; } /** * A domain-specific analyzer can post-process contributions to produce a * custom Finding. If no analyzer is registered for a domain, the generic * default is used (multi-model synthesis with neutral defaults). * * bober: registry is a simple Map; swap for a plugin loader if the number of * domains grows beyond ~5 (the current set is research/medical/coding). * Do NOT register src/medical/ analyzers here — that is a future sprint. */ export type DomainAnalyzer = (job: ResearchJob, contributions: ModelContribution[], now: string) => Finding; /** * Register a domain-specific analyzer. The key is the domain string * (e.g. "medical"). Overwrites any existing registration for that domain. * Called by domain modules (never by src/medical/ in this sprint). */ export declare function registerAnalyzer(domain: string, analyzer: DomainAnalyzer): void; /** * Execute one research job: query >=2 distinct model blocks, write a vault * note, and emit exactly ONE Finding to the injected sink. * * @param job - The stored ResearchJob definition. * @param deps - Injected dependencies (queryModel, findingSink, now, vaultRoot). * @returns RunResult with the note path, model labels, and the emitted Finding. */ export declare function runResearchJob(job: ResearchJob, deps: RunDeps): Promise; //# sourceMappingURL=runner.d.ts.map