@tangle-network/agent-knowledge 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { S as SourceRecord, c as KnowledgeIndex, d as KnowledgeSearchResult, e as KnowledgeEventType, f as KnowledgeEvent, g as KnowledgePage, b as KnowledgeGraph, h as KnowledgeLintFinding, i as KnowledgeWriteBlock, j as KnowledgeClaim, k as KnowledgeRelease, l as SourceRegistry, m as KnowledgeWriteParseResult } from './types-BEDGlXB-.js';
2
- export { C as ClaimRef, n as KnowledgeBaseCandidate, K as KnowledgeGraphEdge, a as KnowledgeGraphNode, o as KnowledgeId, p as KnowledgePolicy, q as KnowledgeRelation, r as KnowledgeUnit, s as SourceAnchor } from './types-BEDGlXB-.js';
1
+ import { S as SourceRecord, c as KnowledgeIndex, d as KnowledgeSearchResult, e as SourceRegistry, f as KnowledgeEvent, g as KnowledgeEventType, h as KnowledgePage, b as KnowledgeGraph, i as KnowledgeLintFinding, j as KnowledgeWriteBlock, k as KnowledgeClaim, l as KnowledgeRelease, m as KnowledgeWriteParseResult } from './types-C17sAROL.js';
2
+ export { C as ClaimRef, n as KnowledgeBaseCandidate, K as KnowledgeGraphEdge, a as KnowledgeGraphNode, o as KnowledgeId, p as KnowledgePolicy, q as KnowledgeRelation, r as KnowledgeUnit, s as SourceAnchor } from './types-C17sAROL.js';
3
+ import { KnowledgeRequirementCategory, KnowledgeAcquisitionMode, KnowledgeImportance, KnowledgeFreshness, KnowledgeSensitivity, KnowledgeRequirement, KnowledgeBundle, KnowledgeReadinessReport, UserQuestion, DataAcquisitionPlan, AnalystSeverity, AnalystFinding, RunRecord, ReleaseTraceEvidence, GateDecision, ReleaseConfidenceScorecard, ControlRuntimeConfig, ControlEvalResult } from '@tangle-network/agent-eval';
3
4
  import { KnowledgeFragment } from './sources/index.js';
4
5
  export { CornellLiiSelector, CornellLiiSourceOptions, FetchOpts, FragmentProvenance, IRS_DIMENSION_HINTS, IrsPublicationsSourceOptions, KnowledgeSource, MAX_RESPONSE_BYTES, MIN_REQUEST_GAP_MS, POLITE_USER_AGENT, PoliteFetchOptions, PoliteFetchResult, StateSosEntity, StateSosSourceConfig, __resetHttpThrottle, createCornellLiiSource, createIrsPublicationsSource, createStateSosSource, extractLinks, firstMatch, htmlToText, innerHtmlById, looksLikeBlockPage, politeFetch } from './sources/index.js';
5
- import { KnowledgeRequirementCategory, KnowledgeAcquisitionMode, KnowledgeImportance, KnowledgeFreshness, KnowledgeSensitivity, KnowledgeRequirement, KnowledgeBundle, KnowledgeReadinessReport, UserQuestion, DataAcquisitionPlan, AnalystSeverity, AnalystFinding, RunRecord, ReleaseTraceEvidence, GateDecision, ReleaseConfidenceScorecard, ControlRuntimeConfig, ControlEvalResult } from '@tangle-network/agent-eval';
6
6
  export { AgentMemoryAdapter, AgentMemoryContext, AgentMemoryHit, AgentMemoryHitSchema, AgentMemoryKind, AgentMemoryKindSchema, AgentMemoryScope, AgentMemoryScopeSchema, AgentMemorySearchOptions, AgentMemoryWriteInput, AgentMemoryWriteInputSchema, AgentMemoryWriteResult, Neo4jAgentMemoryAdapterOptions, createNeo4jAgentMemoryAdapter, defaultGetMemoryContext, memoryHitToSourceRecord, memoryWriteResultToSourceRecord, renderMemoryContext } from './memory/index.js';
7
7
  import { Budget, ExecutorConfig, SuperviseOptions, DeliverableSpec, SupervisedResult } from '@tangle-network/agent-runtime/loops';
8
8
  import { z } from 'zod';
@@ -28,140 +28,6 @@ interface SourceAdapter {
28
28
  declare const textSourceAdapter: SourceAdapter;
29
29
  declare function mediaTypeFor(uri: string): string;
30
30
 
31
- /**
32
- * Change detection across snapshots of one source's fragments.
33
- *
34
- * The output drives the continuous-ingestion loop: each `KnowledgeChange`
35
- * carries the eval dimensions affected (`affectedDimensions`), which an
36
- * agent-eval campaign scheduler consumes to decide which campaigns to
37
- * re-run. Three change kinds:
38
- *
39
- * - `added` — fragment id appears in `next` but not `prev`.
40
- * - `removed` — fragment id appears in `prev` but not `next`. Typical
41
- * trigger: an authority retires a Wex slug, or a state SOS reorganises
42
- * its forms catalogue.
43
- * - `modified` — fragment id appears in both, body hash differs. This
44
- * is the dominant change kind in practice — the Ryan-LLC v. FTC
45
- * vacatur case manifests as a `modified` on the Wex non-compete
46
- * fragment, NOT as a removed one.
47
- *
48
- * Unverifiable fragments are filtered out before diffing — comparing a
49
- * captcha-blocked snapshot against a real one would falsely fire every
50
- * fragment as removed. The caller can inspect the raw lists if they want
51
- * to surface block-page failures.
52
- *
53
- * Within-snapshot duplicate ids are an upstream bug; this function picks
54
- * the LAST one and emits a diagnostic via the `warnings` field.
55
- *
56
- * @stable
57
- */
58
- type KnowledgeChangeKind = 'added' | 'removed' | 'modified';
59
- interface KnowledgeChange {
60
- /** Source-scoped id (matches `KnowledgeFragment.id`). */
61
- fragmentId: string;
62
- kind: KnowledgeChangeKind;
63
- /**
64
- * For `added`: full body of the new fragment.
65
- * For `removed`: full body of the prior fragment.
66
- * For `modified`: unified-diff-style payload `{ before, after }` body strings.
67
- */
68
- diff?: {
69
- before?: string;
70
- after?: string;
71
- };
72
- /**
73
- * Eval dimensions to re-score. Computed as the union of both fragments'
74
- * `dimensionHints`. The eval cron treats this as a set of campaign tags.
75
- */
76
- affectedDimensions: string[];
77
- /** URL of the affected authority page (from whichever side has it). */
78
- url?: string;
79
- /**
80
- * Source-attested change time. For `modified`, takes the NEXT fragment's
81
- * `sourceUpdatedAt`. For `removed`, takes the PRIOR fragment's
82
- * `sourceUpdatedAt`. For `added`, takes the NEXT fragment's
83
- * `sourceUpdatedAt`. Consumers index changes by this date.
84
- */
85
- detectedAt: string;
86
- }
87
- interface DetectChangesResult {
88
- changes: KnowledgeChange[];
89
- /** Counts by kind — handy for dashboards. */
90
- summary: {
91
- added: number;
92
- removed: number;
93
- modified: number;
94
- };
95
- /** Non-fatal diagnostics (duplicate ids, dropped unverifiable fragments). */
96
- warnings: string[];
97
- }
98
- interface DetectChangesOptions {
99
- /**
100
- * When true (default), unverifiable fragments are dropped from both
101
- * sides before comparison. Set false ONLY when debugging block-page
102
- * issues — comparing against unverifiable content emits false
103
- * `removed`/`modified` changes.
104
- */
105
- skipUnverifiable?: boolean;
106
- /**
107
- * When provided, only changes whose `affectedDimensions` intersect this
108
- * set are returned. Useful for cron loops that schedule per-dimension
109
- * eval campaigns and only care about a subset.
110
- */
111
- filterDimensions?: string[];
112
- }
113
- declare function detectChanges(prev: KnowledgeFragment[], next: KnowledgeFragment[], options?: DetectChangesOptions): DetectChangesResult;
114
-
115
- interface ChunkingOptions {
116
- targetChars: number;
117
- maxChars: number;
118
- minChars: number;
119
- overlapChars: number;
120
- }
121
- interface KnowledgeChunk {
122
- index: number;
123
- text: string;
124
- headingPath: string;
125
- charStart: number;
126
- charEnd: number;
127
- oversized: boolean;
128
- }
129
- declare function chunkMarkdown(content: string, options?: Partial<ChunkingOptions>): KnowledgeChunk[];
130
- declare function stripFrontmatter(content: string): {
131
- body: string;
132
- bodyOffset: number;
133
- };
134
-
135
- interface DiscoveryTask {
136
- id: string;
137
- goal: string;
138
- query?: string;
139
- sourceHints?: string[];
140
- metadata?: Record<string, unknown>;
141
- }
142
- interface DiscoveryResult {
143
- taskId: string;
144
- summary: string;
145
- sourceUris?: string[];
146
- claims?: Array<{
147
- text: string;
148
- sourceUri?: string;
149
- confidence?: number;
150
- }>;
151
- followUpTasks?: DiscoveryTask[];
152
- metadata?: Record<string, unknown>;
153
- }
154
- interface KnowledgeDiscoveryWorker {
155
- run(task: DiscoveryTask, signal?: AbortSignal): Promise<DiscoveryResult> | DiscoveryResult;
156
- }
157
- interface KnowledgeDiscoveryDispatcher {
158
- dispatch(tasks: DiscoveryTask[], options?: {
159
- concurrency?: number;
160
- signal?: AbortSignal;
161
- }): Promise<DiscoveryResult[]>;
162
- }
163
- declare function createLocalDiscoveryDispatcher(worker: KnowledgeDiscoveryWorker): KnowledgeDiscoveryDispatcher;
164
-
165
31
  interface KnowledgeReadinessSpec {
166
32
  id: string;
167
33
  description: string;
@@ -252,1059 +118,1493 @@ interface EvalKnowledgeBundleBuildResult {
252
118
  }
253
119
  declare function buildEvalKnowledgeBundle(options: BuildEvalKnowledgeBundleOptions): EvalKnowledgeBundleBuildResult;
254
120
 
255
- interface KnowledgeEventQuery {
256
- type?: KnowledgeEventType;
257
- target?: string;
258
- limit?: number;
121
+ interface AddSourceOptions {
122
+ copyIntoRaw?: boolean;
123
+ adapters?: SourceAdapter[];
124
+ now?: () => Date;
259
125
  }
260
- declare function createKnowledgeEvent(input: {
261
- type: KnowledgeEventType;
262
- actor?: string;
263
- target?: string;
126
+ interface AddSourceTextInput {
127
+ uri: string;
128
+ text: string;
129
+ title?: string;
130
+ mediaType?: string;
131
+ validUntil?: string;
132
+ lastVerifiedAt?: string;
264
133
  metadata?: Record<string, unknown>;
265
- now?: () => Date;
266
- }): KnowledgeEvent;
134
+ }
135
+ declare function loadSourceRegistry(root: string): Promise<SourceRegistry>;
136
+ declare function writeSourceRegistry(root: string, registry: SourceRegistry): Promise<void>;
137
+ declare function addSourcePath(root: string, sourcePath: string, options?: AddSourceOptions): Promise<SourceRecord[]>;
138
+ declare function addSourceText(root: string, input: AddSourceTextInput, options?: Pick<AddSourceOptions, 'adapters' | 'now'>): Promise<SourceRecord>;
139
+ declare function sourceRegistryPath(root: string): string;
267
140
 
268
141
  /**
269
- * Knowledge freshness store: tracks when each `(workspaceId, sourceId)` pair
270
- * was last successfully refreshed, and reports staleness against a TTL.
271
- *
272
- * The contract is intentionally minimal — just enough to drive a cron loop:
273
- *
274
- * ```ts
275
- * const store = createFileSystemFreshnessStore({ root: '.agent-knowledge' })
276
- * for (const source of sources) {
277
- * if (await store.stale({ workspaceId, sourceId: source.id, ttlMs: DAY })) {
278
- * const fragments = await source.fetch({ cacheDir })
279
- * await persistFragments(fragments)
280
- * await store.mark({ workspaceId, sourceId: source.id, when: new Date() })
281
- * }
282
- * }
283
- * ```
284
- *
285
- * Per-tenant isolation is enforced by `workspaceId` keying — there is no
286
- * global mutable state across workspaces.
287
- *
288
- * Two adapters ship in-package:
289
- *
290
- * - `createFileSystemFreshnessStore` — JSON file under the knowledge root,
291
- * mirrors the layout convention already used by `sources.json`.
292
- * - `createD1FreshnessStoreStub` — adapter scaffold for Cloudflare D1 /
293
- * Postgres. Production consumers should implement the `D1Adapter`
294
- * interface inside their own app; this stub exists to anchor the shape.
295
- *
296
- * @stable contract — interface is frozen at 0.x within this major.
297
- * @stable filesystem adapter
298
- * @experimental D1 stub — interface will evolve as real consumers wire it.
142
+ * A knowledge gap the loop surfaces from `scoreKnowledgeReadiness`. The worker
143
+ * targets these; the driver folds the unfilled remainder into the worker's next
144
+ * prompt and runs its own gap-fill pass over them.
299
145
  */
300
- /** Identity for one freshness record. */
301
- interface FreshnessKey {
302
- workspaceId: string;
303
- sourceId: string;
304
- }
305
- /** TTL bound for staleness checks. */
306
- interface FreshnessTtl extends FreshnessKey {
307
- /** Milliseconds `Date.now() - last() > ttlMs` stale. */
308
- ttlMs: number;
309
- /** Injected clock for deterministic tests; defaults to system time. */
310
- now?: Date;
311
- }
312
- /** Mark argument. */
313
- interface FreshnessMark extends FreshnessKey {
314
- when: Date;
315
- /** Optional content hash captured at refresh time; aids debugging. */
316
- contentHash?: string;
317
- }
318
- interface KnowledgeFreshnessStore {
319
- /** Last refresh time, or null if never refreshed. */
320
- last(key: FreshnessKey): Promise<Date | null>;
321
- /** Record a successful refresh. */
322
- mark(input: FreshnessMark): Promise<void>;
323
- /** True iff `last(key)` is null or older than `ttlMs`. */
324
- stale(input: FreshnessTtl): Promise<boolean>;
325
- /** All records for a workspace — useful for dashboards / debugging. */
326
- list(workspaceId: string): Promise<FreshnessRecord[]>;
327
- }
328
- interface FreshnessRecord {
329
- workspaceId: string;
330
- sourceId: string;
331
- lastRefreshedAt: string;
332
- contentHash?: string;
333
- }
334
- interface FileSystemFreshnessStoreOptions {
335
- /**
336
- * Knowledge root. The store writes to `<root>/.agent-knowledge/freshness.json`,
337
- * mirroring the convention used by `sources.json`.
338
- */
339
- root: string;
146
+ interface KnowledgeGap {
147
+ /** Readiness-spec id this gap belongs to. */
148
+ id: string;
149
+ /** Human-readable description of what's missing. */
150
+ description: string;
151
+ /** The search query the readiness check ran for this requirement. */
152
+ query: string;
153
+ /** True when the gap blocks readiness (vs. a soft, non-blocking gap). */
154
+ blocking: boolean;
340
155
  }
156
+ /** A new source the worker (or driver) discovered and wants to add to the KB. */
157
+ type ResearchSourceProposal = AddSourceTextInput;
341
158
  /**
342
- * Filesystem-backed implementation. Single JSON file per knowledge root,
343
- * indexed by `${workspaceId}::${sourceId}`. Reads parse on every call
344
- * cron tick rate is well below the cost of one JSON parse.
159
+ * What a research agent contributes in one round. Both the worker and (when
160
+ * `driverResearches` is on) the driver produce this shape the worker ADDS
161
+ * primary findings, the driver gap-FILLS the ones the worker missed.
345
162
  *
346
- * Concurrent writes from a single process serialize through `writeQueue`.
347
- * Cross-process concurrency is undefined; the consuming app should run the
348
- * cron in a single worker.
349
- */
350
- declare function createFileSystemFreshnessStore(options: FileSystemFreshnessStoreOptions): KnowledgeFreshnessStore;
351
- /**
352
- * D1 / Postgres adapter scaffold. Production consumers implement
353
- * `D1Adapter` against their own driver (better-sqlite3, postgres,
354
- * Cloudflare D1 binding, ...). This factory wires the adapter to the
355
- * `KnowledgeFreshnessStore` interface.
356
- *
357
- * The expected schema:
358
- *
359
- * ```sql
360
- * CREATE TABLE knowledge_freshness (
361
- * workspace_id TEXT NOT NULL,
362
- * source_id TEXT NOT NULL,
363
- * last_refreshed_at TEXT NOT NULL,
364
- * content_hash TEXT,
365
- * PRIMARY KEY (workspace_id, source_id)
366
- * );
367
- * ```
163
+ * `proposalText` is the safe write-protocol text (`---FILE: knowledge/...---`
164
+ * blocks). The loop only applies it AFTER the driver has verified the round's
165
+ * sources, so a rejected source never reaches the curated pages.
368
166
  */
369
- interface D1Adapter {
370
- get(workspaceId: string, sourceId: string): Promise<FreshnessRecord | null>;
371
- upsert(record: FreshnessRecord): Promise<void>;
372
- listByWorkspace(workspaceId: string): Promise<FreshnessRecord[]>;
167
+ interface ResearchContribution {
168
+ /** Immutable sources to register (the raw evidence). */
169
+ sources?: ResearchSourceProposal[];
170
+ /** Safe write-protocol text producing curated `knowledge/*.md` pages. */
171
+ proposalText?: string;
172
+ /**
173
+ * Build the page write-protocol text FROM the sources the driver accepted —
174
+ * the curated, citing pages the readiness gate searches. Receives the
175
+ * registered `SourceRecord`s (with their assigned ids, so a page's frontmatter
176
+ * `sources:` can cite them). Returns `---FILE: knowledge/...---` block text or
177
+ * `undefined`. Runs after verification, so a page never cites a rejected
178
+ * source. Concatenated after any static `proposalText`.
179
+ */
180
+ buildPages?: (acceptedSources: SourceRecord[]) => string | undefined;
181
+ /** Free-form research transcript — products can persist this. */
182
+ notes?: string;
183
+ metadata?: Record<string, unknown>;
373
184
  }
374
- declare function createD1FreshnessStoreStub(adapter: D1Adapter): KnowledgeFreshnessStore;
375
-
376
- interface ParsedFrontmatter {
377
- frontmatter: Record<string, unknown>;
378
- body: string;
185
+ /** Context handed to the worker each round. */
186
+ interface WorkerResearchContext {
187
+ root: string;
188
+ goal: string;
189
+ round: number;
190
+ index: KnowledgeIndex;
191
+ /** Gaps the readiness gate currently reports — what the worker should close. */
192
+ gaps: KnowledgeGap[];
193
+ /** Steer text the driver folded in from the previous round's remaining gaps. */
194
+ steer?: string;
195
+ readiness: EvalKnowledgeBundleBuildResult;
196
+ signal?: AbortSignal;
379
197
  }
380
- declare function parseFrontmatter(content: string): ParsedFrontmatter;
381
- declare function formatFrontmatter(frontmatter: Record<string, unknown>, body: string): string;
382
-
383
- declare function buildKnowledgeGraph(pages: KnowledgePage[]): KnowledgeGraph;
384
-
385
- declare function sha256(text: string): string;
386
- declare function slugify(input: string): string;
387
- declare function stableId(prefix: string, content: string): string;
388
-
389
- declare function buildKnowledgeIndex(root: string): Promise<KnowledgeIndex>;
390
- declare function writeKnowledgeIndex(root: string): Promise<KnowledgeIndex>;
391
-
392
- interface KnowledgeInspection {
393
- pageCount: number;
394
- sourceCount: number;
395
- expiredSourceCount: number;
396
- staleSourceCount: number;
397
- edgeCount: number;
398
- findingCount: number;
399
- blockingFindingCount: number;
400
- topPages: Array<{
401
- path: string;
402
- title: string;
403
- degree: number;
404
- sources: number;
405
- }>;
406
- sourceFreshness: SourceFreshnessInspection[];
407
- findings: KnowledgeLintFinding[];
198
+ /** Context handed to the driver's verifier for one candidate source. */
199
+ interface SourceVerificationContext {
200
+ root: string;
201
+ goal: string;
202
+ round: number;
203
+ index: KnowledgeIndex;
204
+ gaps: KnowledgeGap[];
205
+ /** Sources already accepted earlier THIS round (in-round dedup). */
206
+ acceptedThisRound: ResearchSourceProposal[];
207
+ signal?: AbortSignal;
408
208
  }
409
- interface SourceFreshnessInspection {
410
- id: string;
411
- title?: string;
412
- uri: string;
413
- status: 'fresh' | 'expired' | 'unknown';
414
- validUntil?: string;
415
- lastVerifiedAt?: string;
209
+ /** A single rejected source plus the reason the driver gave. */
210
+ interface RejectedSource {
211
+ source: ResearchSourceProposal;
212
+ reason: string;
416
213
  }
417
- declare function inspectKnowledgeIndex(index: KnowledgeIndex, options?: {
418
- now?: Date;
419
- }): KnowledgeInspection;
420
- interface KnowledgeExplanation {
421
- target: string;
422
- page?: KnowledgePage;
423
- sources: Array<{
424
- id: string;
425
- title?: string;
426
- uri: string;
427
- }>;
428
- links: string[];
429
- inbound: string[];
430
- related: Array<{
431
- path: string;
432
- title: string;
433
- score: number;
434
- }>;
214
+ /** Context handed to the driver's gap-fill pass (only when `driverResearches`). */
215
+ interface DriverResearchContext {
216
+ root: string;
217
+ goal: string;
218
+ round: number;
219
+ index: KnowledgeIndex;
220
+ /** Gaps STILL open after the worker's accepted contribution applied. */
221
+ remainingGaps: KnowledgeGap[];
222
+ readiness: EvalKnowledgeBundleBuildResult;
223
+ signal?: AbortSignal;
435
224
  }
436
- declare function explainKnowledgeTarget(index: KnowledgeIndex, target: string): KnowledgeExplanation;
437
-
438
- interface KbStore {
439
- putSource(source: SourceRecord): Promise<void>;
440
- getSource(id: string): Promise<SourceRecord | null>;
441
- listSources(): Promise<SourceRecord[]>;
442
- putPage(page: KnowledgePage): Promise<void>;
443
- getPage(idOrPath: string): Promise<KnowledgePage | null>;
444
- listPages(): Promise<KnowledgePage[]>;
445
- putIndex(index: KnowledgeIndex): Promise<void>;
446
- getIndex(): Promise<KnowledgeIndex | null>;
447
- putEvent(event: KnowledgeEvent): Promise<void>;
448
- listEvents(query?: KnowledgeEventQuery): Promise<KnowledgeEvent[]>;
225
+ /**
226
+ * The differentiated driver role.
227
+ *
228
+ * - `verifySource` — the gate the worker's additions pass before they commit.
229
+ * Return `{ accept: true }` to keep a source or `{ accept: false, reason }`
230
+ * to reject it (not real / not relevant / duplicate). The loop dedups exact
231
+ * duplicates (same `uri` already in the KB or accepted this round) BEFORE
232
+ * calling this, so the verifier only sees genuinely-new candidates.
233
+ * - `research` — the driver's OWN gap-fill pass over the gaps the worker left
234
+ * open. Only invoked when `driverResearches` is true.
235
+ * - `foldGaps` — turn the remaining gaps into a steer string for the worker's
236
+ * next prompt. Defaults to a compact bulleted list when omitted.
237
+ */
238
+ interface ResearchDriver {
239
+ verifySource(source: ResearchSourceProposal, ctx: SourceVerificationContext): Promise<SourceVerdict> | SourceVerdict;
240
+ research?(ctx: DriverResearchContext): Promise<ResearchContribution> | ResearchContribution;
241
+ foldGaps?(gaps: KnowledgeGap[]): string;
449
242
  }
450
- declare class MemoryKbStore implements KbStore {
451
- private readonly sources;
452
- private readonly pages;
453
- private readonly events;
454
- private index;
455
- putSource(source: SourceRecord): Promise<void>;
456
- getSource(id: string): Promise<SourceRecord | null>;
457
- listSources(): Promise<SourceRecord[]>;
458
- putPage(page: KnowledgePage): Promise<void>;
459
- getPage(idOrPath: string): Promise<KnowledgePage | null>;
460
- listPages(): Promise<KnowledgePage[]>;
461
- putIndex(index: KnowledgeIndex): Promise<void>;
462
- getIndex(): Promise<KnowledgeIndex | null>;
463
- putEvent(event: KnowledgeEvent): Promise<void>;
464
- listEvents(query?: KnowledgeEventQuery): Promise<KnowledgeEvent[]>;
243
+ type SourceVerdict = {
244
+ accept: true;
245
+ } | {
246
+ accept: false;
247
+ reason: string;
248
+ };
249
+ /** The worker: primary research targeting the round's gaps. */
250
+ type ResearchWorker = (ctx: WorkerResearchContext) => Promise<ResearchContribution> | ResearchContribution;
251
+ interface TwoAgentResearchLoopOptions {
252
+ root: string;
253
+ goal: string;
254
+ worker: ResearchWorker;
255
+ driver: ResearchDriver;
256
+ /**
257
+ * When false (default), the driver ONLY verifies + gates — a pure coordinator
258
+ * that contributes no research of its own (the "doesn't participate in the
259
+ * work" mode). When true, the driver also runs its `research` gap-fill pass
260
+ * each round over the gaps the worker left open.
261
+ */
262
+ driverResearches?: boolean;
263
+ maxRounds?: number;
264
+ actor?: string;
265
+ /** Readiness specs define the gate; an empty list means the loop never gates. */
266
+ readinessSpecs?: KnowledgeReadinessSpec[];
267
+ readinessTaskId?: string;
268
+ readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
269
+ sourceOptions?: Pick<AddSourceOptions, 'adapters' | 'now'>;
270
+ signal?: AbortSignal;
271
+ onRound?: (round: TwoAgentResearchRound) => Promise<void> | void;
465
272
  }
466
- declare class FileSystemKbStore extends MemoryKbStore {
467
- private readonly dir;
468
- constructor(dir: string);
469
- putIndex(index: KnowledgeIndex): Promise<void>;
470
- getIndex(): Promise<KnowledgeIndex | null>;
471
- putEvent(event: KnowledgeEvent): Promise<void>;
273
+ interface TwoAgentResearchRound {
274
+ round: number;
275
+ /** Gaps reported at the START of the round (what the worker targeted). */
276
+ gaps: KnowledgeGap[];
277
+ /** Worker sources accepted by the driver and written to the KB. */
278
+ acceptedWorkerSources: SourceRecord[];
279
+ /** Worker sources the driver rejected (with reasons) — never written. */
280
+ rejectedWorkerSources: RejectedSource[];
281
+ /** Sources the driver itself added in its gap-fill pass. */
282
+ driverSources: SourceRecord[];
283
+ /** Curated pages written this round (worker proposal + driver proposal). */
284
+ writtenPages: string[];
285
+ readiness?: EvalKnowledgeBundleBuildResult;
286
+ /** True once the readiness gate reports no blocking gaps. */
287
+ ready: boolean;
288
+ event: KnowledgeEvent;
289
+ notes: {
290
+ worker?: string;
291
+ driver?: string;
292
+ };
472
293
  }
473
-
474
- declare function lintKnowledgeIndex(index: KnowledgeIndex): KnowledgeLintFinding[];
475
-
476
- interface ApplyWriteBlocksResult {
477
- written: string[];
478
- warnings: string[];
294
+ interface TwoAgentResearchLoopResult {
295
+ root: string;
296
+ goal: string;
297
+ rounds: number;
298
+ ready: boolean;
299
+ index: KnowledgeIndex;
300
+ readiness?: EvalKnowledgeBundleBuildResult;
301
+ steps: TwoAgentResearchRound[];
479
302
  }
480
- declare function applyKnowledgeWriteBlocks(root: string, proposalText: string): Promise<ApplyWriteBlocksResult>;
481
- declare function applyKnowledgeWriteBlocksFile(root: string, proposalPath: string): Promise<ApplyWriteBlocksResult>;
482
-
483
303
  /**
484
- * Bridge from `AnalystFinding` (agent-eval) to knowledge proposals.
304
+ * Two-agent (driver + worker) sibling of `runKnowledgeResearchLoop`.
485
305
  *
486
- * Closes the failure wiki side of the recursive-self-improvement
487
- * loop: a knowledge-gap or knowledge-poisoning finding produced by an
488
- * analyst becomes a concrete proposal an operator (or auto-merge bot)
489
- * can review and apply. The bridge is intentionally lossless on the
490
- * fail-loud side — a finding the parser can't classify returns a
491
- * `KnowledgeProposalParseError` rather than a silent skip, so the
492
- * loop never accepts an underspecified edit.
306
+ * Both agents research to grow ONE knowledge base. The roles are differentiated:
493
307
  *
494
- * Subject grammar this bridge understands (analyst-side convention,
495
- * stamped in the kind prompts):
308
+ * - **WORKER** = primary research. Each round it reads the open gaps, discovers
309
+ * new sources, and proposes additions (`sources` + `proposalText`). It ADDS.
310
+ * - **DRIVER** = the verifier / gap-filler / gate. It (1) VERIFIES the worker's
311
+ * sources before they commit — dedup against the KB, then `verifySource`
312
+ * rejects ones that aren't real/relevant; (2) GAP-FILLS the gaps the worker
313
+ * missed with its own research pass (when `driverResearches`); (3) folds the
314
+ * remaining gaps into the worker's next prompt; and (4) GATES on
315
+ * `scoreKnowledgeReadiness` — the loop stops as soon as there are no blocking
316
+ * gaps.
496
317
  *
497
- * agent-knowledge:wiki:<page-slug> create / update page
498
- * agent-knowledge:wiki:<page-slug>#<heading> insert section under page
499
- * agent-knowledge:claim:<topic> draft claim row
500
- * agent-knowledge:raw:<source-id> lift raw → curated
501
- * agent-knowledge:stale:<page-slug> mark page superseded
318
+ * Set `driverResearches: false` (default) for the pure-coordinator mode: the
319
+ * driver only verifies + gates and contributes no research itself.
502
320
  *
503
- * Anything else (websearch:outdated:*, tool-doc:*, system-prompt:*,
504
- * memory:*) is NOT a knowledge-base concern and returns `null` so the
505
- * loop's improvement-applier handles it.
321
+ * Composes existing atoms `initKnowledgeBase`, `addSourceText`,
322
+ * `applyKnowledgeWriteBlocks`, `buildEvalKnowledgeBundle` (the readiness gate),
323
+ * and `searchKnowledge` and reinvents none of them.
506
324
  */
325
+ declare function runTwoAgentResearchLoop(options: TwoAgentResearchLoopOptions): Promise<TwoAgentResearchLoopResult>;
326
+ /**
327
+ * Helper for verifiers: does the candidate source's text/title overlap any page
328
+ * the readiness search returns for a gap query? A cheap relevance heuristic the
329
+ * driver can compose into `verifySource` (real verifiers can do more).
330
+ */
331
+ declare function sourceMatchesGaps(source: ResearchSourceProposal, index: KnowledgeIndex, gaps: KnowledgeGap[]): KnowledgeSearchResult[];
507
332
 
508
- interface KnowledgeProposal {
509
- /**
510
- * Stable id derived from the finding so cross-run diffs share an
511
- * identity. Re-proposing the same finding produces the same id.
512
- */
513
- id: string;
514
- /** The finding that generated this proposal — useful for audit + revert. */
515
- sourceFindingId: string;
516
- /** What the proposal does. */
517
- kind: 'create-page' | 'update-page' | 'append-section' | 'create-claim' | 'lift-raw' | 'mark-stale';
518
- /** Locus on disk (page slug or claim topic). */
519
- locus: string;
520
- /**
521
- * Page write blocks the standard `applyKnowledgeWriteBlocks` consumer
522
- * accepts. Empty for proposals that don't change page text (e.g.
523
- * `create-claim` produces a `claim` field instead).
524
- */
525
- writeBlocks: KnowledgeWriteBlock[];
526
- /**
527
- * Granular claim draft for proposals whose unit-of-change is a claim
528
- * row rather than a whole page. `status: 'draft'` until reviewed.
529
- */
530
- claim?: KnowledgeClaim;
531
- /** Per-proposal metadata: severity, confidence, source span. */
532
- metadata: {
533
- severity: AnalystSeverity;
534
- confidence: number;
535
- evidence_uri?: string;
536
- analyst_id: string;
537
- };
538
- }
539
- declare class KnowledgeProposalParseError extends Error {
540
- readonly findingId: string;
541
- readonly subject: string;
542
- constructor(findingId: string, subject: string, message: string);
543
- }
544
333
  /**
545
- * Convert one `AnalystFinding` into a knowledge proposal. Returns
546
- * `null` when the finding's locus isn't a knowledge-base concern
547
- * (`websearch:outdated:*`, `tool-doc:*`, `system-prompt:*`,
548
- * `memory:*`, missing subject). Throws when the locus IS a
549
- * knowledge-base concern but is malformed — that's a bug in the
550
- * analyst prompt and should fail loud.
334
+ * Real web-research worker + verifying driver for `runTwoAgentResearchLoop`.
551
335
  *
552
- * Caller convention: feed the function the analyst's full findings
553
- * list and filter out the `null`s; the orchestrator passes the
554
- * remaining proposals to the existing review / apply pipeline.
336
+ * This is the GENERAL, any-topic implementation behind the two-agent research
337
+ * loop's live arm. Given the open knowledge gaps the readiness gate surfaces,
338
+ * the worker:
339
+ *
340
+ * 1. asks an LLM (glm-5.2 by default) to turn each gap into focused web
341
+ * search queries,
342
+ * 2. runs a REAL web search over the Tangle router (`POST /v1/search` — the
343
+ * same endpoint `tcloud mcp`'s `web_search` tool forwards to), so there is
344
+ * no hardcoded corpus,
345
+ * 3. fetches the top results with the repo's polite, cached `politeFetch` and
346
+ * reduces each page to text with `htmlToText`,
347
+ * 4. proposes the readable, verifiable pages as `ResearchSourceProposal`s plus
348
+ * a `buildPages` that writes citing `knowledge/*.md` pages from the sources
349
+ * the driver accepts.
350
+ *
351
+ * The verifying DRIVER is the differentiated role from the two-agent loop: a
352
+ * second LLM pass that judges each fetched source's on-topic relevance to the
353
+ * goal + open gaps and rejects off-topic / spam / already-covered material. The
354
+ * worker ADDS; the driver GATES. Together they build a cleaner knowledge base
355
+ * than a single agent at the same compute budget.
356
+ *
357
+ * Dependency-free on purpose: it talks to the router over `fetch` directly with
358
+ * the published OpenAI-compatible chat shape and the `/v1/search` shape, so it
359
+ * works whether or not the `tcloud` CLI is installed. Point it at any router by
360
+ * passing `baseUrl`; supply the key via `apiKey` or `TANGLE_API_KEY`.
555
361
  */
556
- declare function proposeFromFinding(finding: AnalystFinding): KnowledgeProposal | null;
362
+
363
+ /** One live web result, as the router's `/v1/search` returns it. */
364
+ interface WebSearchHit {
365
+ title: string;
366
+ url: string;
367
+ snippet?: string;
368
+ }
557
369
  /**
558
- * Plural convenience: filter + map across an entire findings batch
559
- * with one call. Parse errors collect into `errors[]`; the loop
560
- * decides per-error whether to abort or continue.
370
+ * The two router capabilities the worker/driver need. Injectable so tests can
371
+ * stub the network; the default talks to the live Tangle router over `fetch`.
561
372
  */
562
- interface ProposeFromFindingsResult {
563
- proposals: KnowledgeProposal[];
564
- skipped: number;
565
- errors: KnowledgeProposalParseError[];
373
+ interface RouterClient {
374
+ /** Live web search — returns title/url/snippet hits. */
375
+ search(query: string, opts?: {
376
+ maxResults?: number;
377
+ }): Promise<WebSearchHit[]>;
378
+ /** Chat completion — returns the assistant message's visible text. */
379
+ chat(messages: {
380
+ role: 'system' | 'user';
381
+ content: string;
382
+ }[], maxTokens?: number): Promise<string>;
383
+ /** Cumulative cost (chat + search) since this client was created. */
384
+ usage(): RouterUsage;
566
385
  }
567
- declare function proposeFromFindings(findings: ReadonlyArray<AnalystFinding>): ProposeFromFindingsResult;
568
-
569
- interface KnowledgeReleaseReport {
570
- release: KnowledgeRelease;
571
- scorecard: ReleaseConfidenceScorecard;
572
- candidateRuns: RunRecord[];
573
- baselineRuns: RunRecord[];
386
+ /**
387
+ * Cumulative router cost — the per-arm signal the A/B reports ALONGSIDE quality,
388
+ * so "2.3 fewer sources" can be read against the token/$/latency it cost. A
389
+ * two-agent round is one worker pass plus N `verifySource` LLM calls; counting
390
+ * each call here is what surfaces that the two-agent loop spends more inference
391
+ * than its "equal passes" budget implies.
392
+ */
393
+ interface RouterUsage {
394
+ chatCalls: number;
395
+ searchCalls: number;
396
+ promptTokens: number;
397
+ completionTokens: number;
398
+ usd: number;
399
+ wallMs: number;
400
+ }
401
+ interface TangleRouterOptions {
402
+ /** Router base URL. Defaults to `https://router.tangle.tools/v1`. */
403
+ baseUrl?: string;
404
+ /** Bearer key. Defaults to `process.env.TANGLE_API_KEY`. */
405
+ apiKey?: string;
406
+ /** Chat model id. Defaults to `glm-5.2`. */
407
+ model?: string;
408
+ /** Optional preferred search provider (exa | you | perplexity | …). */
409
+ searchProvider?: string;
410
+ signal?: AbortSignal;
411
+ }
412
+ /** A small error so a failed router call fails loud rather than returning junk. */
413
+ declare class RouterError extends Error {
414
+ readonly status: number;
415
+ constructor(status: number, message: string);
574
416
  }
575
417
  /**
576
- * Campaign-native release report. The caller (a consumer's KB self-improvement
577
- * loop) supplies the candidate/baseline `RunRecord[]` (e.g. via
578
- * `campaignToRunRecords`) + optional per-instance `ReleaseTraceEvidence` + the
579
- * gate decision; this folds them into a `ReleaseConfidenceScorecard` + a
580
- * `KnowledgeRelease`. Release confidence is computed from run records + traces,
581
- * independent of any optimizer result shape.
418
+ * Build a dependency-free Tangle router client over `fetch`. This is the same
419
+ * wire surface the `tcloud` SDK + `tcloud mcp` use (`/v1/search` for web search,
420
+ * `/v1/chat/completions` for chat) so it needs no CLI installed.
582
421
  */
583
- interface KnowledgeReleaseInput {
584
- candidateId: string;
585
- baselineId?: string;
586
- candidateRuns: RunRecord[];
587
- baselineRuns?: RunRecord[];
588
- traces?: ReleaseTraceEvidence[];
589
- gateDecision?: GateDecision | null;
422
+ declare function createTangleRouterClient(options?: TangleRouterOptions): RouterClient;
423
+ interface WebResearchWorkerOptions {
424
+ /** Router client. Defaults to a live Tangle router client from env creds. */
425
+ router?: RouterClient;
426
+ router_options?: TangleRouterOptions;
427
+ /** Max search queries the LLM may form per gap. Default 2. */
428
+ queriesPerGap?: number;
429
+ /** Max web results fetched per query. Default 3. */
430
+ resultsPerQuery?: number;
431
+ /** Hard cap on sources proposed per round (across all gaps). Default 6. */
432
+ maxSourcesPerRound?: number;
433
+ /** Disk cache dir for `politeFetch`. Optional; speeds repeat runs. */
434
+ cacheDir?: string;
435
+ /** Minimum readable text length to keep a fetched page. Default 200. */
436
+ minTextChars?: number;
437
+ /** Max chars of page text stored per source (keeps pages bounded). Default 4000. */
438
+ maxTextChars?: number;
439
+ }
440
+ /**
441
+ * The real web-research worker. Conforms to the loop's `ResearchWorker`
442
+ * contract: given the open gaps, it returns the sources it found plus a
443
+ * `buildPages` that emits citing pages from the sources the driver accepted.
444
+ */
445
+ declare function createWebResearchWorker(options?: WebResearchWorkerOptions): ResearchWorker;
446
+ interface VerifyingDriverOptions {
447
+ router?: RouterClient;
448
+ router_options?: TangleRouterOptions;
590
449
  /**
591
- * True when a held-out split was evaluated (drives the holdout threshold).
592
- *
593
- * Constraint: the substrate gate keys the holdout requirement off a scenario
594
- * corpus, which this run-only report does not carry. With `hasHoldout: true`
595
- * the gate fails closed (`missing_holdout_split`) even when holdout RunRecords
596
- * are supplied. Callers that gate on a real held-out split should drive
597
- * `evaluateReleaseConfidence` directly with a dataset/scenarios.
450
+ * When the LLM verdict can't be parsed, default to REJECT (fail-closed) so a
451
+ * model hiccup never poisons the KB with an unverified source. Set `true` to
452
+ * accept-on-parse-failure only if you have a reason to. Default false.
598
453
  */
599
- hasHoldout?: boolean;
600
- /** Candidate is the search-best variant — a promotion precondition. Default true. */
601
- promotedIsBest?: boolean;
602
- createdAt?: string;
603
- minScore?: number;
454
+ acceptOnParseFailure?: boolean;
604
455
  }
605
- declare function knowledgeReleaseReport(input: KnowledgeReleaseInput): KnowledgeReleaseReport;
456
+ /**
457
+ * The verifying driver: a real LLM pass that judges each candidate source's
458
+ * on-topic relevance to the goal + open gaps and whether it duplicates material
459
+ * already accepted this round. This is the differentiated coordinator role — it
460
+ * GATES the worker's additions; it adds nothing itself.
461
+ *
462
+ * The loop already dedups exact-uri duplicates and only calls this on genuinely
463
+ * new candidates, so the verifier focuses on relevance + near-duplicate
464
+ * judgement, not bookkeeping.
465
+ */
466
+ declare function createVerifyingResearchDriver(options?: VerifyingDriverOptions): ResearchDriver;
606
467
 
607
- interface AddSourceOptions {
608
- copyIntoRaw?: boolean;
609
- adapters?: SourceAdapter[];
610
- now?: () => Date;
611
- }
612
- interface AddSourceTextInput {
468
+ /**
469
+ * Adaptive verifier mode for `runTwoAgentResearchLoop`.
470
+ *
471
+ * The cost/quality A/B (`docs/results/cost-quality.md`) found the LLM relevance
472
+ * verifier's cleanliness win is dominated by DE-DUPLICATION — which a
473
+ * deterministic content-hash / canonical-URL check captures at ~none of the LLM
474
+ * premium — and that an LLM check only earns its dollar on the off-scope tail.
475
+ * The honest production move it names is: do the cheap deterministic work first,
476
+ * spend the LLM only where it pays. This module is that driver.
477
+ *
478
+ * Per candidate source the adaptive driver runs THREE stages, cheapest first,
479
+ * and stops at the first that decides:
480
+ *
481
+ * 1. DEDUP ($0, no LLM). Reject a source whose CONTENT (normalized-text hash)
482
+ * or whose CANONICAL URL matches one already accepted this round or already
483
+ * in the knowledge base. This is the de-dup the relevance judge was being
484
+ * paid to do; doing it deterministically is free and exact.
485
+ *
486
+ * 2. HEURISTIC TRIAGE ($0, no LLM). For a unique survivor, a cheap host /
487
+ * title / length signal classifies it as clearly-keep, clearly-drop, or
488
+ * AMBIGUOUS. Clear cases are resolved without a model: an authoritative host
489
+ * (arxiv, *.edu, *.gov, official docs) with a substantial readable body is
490
+ * kept; an obvious spam/listicle/marketing title or a too-thin body is
491
+ * dropped. Only genuinely ambiguous survivors fall through.
492
+ *
493
+ * 3. LLM ESCALATION ($, one call). ONLY the ambiguous survivors reach the LLM
494
+ * `verifySource` — the shipped `createVerifyingResearchDriver` relevance
495
+ * judge. This is where the verifier earns its premium: the off-scope tail a
496
+ * cheap rule can't adjudicate.
497
+ *
498
+ * The result is the cost/quality frontier point the doc predicted: most of the
499
+ * cleanliness (dedup + clear drops) at a fraction of the LLM $/calls (only the
500
+ * ambiguous tail pays). It is a real `ResearchDriver` — same contract the
501
+ * two-agent loop already gates on — and reuses `sha256`, the relevance verifier,
502
+ * and the index; it reinvents none of them.
503
+ */
504
+
505
+ /**
506
+ * Canonicalize a URL for duplicate detection: lowercase host, strip a leading
507
+ * `www.`, drop the scheme, the fragment, a trailing slash, and tracking query
508
+ * params (`utm_*`, `ref`, `fbclid`, `gclid`, …). Two URLs that differ only by
509
+ * those decorations canonicalize to the same key, so the dedup stage treats them
510
+ * as the same source. Falls back to the lowercased raw string when the input is
511
+ * not a parseable absolute URL (so non-http identifiers still dedup by equality).
512
+ */
513
+ declare function canonicalizeUrl(uri: string): string;
514
+ /**
515
+ * A stable content key for a fetched page: the sha256 of its normalized text
516
+ * (lowercased, punctuation/whitespace collapsed). Two pages whose readable body
517
+ * is the same modulo formatting collide here, so the dedup stage rejects a
518
+ * mirror/syndication of an already-accepted source even when the URL differs.
519
+ */
520
+ declare function contentKey(text: string): string;
521
+ /** Why the deterministic dedup stage rejected a candidate (for audit/notes). */
522
+ type DedupReason = 'duplicate-url' | 'duplicate-content';
523
+ /** The cheap-triage classification of a unique survivor. */
524
+ type TriageClass = 'keep' | 'drop' | 'ambiguous';
525
+ /** One source's adaptive routing decision, for instrumentation and the doc. */
526
+ interface AdaptiveDecision {
613
527
  uri: string;
614
- text: string;
615
- title?: string;
616
- mediaType?: string;
617
- validUntil?: string;
618
- lastVerifiedAt?: string;
619
- metadata?: Record<string, unknown>;
620
- }
621
- declare function loadSourceRegistry(root: string): Promise<SourceRegistry>;
622
- declare function writeSourceRegistry(root: string, registry: SourceRegistry): Promise<void>;
623
- declare function addSourcePath(root: string, sourcePath: string, options?: AddSourceOptions): Promise<SourceRecord[]>;
624
- declare function addSourceText(root: string, input: AddSourceTextInput, options?: Pick<AddSourceOptions, 'adapters' | 'now'>): Promise<SourceRecord>;
625
- declare function sourceRegistryPath(root: string): string;
626
-
627
- interface ValidateKnowledgeOptions {
628
- strict?: boolean;
528
+ /** The stage that decided this source: dedup | heuristic | llm. */
529
+ stage: 'dedup' | 'heuristic' | 'llm';
530
+ accepted: boolean;
531
+ /** The triage class assigned (set once past dedup). */
532
+ triage?: TriageClass;
533
+ reason?: string;
534
+ }
535
+ /** Running tally of where the adaptive driver spent its decisions. */
536
+ interface AdaptiveStats {
537
+ total: number;
538
+ /** Rejected by deterministic dedup (URL or content). $0. */
539
+ dedupRejected: number;
540
+ /** Kept by the cheap heuristic without an LLM call. $0. */
541
+ heuristicKept: number;
542
+ /** Dropped by the cheap heuristic without an LLM call. $0. */
543
+ heuristicDropped: number;
544
+ /** Escalated to the LLM relevance verifier ($ — the only paid stage). */
545
+ llmCalls: number;
546
+ /** Of the escalations, how many the LLM accepted. */
547
+ llmAccepted: number;
548
+ decisions: AdaptiveDecision[];
549
+ }
550
+ interface AdaptiveDriverOptions {
551
+ /** Router client for the LLM escalation. Defaults to a live client from env. */
552
+ router?: RouterClient;
553
+ router_options?: TangleRouterOptions;
554
+ /** Passed through to the escalation relevance verifier. */
555
+ verifying?: Pick<VerifyingDriverOptions, 'acceptOnParseFailure'>;
556
+ /**
557
+ * Hosts an authoritative source lives on. A unique survivor on one of these,
558
+ * with a substantial body, is KEPT deterministically (no LLM). Suffix-matched
559
+ * against the canonical host, so `arxiv.org` matches `export.arxiv.org`. The
560
+ * defaults cover papers, official docs, and standards bodies.
561
+ */
562
+ authoritativeHosts?: string[];
563
+ /**
564
+ * Title/snippet patterns that mark obvious spam / listicle / marketing — a
565
+ * unique survivor matching one is DROPPED deterministically (no LLM).
566
+ */
567
+ spamPatterns?: RegExp[];
568
+ /**
569
+ * Below this many readable chars a survivor is too thin to be a real reference
570
+ * and is dropped deterministically. Default 400.
571
+ */
572
+ minBodyChars?: number;
573
+ /**
574
+ * A survivor whose body is at or above this many chars AND on an authoritative
575
+ * host is kept without an LLM call. Default 600.
576
+ */
577
+ substantialBodyChars?: number;
578
+ /** Receives each routing decision as it is made (for live instrumentation). */
579
+ onDecision?: (decision: AdaptiveDecision) => void;
629
580
  }
630
- interface ValidateKnowledgeResult {
631
- ok: boolean;
632
- findings: KnowledgeLintFinding[];
581
+ /**
582
+ * Classify a UNIQUE survivor (already past dedup) with cheap host/title/length
583
+ * signals only — no LLM. Returns `keep`, `drop`, or `ambiguous`. `ambiguous` is
584
+ * the residue the LLM is reserved for: on-topic-looking pages on unknown hosts
585
+ * with a plausible body, which a host/title rule cannot adjudicate.
586
+ */
587
+ declare function triageSource(source: ResearchSourceProposal, options: {
588
+ authoritativeHosts: string[];
589
+ spamPatterns: RegExp[];
590
+ minBodyChars: number;
591
+ substantialBodyChars: number;
592
+ }): {
593
+ triage: TriageClass;
594
+ reason: string;
595
+ };
596
+ interface AdaptiveResearchDriver {
597
+ verifySource(source: ResearchSourceProposal, ctx: SourceVerificationContext): Promise<SourceVerdict>;
598
+ /** Live tally of where decisions were spent — the cost/quality instrumentation. */
599
+ stats(): AdaptiveStats;
633
600
  }
634
- declare function validateKnowledgeIndex(index: KnowledgeIndex, options?: ValidateKnowledgeOptions): ValidateKnowledgeResult;
601
+ /**
602
+ * Build the adaptive verifier. The deterministic stages (dedup + heuristic
603
+ * triage) cost $0; only AMBIGUOUS survivors escalate to the LLM relevance
604
+ * verifier. `stats()` exposes where every decision was spent so the A/B can read
605
+ * the LLM $/calls the adaptive driver saved against the cleanliness it kept.
606
+ *
607
+ * Dedup state is kept on the driver instance: it tracks the canonical URLs and
608
+ * content hashes it has ACCEPTED, plus those it sees in the verification
609
+ * context's `acceptedThisRound` and the KB index. Use one driver per loop run.
610
+ */
611
+ declare function createAdaptiveResearchDriver(options?: AdaptiveDriverOptions): AdaptiveResearchDriver;
635
612
 
636
- interface KnowledgeResearchLoopContext {
637
- root: string;
638
- goal: string;
639
- iteration: number;
640
- index: KnowledgeIndex;
641
- lintFindings: KnowledgeLintFinding[];
642
- validation: ValidateKnowledgeResult;
643
- readiness?: EvalKnowledgeBundleBuildResult;
644
- previousSteps: KnowledgeResearchLoopStep[];
645
- signal?: AbortSignal;
646
- }
647
- interface KnowledgeResearchLoopDecision {
613
+ /**
614
+ * Change detection across snapshots of one source's fragments.
615
+ *
616
+ * The output drives the continuous-ingestion loop: each `KnowledgeChange`
617
+ * carries the eval dimensions affected (`affectedDimensions`), which an
618
+ * agent-eval campaign scheduler consumes to decide which campaigns to
619
+ * re-run. Three change kinds:
620
+ *
621
+ * - `added` — fragment id appears in `next` but not `prev`.
622
+ * - `removed` — fragment id appears in `prev` but not `next`. Typical
623
+ * trigger: an authority retires a Wex slug, or a state SOS reorganises
624
+ * its forms catalogue.
625
+ * - `modified` — fragment id appears in both, body hash differs. This
626
+ * is the dominant change kind in practice — the Ryan-LLC v. FTC
627
+ * vacatur case manifests as a `modified` on the Wex non-compete
628
+ * fragment, NOT as a removed one.
629
+ *
630
+ * Unverifiable fragments are filtered out before diffing — comparing a
631
+ * captcha-blocked snapshot against a real one would falsely fire every
632
+ * fragment as removed. The caller can inspect the raw lists if they want
633
+ * to surface block-page failures.
634
+ *
635
+ * Within-snapshot duplicate ids are an upstream bug; this function picks
636
+ * the LAST one and emits a diagnostic via the `warnings` field.
637
+ *
638
+ * @stable
639
+ */
640
+ type KnowledgeChangeKind = 'added' | 'removed' | 'modified';
641
+ interface KnowledgeChange {
642
+ /** Source-scoped id (matches `KnowledgeFragment.id`). */
643
+ fragmentId: string;
644
+ kind: KnowledgeChangeKind;
648
645
  /**
649
- * Free-form notes from the researcher. Keep this human-readable; products can
650
- * store it as the research transcript.
646
+ * For `added`: full body of the new fragment.
647
+ * For `removed`: full body of the prior fragment.
648
+ * For `modified`: unified-diff-style payload `{ before, after }` body strings.
651
649
  */
652
- notes?: string;
650
+ diff?: {
651
+ before?: string;
652
+ after?: string;
653
+ };
653
654
  /**
654
- * Local files to register as immutable sources before applying proposals.
655
+ * Eval dimensions to re-score. Computed as the union of both fragments'
656
+ * `dimensionHints`. The eval cron treats this as a set of campaign tags.
655
657
  */
656
- sourcePaths?: string[];
658
+ affectedDimensions: string[];
659
+ /** URL of the affected authority page (from whichever side has it). */
660
+ url?: string;
657
661
  /**
658
- * Textual source artifacts discovered by an agent, browser worker, connector,
659
- * or deep-research process.
662
+ * Source-attested change time. For `modified`, takes the NEXT fragment's
663
+ * `sourceUpdatedAt`. For `removed`, takes the PRIOR fragment's
664
+ * `sourceUpdatedAt`. For `added`, takes the NEXT fragment's
665
+ * `sourceUpdatedAt`. Consumers index changes by this date.
660
666
  */
661
- sourceTexts?: AddSourceTextInput[];
667
+ detectedAt: string;
668
+ }
669
+ interface DetectChangesResult {
670
+ changes: KnowledgeChange[];
671
+ /** Counts by kind — handy for dashboards. */
672
+ summary: {
673
+ added: number;
674
+ removed: number;
675
+ modified: number;
676
+ };
677
+ /** Non-fatal diagnostics (duplicate ids, dropped unverifiable fragments). */
678
+ warnings: string[];
679
+ }
680
+ interface DetectChangesOptions {
662
681
  /**
663
- * Safe write protocol text. The loop parses and applies only accepted
664
- * `---FILE: knowledge/...---` blocks.
682
+ * When true (default), unverifiable fragments are dropped from both
683
+ * sides before comparison. Set false ONLY when debugging block-page
684
+ * issues — comparing against unverifiable content emits false
685
+ * `removed`/`modified` changes.
665
686
  */
666
- proposalText?: string;
687
+ skipUnverifiable?: boolean;
667
688
  /**
668
- * The researcher decides when the wiki is good enough. The loop deliberately
669
- * does not encode a domain-specific definition of "done".
689
+ * When provided, only changes whose `affectedDimensions` intersect this
690
+ * set are returned. Useful for cron loops that schedule per-dimension
691
+ * eval campaigns and only care about a subset.
670
692
  */
671
- done?: boolean;
672
- metadata?: Record<string, unknown>;
673
- }
674
- interface KnowledgeResearchLoopStep {
675
- iteration: number;
676
- notes?: string;
677
- addedSources: SourceRecord[];
678
- applied?: ApplyWriteBlocksResult;
679
- lintFindings: KnowledgeLintFinding[];
680
- validation: ValidateKnowledgeResult;
681
- readiness?: EvalKnowledgeBundleBuildResult;
682
- event: KnowledgeEvent;
683
- done: boolean;
684
- metadata?: Record<string, unknown>;
685
- }
686
- interface RunKnowledgeResearchLoopOptions {
687
- root: string;
688
- goal: string;
689
- maxIterations?: number;
690
- actor?: string;
691
- strict?: ValidateKnowledgeOptions['strict'];
692
- readinessSpecs?: KnowledgeReadinessSpec[];
693
- readinessTaskId?: string;
694
- readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
695
- sourceOptions?: Pick<AddSourceOptions, 'adapters' | 'now'>;
696
- signal?: AbortSignal;
697
- step(context: KnowledgeResearchLoopContext): Promise<KnowledgeResearchLoopDecision> | KnowledgeResearchLoopDecision;
698
- onStep?: (step: KnowledgeResearchLoopStep) => Promise<void> | void;
693
+ filterDimensions?: string[];
699
694
  }
700
- interface KnowledgeResearchLoopResult {
701
- root: string;
702
- goal: string;
703
- iterations: number;
704
- done: boolean;
705
- index: KnowledgeIndex;
706
- lintFindings: KnowledgeLintFinding[];
707
- validation: ValidateKnowledgeResult;
708
- readiness?: EvalKnowledgeBundleBuildResult;
709
- steps: KnowledgeResearchLoopStep[];
695
+ declare function detectChanges(prev: KnowledgeFragment[], next: KnowledgeFragment[], options?: DetectChangesOptions): DetectChangesResult;
696
+
697
+ interface ChunkingOptions {
698
+ targetChars: number;
699
+ maxChars: number;
700
+ minChars: number;
701
+ overlapChars: number;
710
702
  }
711
- type KnowledgeControlLoopState = KnowledgeResearchLoopContext;
712
- type KnowledgeControlLoopAction = KnowledgeResearchLoopDecision;
713
- type KnowledgeControlLoopActionResult = KnowledgeResearchLoopStep;
714
- interface KnowledgeControlLoopAdapterOptions {
715
- root: string;
716
- goal: string;
717
- actor?: string;
718
- strict?: ValidateKnowledgeOptions['strict'];
719
- readinessSpecs?: KnowledgeReadinessSpec[];
720
- readinessTaskId?: string;
721
- readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
722
- sourceOptions?: Pick<AddSourceOptions, 'adapters' | 'now'>;
703
+ interface KnowledgeChunk {
704
+ index: number;
705
+ text: string;
706
+ headingPath: string;
707
+ charStart: number;
708
+ charEnd: number;
709
+ oversized: boolean;
723
710
  }
724
- type KnowledgeControlLoopAdapter = Pick<ControlRuntimeConfig<KnowledgeControlLoopState, KnowledgeControlLoopAction, KnowledgeControlLoopActionResult, ControlEvalResult>, 'intent' | 'observe' | 'validate' | 'act' | 'shouldStop'>;
711
+ declare function chunkMarkdown(content: string, options?: Partial<ChunkingOptions>): KnowledgeChunk[];
712
+ declare function stripFrontmatter(content: string): {
713
+ body: string;
714
+ bodyOffset: number;
715
+ };
716
+
725
717
  /**
726
- * Adapter for running knowledge growth through `agent-eval`'s generic control
727
- * runtime. The caller still owns `decide`: that can be a proposer agent,
728
- * reviewer agent, deterministic policy, or a composition of all three.
718
+ * Claim-grounding mode for `runTwoAgentResearchLoop`.
719
+ *
720
+ * The two-agent loop's existing verifier judges a source's on-topic RELEVANCE
721
+ * (is this page about the goal?). On the topic sets we have measured, its
722
+ * cleanliness win is dominated by DE-DUPLICATION — which a deterministic
723
+ * content-hash / canonical-URL check captures at ~none of the LLM premium (see
724
+ * `docs/results/cost-quality.md`). That makes the LLM verifier look expensive
725
+ * for what a cheap rule already does.
726
+ *
727
+ * Claim-grounding targets a DIFFERENT, harder error band: a citation that is
728
+ * relevant and unique but **misattributed** — the page is on-topic, the URL is
729
+ * real, yet the specific CLAIM the source is cited for does NOT actually appear
730
+ * in the page. This is the citation-fabrication failure mode of LLM research:
731
+ * the model writes a plausible sentence and hangs a real URL off it that never
732
+ * says any such thing. Neither de-dup nor a relevance judge catches it (both can
733
+ * pass a misattributed-but-on-topic page); only checking the claim against the
734
+ * fetched text does.
735
+ *
736
+ * The check is EXECUTABLE GROUND TRUTH, not another LLM opinion: the worker
737
+ * attaches the specific claim it is citing the source for, and the verifier
738
+ * tests whether that claim is PRESENT (verbatim, normalized, or as a sufficient
739
+ * content-word overlap / close paraphrase) in the `htmlToText` output of the
740
+ * page the worker actually fetched. A claim that is not grounded is rejected as
741
+ * misattributed. Because the oracle is deterministic text presence — not a model
742
+ * call — it is a deployable, non-oracle verifier: it can run in production with
743
+ * zero inference cost, OR be composed with the LLM relevance verifier so the
744
+ * loop rejects BOTH off-topic AND misattributed sources.
745
+ *
746
+ * This module is content-free and any-topic: it adds (1) a way for a proposal to
747
+ * carry the claim it is cited for, (2) the `groundClaimInText` oracle, and (3) a
748
+ * `ResearchDriver` that gates on grounding. It composes the existing
749
+ * `ResearchDriver` / `ResearchSourceProposal` contracts and the shipped
750
+ * `htmlToText`; it reinvents none of them.
729
751
  */
730
- declare function createKnowledgeControlLoopAdapter(options: KnowledgeControlLoopAdapterOptions): KnowledgeControlLoopAdapter;
731
- declare function runKnowledgeResearchLoop(options: RunKnowledgeResearchLoopOptions): Promise<KnowledgeResearchLoopResult>;
732
752
 
733
753
  /**
734
- * Default standing instructions for a research supervisor. It does NOT solve the
735
- * research itself it DECOMPOSES the goal into sub-topics and spawns one
736
- * researcher per sub-topic over the live `Scope`, widening/narrowing until the
737
- * readiness gate (the deliverable check) reports the knowledge base is ready.
754
+ * Metadata key under which a proposal carries the specific claim it is cited
755
+ * for. The worker sets `metadata[citedClaimKey] = '<the claim>'`; the
756
+ * claim-grounding driver reads it and checks it against the fetched page text.
738
757
  */
739
- declare const RESEARCH_SUPERVISOR_SYSTEM_PROMPT: string;
740
- interface ResearchSupervisorOptions {
741
- /** Where the knowledge base lives. The deliverable check reads readiness here. */
742
- root: string;
743
- /** The research goal handed to the supervisor as its task. */
744
- goal: string;
758
+ declare const citedClaimKey = "citedClaim";
759
+ /** Read the cited claim a proposal carries, if any. */
760
+ declare function citedClaimOf(source: ResearchSourceProposal): string | undefined;
761
+ /** Attach a cited claim to a proposal (immutably returns a new proposal). */
762
+ declare function withCitedClaim(source: ResearchSourceProposal, claim: string): ResearchSourceProposal;
763
+ interface GroundingResult {
764
+ /** True when the claim is sufficiently present in the page text. */
765
+ grounded: boolean;
766
+ /** How the claim matched (or why it didn't). For audit/notes. */
767
+ mode: 'verbatim' | 'normalized' | 'overlap' | 'absent' | 'empty-claim' | 'empty-text';
745
768
  /**
746
- * Readiness specs define DONE: the supervisor's deliverable check passes once
747
- * `scoreKnowledgeReadiness` reports no blocking gaps over the live KB. Required
748
- * — without a gate the supervisor would never know when to stop.
769
+ * Fraction of the claim's content words found in the page text. 1 for a
770
+ * verbatim/normalized hit; the measured overlap otherwise.
749
771
  */
750
- readinessSpecs: KnowledgeReadinessSpec[];
751
- readinessTaskId?: string;
752
- readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
753
- /** The conserved compute pool for the whole supervised run. */
754
- budget: Budget;
772
+ overlap: number;
773
+ /** Content words present in the claim but NOT in the page text. */
774
+ missingWords: string[];
775
+ }
776
+ interface GroundClaimOptions {
755
777
  /**
756
- * WHERE researcher workers run — the caller's real backend seam (sandbox with
757
- * a `sandboxClient`, or router/cli/bridge). The supervisor spawns researchers
758
- * on this backend; assembling the seam (creds, sandbox SDK) is the caller's,
759
- * so there is no fabricated default.
778
+ * Minimum fraction of the claim's content words that must appear in the page
779
+ * text to count as a close paraphrase when there is no verbatim/normalized
780
+ * hit. Default 0.7 a high bar, because a misattribution is exactly a claim
781
+ * whose specific words the page does not contain.
760
782
  */
761
- backend: ExecutorConfig;
762
- /** Harness for the researcher worker profile (default `researcherProfile`'s). */
763
- harness?: string;
764
- /** The supervisor brain's router model. */
765
- supervisorModel?: string;
766
- /** Custom supervisor instructions. Defaults to {@link RESEARCH_SUPERVISOR_SYSTEM_PROMPT}. */
767
- supervisorSystemPrompt?: string;
768
- /** Extra `supervise()` knobs (perWorker, maxLiveWorkers, router, allowedModels, …). */
769
- superviseOptions?: Partial<Omit<SuperviseOptions, 'budget' | 'backend' | 'deliverable'>>;
783
+ minOverlap?: number;
784
+ /**
785
+ * Content words shorter than this are ignored (drops "the", "of", "is", …)
786
+ * and never count toward overlap. Default 3.
787
+ */
788
+ minWordLength?: number;
770
789
  }
771
790
  /**
772
- * Build the deliverable (completion oracle) for a research supervisor: "settled
773
- * the knowledge base is ready". It re-reads the KB from disk and runs the
774
- * readiness gate, so the supervisor stops on the REAL grounded state — not on a
775
- * worker's self-report.
791
+ * THE ORACLE. Is `claim` grounded in `pageText` (the `htmlToText` output of the
792
+ * page the worker fetched)? Deterministic, no model call:
793
+ *
794
+ * 1. verbatim — the claim string appears as-is (case-insensitive).
795
+ * 2. normalized — the claim appears after collapsing punctuation/whitespace
796
+ * on both sides (so "5.4x" vs "5.4 x", smart quotes, etc. still match).
797
+ * 3. overlap — a close paraphrase: at least `minOverlap` of the claim's
798
+ * substantive content words appear in the page text. A misattributed page
799
+ * fails here because the SPECIFIC words the claim asserts are absent.
800
+ *
801
+ * Returns the match mode, the measured overlap, and the missing content words —
802
+ * enough for the driver to give a precise rejection reason and for a test/doc to
803
+ * audit WHY a claim grounded or didn't.
776
804
  */
777
- declare function knowledgeReadinessDeliverable(options: Pick<ResearchSupervisorOptions, 'root' | 'goal' | 'readinessSpecs' | 'readinessTaskId' | 'readiness'>): DeliverableSpec<unknown>;
805
+ declare function groundClaimInText(claim: string, pageText: string, options?: GroundClaimOptions): GroundingResult;
806
+ interface ClaimGroundingDriverOptions extends GroundClaimOptions {
807
+ /**
808
+ * Optional second verifier to compose AFTER grounding passes. When set, a
809
+ * source must BOTH ground its claim AND pass this verifier (e.g. the LLM
810
+ * relevance driver's `verifySource`). Lets the loop reject off-topic AND
811
+ * misattributed sources in one driver. Omit for the pure, zero-inference
812
+ * grounding gate.
813
+ */
814
+ relevanceVerifier?: (source: ResearchSourceProposal, ctx: SourceVerificationContext) => Promise<SourceVerdict> | SourceVerdict;
815
+ /**
816
+ * What to do when a proposal carries NO cited claim. `'reject'` (default) is
817
+ * fail-closed: in claim-grounding mode every source must declare what it is
818
+ * cited for, so an un-annotated source is treated as ungrounded. `'accept'`
819
+ * lets un-annotated sources through to the relevance verifier (if any) —
820
+ * useful when mixing annotated and legacy proposals.
821
+ */
822
+ onMissingClaim?: 'reject' | 'accept';
823
+ }
778
824
  /**
779
- * A research supervisor: a SUPERVISOR brain creates the topology dynamically —
780
- * how many researchers, split by sub-topic over the `Scope`, with the
781
- * knowledge-readiness gate as its stop condition.
782
- *
783
- * This is a thin wrapper over `supervise(profile, goal, opts)`. It composes the
784
- * existing pieces and builds nothing new: the researcher `AgentProfile` (from
785
- * `researcherProfile`) is the worker shape the supervisor spawns, and
786
- * `knowledgeReadinessDeliverable` (over `scoreKnowledgeReadiness`) is the
787
- * completion oracle that decides when the KB is ready.
825
+ * A `ResearchDriver`-shaped verifier (just the `verifySource` arm) that gates on
826
+ * CLAIM GROUNDING: it rejects a source whose cited claim is not present in its
827
+ * fetched page text a misattributed / fabricated citation — and (optionally)
828
+ * composes a relevance verifier after grounding passes.
788
829
  *
789
- * Needs creds (a real supervisor router brain + a worker backend), so it is the
790
- * LIVE path. The offline two-agent loop is `runTwoAgentResearchLoop`.
830
+ * The returned function matches `ResearchDriver['verifySource']`, so it drops
831
+ * straight into `runTwoAgentResearchLoop` as `{ verifySource: createClaimGroundingVerifier(...) }`.
791
832
  */
792
- declare function runResearchSupervisor(options: ResearchSupervisorOptions): Promise<SupervisedResult<unknown>>;
793
-
794
- declare const SourceAnchorSchema: z.ZodObject<{
795
- id: z.ZodString;
796
- sourceId: z.ZodString;
797
- label: z.ZodOptional<z.ZodString>;
798
- page: z.ZodOptional<z.ZodNumber>;
799
- lineStart: z.ZodOptional<z.ZodNumber>;
800
- lineEnd: z.ZodOptional<z.ZodNumber>;
801
- charStart: z.ZodOptional<z.ZodNumber>;
802
- charEnd: z.ZodOptional<z.ZodNumber>;
803
- timestampMs: z.ZodOptional<z.ZodNumber>;
804
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
805
- }, z.core.$strip>;
806
- declare const SourceRecordSchema: z.ZodObject<{
807
- id: z.ZodString;
808
- uri: z.ZodString;
809
- title: z.ZodOptional<z.ZodString>;
810
- mediaType: z.ZodOptional<z.ZodString>;
811
- contentHash: z.ZodString;
812
- text: z.ZodOptional<z.ZodString>;
813
- anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
814
- id: z.ZodString;
815
- sourceId: z.ZodString;
816
- label: z.ZodOptional<z.ZodString>;
817
- page: z.ZodOptional<z.ZodNumber>;
818
- lineStart: z.ZodOptional<z.ZodNumber>;
819
- lineEnd: z.ZodOptional<z.ZodNumber>;
820
- charStart: z.ZodOptional<z.ZodNumber>;
821
- charEnd: z.ZodOptional<z.ZodNumber>;
822
- timestampMs: z.ZodOptional<z.ZodNumber>;
823
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
824
- }, z.core.$strip>>>;
825
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
826
- createdAt: z.ZodString;
827
- }, z.core.$strip>;
828
- declare const KnowledgePageSchema: z.ZodObject<{
829
- id: z.ZodString;
830
- path: z.ZodString;
831
- title: z.ZodString;
832
- text: z.ZodString;
833
- frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
834
- sourceIds: z.ZodArray<z.ZodString>;
835
- tags: z.ZodArray<z.ZodString>;
836
- outLinks: z.ZodArray<z.ZodString>;
837
- }, z.core.$strip>;
838
- declare const KnowledgeGraphNodeSchema: z.ZodObject<{
839
- id: z.ZodString;
840
- title: z.ZodString;
841
- path: z.ZodString;
842
- tags: z.ZodArray<z.ZodString>;
843
- sourceIds: z.ZodArray<z.ZodString>;
844
- outDegree: z.ZodNumber;
845
- inDegree: z.ZodNumber;
846
- }, z.core.$strip>;
847
- declare const KnowledgeGraphEdgeSchema: z.ZodObject<{
848
- source: z.ZodString;
849
- target: z.ZodString;
850
- weight: z.ZodNumber;
851
- reasons: z.ZodArray<z.ZodString>;
852
- }, z.core.$strip>;
853
- declare const KnowledgeIndexSchema: z.ZodObject<{
854
- root: z.ZodString;
855
- generatedAt: z.ZodString;
856
- sources: z.ZodArray<z.ZodObject<{
857
- id: z.ZodString;
858
- uri: z.ZodString;
859
- title: z.ZodOptional<z.ZodString>;
860
- mediaType: z.ZodOptional<z.ZodString>;
861
- contentHash: z.ZodString;
862
- text: z.ZodOptional<z.ZodString>;
863
- anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
864
- id: z.ZodString;
865
- sourceId: z.ZodString;
866
- label: z.ZodOptional<z.ZodString>;
867
- page: z.ZodOptional<z.ZodNumber>;
868
- lineStart: z.ZodOptional<z.ZodNumber>;
869
- lineEnd: z.ZodOptional<z.ZodNumber>;
870
- charStart: z.ZodOptional<z.ZodNumber>;
871
- charEnd: z.ZodOptional<z.ZodNumber>;
872
- timestampMs: z.ZodOptional<z.ZodNumber>;
873
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
874
- }, z.core.$strip>>>;
875
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
876
- createdAt: z.ZodString;
877
- }, z.core.$strip>>;
878
- pages: z.ZodArray<z.ZodObject<{
879
- id: z.ZodString;
880
- path: z.ZodString;
881
- title: z.ZodString;
882
- text: z.ZodString;
883
- frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
884
- sourceIds: z.ZodArray<z.ZodString>;
885
- tags: z.ZodArray<z.ZodString>;
886
- outLinks: z.ZodArray<z.ZodString>;
887
- }, z.core.$strip>>;
888
- graph: z.ZodObject<{
889
- nodes: z.ZodArray<z.ZodObject<{
890
- id: z.ZodString;
891
- title: z.ZodString;
892
- path: z.ZodString;
893
- tags: z.ZodArray<z.ZodString>;
894
- sourceIds: z.ZodArray<z.ZodString>;
895
- outDegree: z.ZodNumber;
896
- inDegree: z.ZodNumber;
897
- }, z.core.$strip>>;
898
- edges: z.ZodArray<z.ZodObject<{
899
- source: z.ZodString;
900
- target: z.ZodString;
901
- weight: z.ZodNumber;
902
- reasons: z.ZodArray<z.ZodString>;
903
- }, z.core.$strip>>;
904
- }, z.core.$strip>;
905
- }, z.core.$strip>;
906
- declare const KnowledgeEventSchema: z.ZodObject<{
907
- id: z.ZodString;
908
- type: z.ZodEnum<{
909
- "source.added": "source.added";
910
- "proposal.applied": "proposal.applied";
911
- "index.built": "index.built";
912
- "lint.run": "lint.run";
913
- "optimization.run": "optimization.run";
914
- "release.promoted": "release.promoted";
915
- "release.rejected": "release.rejected";
916
- }>;
917
- createdAt: z.ZodString;
918
- actor: z.ZodOptional<z.ZodString>;
919
- target: z.ZodOptional<z.ZodString>;
920
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
921
- }, z.core.$strip>;
922
- declare const KnowledgeBaseCandidateSchema: z.ZodObject<{
923
- id: z.ZodString;
924
- units: z.ZodArray<z.ZodObject<{
925
- id: z.ZodString;
926
- title: z.ZodString;
927
- text: z.ZodString;
928
- claims: z.ZodOptional<z.ZodArray<z.ZodObject<{
929
- id: z.ZodString;
930
- text: z.ZodString;
931
- refs: z.ZodArray<z.ZodObject<{
932
- sourceId: z.ZodString;
933
- anchorId: z.ZodOptional<z.ZodString>;
934
- quote: z.ZodOptional<z.ZodString>;
935
- }, z.core.$strip>>;
936
- confidence: z.ZodOptional<z.ZodNumber>;
937
- status: z.ZodOptional<z.ZodEnum<{
938
- draft: "draft";
939
- active: "active";
940
- superseded: "superseded";
941
- rejected: "rejected";
942
- }>>;
943
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
944
- }, z.core.$strip>>>;
945
- relations: z.ZodOptional<z.ZodArray<z.ZodObject<{
946
- sourceId: z.ZodString;
947
- targetId: z.ZodString;
948
- predicate: z.ZodString;
949
- weight: z.ZodOptional<z.ZodNumber>;
950
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
951
- }, z.core.$strip>>>;
952
- sourceIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
953
- tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
954
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
955
- updatedAt: z.ZodOptional<z.ZodString>;
956
- }, z.core.$strip>>;
957
- retrievalPolicy: z.ZodOptional<z.ZodString>;
958
- synthesisPolicy: z.ZodOptional<z.ZodString>;
959
- questionPolicy: z.ZodOptional<z.ZodString>;
960
- updatePolicy: z.ZodOptional<z.ZodString>;
961
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
962
- }, z.core.$strip>;
963
-
964
- declare function searchKnowledge(index: KnowledgeIndex, query: string, limit?: number): KnowledgeSearchResult[];
965
- declare function tokenizeQuery(query: string): string[];
966
- declare function reciprocalRankFusion(rankLists: string[][], k?: number): Map<string, number>;
967
-
968
- interface KnowledgeLayout {
969
- root: string;
970
- knowledgeDir: string;
971
- rawSourcesDir: string;
972
- sourceRegistryPath: string;
973
- indexPath: string;
974
- logPath: string;
975
- cacheDir: string;
833
+ declare function createClaimGroundingVerifier(options?: ClaimGroundingDriverOptions): (source: ResearchSourceProposal, ctx: SourceVerificationContext) => Promise<SourceVerdict>;
834
+ interface WorkerClaimDecorationOptions {
835
+ router?: RouterClient;
836
+ router_options?: TangleRouterOptions;
837
+ /** Max output tokens for the claim-extraction call. Default 1200 (glm floor). */
838
+ maxTokens?: number;
976
839
  }
977
- declare function layoutFor(root: string): KnowledgeLayout;
978
840
  /**
979
- * Filenames that `initKnowledgeBase` writes as human-navigation scaffolding.
980
- * These are excluded from the page index they exist on disk so authors can
981
- * curate their vault, but they are not searchable content.
841
+ * Ask an LLM to state, for one source, the single specific factual claim a
842
+ * researcher would cite THIS page for toward the goal. Used to DECORATE the
843
+ * sources a relevance-only worker produced with the claim a citation would make,
844
+ * so the claim-grounding verifier has something executable to check. The model
845
+ * is told to ground the claim in the provided excerpt; the verifier then checks
846
+ * it against the FULL page text independently — the model does not get to mark
847
+ * its own homework.
982
848
  *
983
- * Add new scaffold filenames here (and only here) to keep lint, validate, viz,
984
- * and the indexer consistent.
985
- */
986
- declare const SCAFFOLD_PAGE_BASENAMES: readonly string[];
987
- /**
988
- * True when a knowledge-relative path points at a scaffold file rather than
989
- * authored content. Accepts both repo-relative paths (`knowledge/index.md`)
990
- * and any nested `<dir>/index.md` or `<dir>/log.md` (for example
991
- * `knowledge/concepts/index.md`) so subdirectory README-style scaffolds are
992
- * also excluded.
849
+ * Returns the proposal annotated via `withCitedClaim`, or the original proposal
850
+ * unchanged if the model returns nothing parseable (the verifier's
851
+ * `onMissingClaim` policy then decides).
993
852
  */
994
- declare function isScaffoldPath(path: string): boolean;
995
- declare function initKnowledgeBase(root: string): Promise<KnowledgeLayout>;
996
- declare function loadKnowledgePages(root: string): Promise<KnowledgePage[]>;
997
- declare function writeJson(path: string, value: unknown): Promise<void>;
853
+ declare function createClaimDecorator(options?: WorkerClaimDecorationOptions): (source: ResearchSourceProposal, goal: string) => Promise<ResearchSourceProposal>;
998
854
 
999
- /**
1000
- * A knowledge gap the loop surfaces from `scoreKnowledgeReadiness`. The worker
1001
- * targets these; the driver folds the unfilled remainder into the worker's next
1002
- * prompt and runs its own gap-fill pass over them.
1003
- */
1004
- interface KnowledgeGap {
1005
- /** Readiness-spec id this gap belongs to. */
855
+ interface DiscoveryTask {
1006
856
  id: string;
1007
- /** Human-readable description of what's missing. */
1008
- description: string;
1009
- /** The search query the readiness check ran for this requirement. */
1010
- query: string;
1011
- /** True when the gap blocks readiness (vs. a soft, non-blocking gap). */
1012
- blocking: boolean;
857
+ goal: string;
858
+ query?: string;
859
+ sourceHints?: string[];
860
+ metadata?: Record<string, unknown>;
1013
861
  }
1014
- /** A new source the worker (or driver) discovered and wants to add to the KB. */
1015
- type ResearchSourceProposal = AddSourceTextInput;
1016
- /**
1017
- * What a research agent contributes in one round. Both the worker and (when
1018
- * `driverResearches` is on) the driver produce this shape — the worker ADDS
1019
- * primary findings, the driver gap-FILLS the ones the worker missed.
1020
- *
1021
- * `proposalText` is the safe write-protocol text (`---FILE: knowledge/...---`
1022
- * blocks). The loop only applies it AFTER the driver has verified the round's
1023
- * sources, so a rejected source never reaches the curated pages.
1024
- */
1025
- interface ResearchContribution {
1026
- /** Immutable sources to register (the raw evidence). */
1027
- sources?: ResearchSourceProposal[];
1028
- /** Safe write-protocol text producing curated `knowledge/*.md` pages. */
1029
- proposalText?: string;
1030
- /**
1031
- * Build the page write-protocol text FROM the sources the driver accepted —
1032
- * the curated, citing pages the readiness gate searches. Receives the
1033
- * registered `SourceRecord`s (with their assigned ids, so a page's frontmatter
1034
- * `sources:` can cite them). Returns `---FILE: knowledge/...---` block text or
1035
- * `undefined`. Runs after verification, so a page never cites a rejected
1036
- * source. Concatenated after any static `proposalText`.
1037
- */
1038
- buildPages?: (acceptedSources: SourceRecord[]) => string | undefined;
1039
- /** Free-form research transcript — products can persist this. */
1040
- notes?: string;
862
+ interface DiscoveryResult {
863
+ taskId: string;
864
+ summary: string;
865
+ sourceUris?: string[];
866
+ claims?: Array<{
867
+ text: string;
868
+ sourceUri?: string;
869
+ confidence?: number;
870
+ }>;
871
+ followUpTasks?: DiscoveryTask[];
1041
872
  metadata?: Record<string, unknown>;
1042
873
  }
1043
- /** Context handed to the worker each round. */
1044
- interface WorkerResearchContext {
1045
- root: string;
1046
- goal: string;
1047
- round: number;
1048
- index: KnowledgeIndex;
1049
- /** Gaps the readiness gate currently reports — what the worker should close. */
1050
- gaps: KnowledgeGap[];
1051
- /** Steer text the driver folded in from the previous round's remaining gaps. */
1052
- steer?: string;
1053
- readiness: EvalKnowledgeBundleBuildResult;
1054
- signal?: AbortSignal;
1055
- }
1056
- /** Context handed to the driver's verifier for one candidate source. */
1057
- interface SourceVerificationContext {
1058
- root: string;
1059
- goal: string;
1060
- round: number;
1061
- index: KnowledgeIndex;
1062
- gaps: KnowledgeGap[];
1063
- /** Sources already accepted earlier THIS round (in-round dedup). */
1064
- acceptedThisRound: ResearchSourceProposal[];
1065
- signal?: AbortSignal;
874
+ interface KnowledgeDiscoveryWorker {
875
+ run(task: DiscoveryTask, signal?: AbortSignal): Promise<DiscoveryResult> | DiscoveryResult;
1066
876
  }
1067
- /** A single rejected source plus the reason the driver gave. */
1068
- interface RejectedSource {
1069
- source: ResearchSourceProposal;
1070
- reason: string;
877
+ interface KnowledgeDiscoveryDispatcher {
878
+ dispatch(tasks: DiscoveryTask[], options?: {
879
+ concurrency?: number;
880
+ signal?: AbortSignal;
881
+ }): Promise<DiscoveryResult[]>;
1071
882
  }
1072
- /** Context handed to the driver's gap-fill pass (only when `driverResearches`). */
1073
- interface DriverResearchContext {
1074
- root: string;
1075
- goal: string;
1076
- round: number;
1077
- index: KnowledgeIndex;
1078
- /** Gaps STILL open after the worker's accepted contribution applied. */
1079
- remainingGaps: KnowledgeGap[];
1080
- readiness: EvalKnowledgeBundleBuildResult;
1081
- signal?: AbortSignal;
883
+ declare function createLocalDiscoveryDispatcher(worker: KnowledgeDiscoveryWorker): KnowledgeDiscoveryDispatcher;
884
+
885
+ interface KnowledgeEventQuery {
886
+ type?: KnowledgeEventType;
887
+ target?: string;
888
+ limit?: number;
1082
889
  }
890
+ declare function createKnowledgeEvent(input: {
891
+ type: KnowledgeEventType;
892
+ actor?: string;
893
+ target?: string;
894
+ metadata?: Record<string, unknown>;
895
+ now?: () => Date;
896
+ }): KnowledgeEvent;
897
+
1083
898
  /**
1084
- * The differentiated driver role.
899
+ * Knowledge freshness store: tracks when each `(workspaceId, sourceId)` pair
900
+ * was last successfully refreshed, and reports staleness against a TTL.
1085
901
  *
1086
- * - `verifySource` the gate the worker's additions pass before they commit.
1087
- * Return `{ accept: true }` to keep a source or `{ accept: false, reason }`
1088
- * to reject it (not real / not relevant / duplicate). The loop dedups exact
1089
- * duplicates (same `uri` already in the KB or accepted this round) BEFORE
1090
- * calling this, so the verifier only sees genuinely-new candidates.
1091
- * - `research` the driver's OWN gap-fill pass over the gaps the worker left
1092
- * open. Only invoked when `driverResearches` is true.
1093
- * - `foldGaps` — turn the remaining gaps into a steer string for the worker's
1094
- * next prompt. Defaults to a compact bulleted list when omitted.
902
+ * The contract is intentionally minimal just enough to drive a cron loop:
903
+ *
904
+ * ```ts
905
+ * const store = createFileSystemFreshnessStore({ root: '.agent-knowledge' })
906
+ * for (const source of sources) {
907
+ * if (await store.stale({ workspaceId, sourceId: source.id, ttlMs: DAY })) {
908
+ * const fragments = await source.fetch({ cacheDir })
909
+ * await persistFragments(fragments)
910
+ * await store.mark({ workspaceId, sourceId: source.id, when: new Date() })
911
+ * }
912
+ * }
913
+ * ```
914
+ *
915
+ * Per-tenant isolation is enforced by `workspaceId` keying — there is no
916
+ * global mutable state across workspaces.
917
+ *
918
+ * Two adapters ship in-package:
919
+ *
920
+ * - `createFileSystemFreshnessStore` — JSON file under the knowledge root,
921
+ * mirrors the layout convention already used by `sources.json`.
922
+ * - `createD1FreshnessStoreStub` — adapter scaffold for Cloudflare D1 /
923
+ * Postgres. Production consumers should implement the `D1Adapter`
924
+ * interface inside their own app; this stub exists to anchor the shape.
925
+ *
926
+ * @stable contract — interface is frozen at 0.x within this major.
927
+ * @stable filesystem adapter
928
+ * @experimental D1 stub — interface will evolve as real consumers wire it.
1095
929
  */
1096
- interface ResearchDriver {
1097
- verifySource(source: ResearchSourceProposal, ctx: SourceVerificationContext): Promise<SourceVerdict> | SourceVerdict;
1098
- research?(ctx: DriverResearchContext): Promise<ResearchContribution> | ResearchContribution;
1099
- foldGaps?(gaps: KnowledgeGap[]): string;
930
+ /** Identity for one freshness record. */
931
+ interface FreshnessKey {
932
+ workspaceId: string;
933
+ sourceId: string;
1100
934
  }
1101
- type SourceVerdict = {
1102
- accept: true;
1103
- } | {
1104
- accept: false;
1105
- reason: string;
1106
- };
1107
- /** The worker: primary research targeting the round's gaps. */
1108
- type ResearchWorker = (ctx: WorkerResearchContext) => Promise<ResearchContribution> | ResearchContribution;
1109
- interface TwoAgentResearchLoopOptions {
1110
- root: string;
1111
- goal: string;
1112
- worker: ResearchWorker;
1113
- driver: ResearchDriver;
1114
- /**
1115
- * When false (default), the driver ONLY verifies + gates — a pure coordinator
1116
- * that contributes no research of its own (the "doesn't participate in the
1117
- * work" mode). When true, the driver also runs its `research` gap-fill pass
1118
- * each round over the gaps the worker left open.
1119
- */
1120
- driverResearches?: boolean;
1121
- maxRounds?: number;
1122
- actor?: string;
1123
- /** Readiness specs define the gate; an empty list means the loop never gates. */
1124
- readinessSpecs?: KnowledgeReadinessSpec[];
1125
- readinessTaskId?: string;
1126
- readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
1127
- sourceOptions?: Pick<AddSourceOptions, 'adapters' | 'now'>;
1128
- signal?: AbortSignal;
1129
- onRound?: (round: TwoAgentResearchRound) => Promise<void> | void;
935
+ /** TTL bound for staleness checks. */
936
+ interface FreshnessTtl extends FreshnessKey {
937
+ /** Milliseconds — `Date.now() - last() > ttlMs` ⇒ stale. */
938
+ ttlMs: number;
939
+ /** Injected clock for deterministic tests; defaults to system time. */
940
+ now?: Date;
1130
941
  }
1131
- interface TwoAgentResearchRound {
1132
- round: number;
1133
- /** Gaps reported at the START of the round (what the worker targeted). */
1134
- gaps: KnowledgeGap[];
1135
- /** Worker sources accepted by the driver and written to the KB. */
1136
- acceptedWorkerSources: SourceRecord[];
1137
- /** Worker sources the driver rejected (with reasons) — never written. */
1138
- rejectedWorkerSources: RejectedSource[];
1139
- /** Sources the driver itself added in its gap-fill pass. */
1140
- driverSources: SourceRecord[];
1141
- /** Curated pages written this round (worker proposal + driver proposal). */
1142
- writtenPages: string[];
1143
- readiness?: EvalKnowledgeBundleBuildResult;
1144
- /** True once the readiness gate reports no blocking gaps. */
1145
- ready: boolean;
1146
- event: KnowledgeEvent;
1147
- notes: {
1148
- worker?: string;
1149
- driver?: string;
1150
- };
942
+ /** Mark argument. */
943
+ interface FreshnessMark extends FreshnessKey {
944
+ when: Date;
945
+ /** Optional content hash captured at refresh time; aids debugging. */
946
+ contentHash?: string;
1151
947
  }
1152
- interface TwoAgentResearchLoopResult {
948
+ interface KnowledgeFreshnessStore {
949
+ /** Last refresh time, or null if never refreshed. */
950
+ last(key: FreshnessKey): Promise<Date | null>;
951
+ /** Record a successful refresh. */
952
+ mark(input: FreshnessMark): Promise<void>;
953
+ /** True iff `last(key)` is null or older than `ttlMs`. */
954
+ stale(input: FreshnessTtl): Promise<boolean>;
955
+ /** All records for a workspace — useful for dashboards / debugging. */
956
+ list(workspaceId: string): Promise<FreshnessRecord[]>;
957
+ }
958
+ interface FreshnessRecord {
959
+ workspaceId: string;
960
+ sourceId: string;
961
+ lastRefreshedAt: string;
962
+ contentHash?: string;
963
+ }
964
+ interface FileSystemFreshnessStoreOptions {
965
+ /**
966
+ * Knowledge root. The store writes to `<root>/.agent-knowledge/freshness.json`,
967
+ * mirroring the convention used by `sources.json`.
968
+ */
1153
969
  root: string;
1154
- goal: string;
1155
- rounds: number;
1156
- ready: boolean;
1157
- index: KnowledgeIndex;
1158
- readiness?: EvalKnowledgeBundleBuildResult;
1159
- steps: TwoAgentResearchRound[];
1160
970
  }
1161
971
  /**
1162
- * Two-agent (driver + worker) sibling of `runKnowledgeResearchLoop`.
1163
- *
1164
- * Both agents research to grow ONE knowledge base. The roles are differentiated:
1165
- *
1166
- * - **WORKER** = primary research. Each round it reads the open gaps, discovers
1167
- * new sources, and proposes additions (`sources` + `proposalText`). It ADDS.
1168
- * - **DRIVER** = the verifier / gap-filler / gate. It (1) VERIFIES the worker's
1169
- * sources before they commit — dedup against the KB, then `verifySource`
1170
- * rejects ones that aren't real/relevant; (2) GAP-FILLS the gaps the worker
1171
- * missed with its own research pass (when `driverResearches`); (3) folds the
1172
- * remaining gaps into the worker's next prompt; and (4) GATES on
1173
- * `scoreKnowledgeReadiness` — the loop stops as soon as there are no blocking
1174
- * gaps.
1175
- *
1176
- * Set `driverResearches: false` (default) for the pure-coordinator mode: the
1177
- * driver only verifies + gates and contributes no research itself.
972
+ * Filesystem-backed implementation. Single JSON file per knowledge root,
973
+ * indexed by `${workspaceId}::${sourceId}`. Reads parse on every call —
974
+ * cron tick rate is well below the cost of one JSON parse.
1178
975
  *
1179
- * Composes existing atoms `initKnowledgeBase`, `addSourceText`,
1180
- * `applyKnowledgeWriteBlocks`, `buildEvalKnowledgeBundle` (the readiness gate),
1181
- * and `searchKnowledge` and reinvents none of them.
1182
- */
1183
- declare function runTwoAgentResearchLoop(options: TwoAgentResearchLoopOptions): Promise<TwoAgentResearchLoopResult>;
1184
- /**
1185
- * Helper for verifiers: does the candidate source's text/title overlap any page
1186
- * the readiness search returns for a gap query? A cheap relevance heuristic the
1187
- * driver can compose into `verifySource` (real verifiers can do more).
976
+ * Concurrent writes from a single process serialize through `writeQueue`.
977
+ * Cross-process concurrency is undefined; the consuming app should run the
978
+ * cron in a single worker.
1188
979
  */
1189
- declare function sourceMatchesGaps(source: ResearchSourceProposal, index: KnowledgeIndex, gaps: KnowledgeGap[]): KnowledgeSearchResult[];
1190
-
980
+ declare function createFileSystemFreshnessStore(options: FileSystemFreshnessStoreOptions): KnowledgeFreshnessStore;
1191
981
  /**
1192
- * Real web-research worker + verifying driver for `runTwoAgentResearchLoop`.
982
+ * D1 / Postgres adapter scaffold. Production consumers implement
983
+ * `D1Adapter` against their own driver (better-sqlite3, postgres,
984
+ * Cloudflare D1 binding, ...). This factory wires the adapter to the
985
+ * `KnowledgeFreshnessStore` interface.
1193
986
  *
1194
- * This is the GENERAL, any-topic implementation behind the two-agent research
1195
- * loop's live arm. Given the open knowledge gaps the readiness gate surfaces,
1196
- * the worker:
987
+ * The expected schema:
1197
988
  *
1198
- * 1. asks an LLM (glm-5.2 by default) to turn each gap into focused web
1199
- * search queries,
1200
- * 2. runs a REAL web search over the Tangle router (`POST /v1/search` — the
1201
- * same endpoint `tcloud mcp`'s `web_search` tool forwards to), so there is
1202
- * no hardcoded corpus,
1203
- * 3. fetches the top results with the repo's polite, cached `politeFetch` and
1204
- * reduces each page to text with `htmlToText`,
1205
- * 4. proposes the readable, verifiable pages as `ResearchSourceProposal`s plus
1206
- * a `buildPages` that writes citing `knowledge/*.md` pages from the sources
1207
- * the driver accepts.
1208
- *
1209
- * The verifying DRIVER is the differentiated role from the two-agent loop: a
1210
- * second LLM pass that judges each fetched source's on-topic relevance to the
1211
- * goal + open gaps and rejects off-topic / spam / already-covered material. The
1212
- * worker ADDS; the driver GATES. Together they build a cleaner knowledge base
1213
- * than a single agent at the same compute budget.
1214
- *
1215
- * Dependency-free on purpose: it talks to the router over `fetch` directly with
1216
- * the published OpenAI-compatible chat shape and the `/v1/search` shape, so it
1217
- * works whether or not the `tcloud` CLI is installed. Point it at any router by
1218
- * passing `baseUrl`; supply the key via `apiKey` or `TANGLE_API_KEY`.
989
+ * ```sql
990
+ * CREATE TABLE knowledge_freshness (
991
+ * workspace_id TEXT NOT NULL,
992
+ * source_id TEXT NOT NULL,
993
+ * last_refreshed_at TEXT NOT NULL,
994
+ * content_hash TEXT,
995
+ * PRIMARY KEY (workspace_id, source_id)
996
+ * );
997
+ * ```
1219
998
  */
999
+ interface D1Adapter {
1000
+ get(workspaceId: string, sourceId: string): Promise<FreshnessRecord | null>;
1001
+ upsert(record: FreshnessRecord): Promise<void>;
1002
+ listByWorkspace(workspaceId: string): Promise<FreshnessRecord[]>;
1003
+ }
1004
+ declare function createD1FreshnessStoreStub(adapter: D1Adapter): KnowledgeFreshnessStore;
1220
1005
 
1221
- /** One live web result, as the router's `/v1/search` returns it. */
1222
- interface WebSearchHit {
1223
- title: string;
1224
- url: string;
1225
- snippet?: string;
1006
+ interface ParsedFrontmatter {
1007
+ frontmatter: Record<string, unknown>;
1008
+ body: string;
1226
1009
  }
1227
- /**
1228
- * The two router capabilities the worker/driver need. Injectable so tests can
1229
- * stub the network; the default talks to the live Tangle router over `fetch`.
1230
- */
1231
- interface RouterClient {
1232
- /** Live web search — returns title/url/snippet hits. */
1233
- search(query: string, opts?: {
1234
- maxResults?: number;
1235
- }): Promise<WebSearchHit[]>;
1236
- /** Chat completion returns the assistant message's visible text. */
1237
- chat(messages: {
1238
- role: 'system' | 'user';
1239
- content: string;
1240
- }[], maxTokens?: number): Promise<string>;
1010
+ declare function parseFrontmatter(content: string): ParsedFrontmatter;
1011
+ declare function formatFrontmatter(frontmatter: Record<string, unknown>, body: string): string;
1012
+
1013
+ declare function buildKnowledgeGraph(pages: KnowledgePage[]): KnowledgeGraph;
1014
+
1015
+ declare function sha256(text: string): string;
1016
+ declare function slugify(input: string): string;
1017
+ declare function stableId(prefix: string, content: string): string;
1018
+
1019
+ declare function buildKnowledgeIndex(root: string): Promise<KnowledgeIndex>;
1020
+ declare function writeKnowledgeIndex(root: string): Promise<KnowledgeIndex>;
1021
+
1022
+ interface KnowledgeInspection {
1023
+ pageCount: number;
1024
+ sourceCount: number;
1025
+ expiredSourceCount: number;
1026
+ staleSourceCount: number;
1027
+ edgeCount: number;
1028
+ findingCount: number;
1029
+ blockingFindingCount: number;
1030
+ topPages: Array<{
1031
+ path: string;
1032
+ title: string;
1033
+ degree: number;
1034
+ sources: number;
1035
+ }>;
1036
+ sourceFreshness: SourceFreshnessInspection[];
1037
+ findings: KnowledgeLintFinding[];
1241
1038
  }
1242
- interface TangleRouterOptions {
1243
- /** Router base URL. Defaults to `https://router.tangle.tools/v1`. */
1244
- baseUrl?: string;
1245
- /** Bearer key. Defaults to `process.env.TANGLE_API_KEY`. */
1246
- apiKey?: string;
1247
- /** Chat model id. Defaults to `glm-5.2`. */
1248
- model?: string;
1249
- /** Optional preferred search provider (exa | you | perplexity | …). */
1250
- searchProvider?: string;
1251
- signal?: AbortSignal;
1039
+ interface SourceFreshnessInspection {
1040
+ id: string;
1041
+ title?: string;
1042
+ uri: string;
1043
+ status: 'fresh' | 'expired' | 'unknown';
1044
+ validUntil?: string;
1045
+ lastVerifiedAt?: string;
1252
1046
  }
1253
- /** A small error so a failed router call fails loud rather than returning junk. */
1254
- declare class RouterError extends Error {
1255
- readonly status: number;
1256
- constructor(status: number, message: string);
1047
+ declare function inspectKnowledgeIndex(index: KnowledgeIndex, options?: {
1048
+ now?: Date;
1049
+ }): KnowledgeInspection;
1050
+ interface KnowledgeExplanation {
1051
+ target: string;
1052
+ page?: KnowledgePage;
1053
+ sources: Array<{
1054
+ id: string;
1055
+ title?: string;
1056
+ uri: string;
1057
+ }>;
1058
+ links: string[];
1059
+ inbound: string[];
1060
+ related: Array<{
1061
+ path: string;
1062
+ title: string;
1063
+ score: number;
1064
+ }>;
1257
1065
  }
1258
- /**
1259
- * Build a dependency-free Tangle router client over `fetch`. This is the same
1260
- * wire surface the `tcloud` SDK + `tcloud mcp` use (`/v1/search` for web search,
1261
- * `/v1/chat/completions` for chat) so it needs no CLI installed.
1262
- */
1263
- declare function createTangleRouterClient(options?: TangleRouterOptions): RouterClient;
1264
- interface WebResearchWorkerOptions {
1265
- /** Router client. Defaults to a live Tangle router client from env creds. */
1266
- router?: RouterClient;
1267
- router_options?: TangleRouterOptions;
1268
- /** Max search queries the LLM may form per gap. Default 2. */
1269
- queriesPerGap?: number;
1270
- /** Max web results fetched per query. Default 3. */
1271
- resultsPerQuery?: number;
1272
- /** Hard cap on sources proposed per round (across all gaps). Default 6. */
1273
- maxSourcesPerRound?: number;
1274
- /** Disk cache dir for `politeFetch`. Optional; speeds repeat runs. */
1275
- cacheDir?: string;
1276
- /** Minimum readable text length to keep a fetched page. Default 200. */
1277
- minTextChars?: number;
1278
- /** Max chars of page text stored per source (keeps pages bounded). Default 4000. */
1279
- maxTextChars?: number;
1066
+ declare function explainKnowledgeTarget(index: KnowledgeIndex, target: string): KnowledgeExplanation;
1067
+
1068
+ interface KbStore {
1069
+ putSource(source: SourceRecord): Promise<void>;
1070
+ getSource(id: string): Promise<SourceRecord | null>;
1071
+ listSources(): Promise<SourceRecord[]>;
1072
+ putPage(page: KnowledgePage): Promise<void>;
1073
+ getPage(idOrPath: string): Promise<KnowledgePage | null>;
1074
+ listPages(): Promise<KnowledgePage[]>;
1075
+ putIndex(index: KnowledgeIndex): Promise<void>;
1076
+ getIndex(): Promise<KnowledgeIndex | null>;
1077
+ putEvent(event: KnowledgeEvent): Promise<void>;
1078
+ listEvents(query?: KnowledgeEventQuery): Promise<KnowledgeEvent[]>;
1079
+ }
1080
+ declare class MemoryKbStore implements KbStore {
1081
+ private readonly sources;
1082
+ private readonly pages;
1083
+ private readonly events;
1084
+ private index;
1085
+ putSource(source: SourceRecord): Promise<void>;
1086
+ getSource(id: string): Promise<SourceRecord | null>;
1087
+ listSources(): Promise<SourceRecord[]>;
1088
+ putPage(page: KnowledgePage): Promise<void>;
1089
+ getPage(idOrPath: string): Promise<KnowledgePage | null>;
1090
+ listPages(): Promise<KnowledgePage[]>;
1091
+ putIndex(index: KnowledgeIndex): Promise<void>;
1092
+ getIndex(): Promise<KnowledgeIndex | null>;
1093
+ putEvent(event: KnowledgeEvent): Promise<void>;
1094
+ listEvents(query?: KnowledgeEventQuery): Promise<KnowledgeEvent[]>;
1095
+ }
1096
+ declare class FileSystemKbStore extends MemoryKbStore {
1097
+ private readonly dir;
1098
+ constructor(dir: string);
1099
+ putIndex(index: KnowledgeIndex): Promise<void>;
1100
+ getIndex(): Promise<KnowledgeIndex | null>;
1101
+ putEvent(event: KnowledgeEvent): Promise<void>;
1102
+ }
1103
+
1104
+ declare function lintKnowledgeIndex(index: KnowledgeIndex): KnowledgeLintFinding[];
1105
+
1106
+ interface ApplyWriteBlocksResult {
1107
+ written: string[];
1108
+ warnings: string[];
1280
1109
  }
1110
+ declare function applyKnowledgeWriteBlocks(root: string, proposalText: string): Promise<ApplyWriteBlocksResult>;
1111
+ declare function applyKnowledgeWriteBlocksFile(root: string, proposalPath: string): Promise<ApplyWriteBlocksResult>;
1112
+
1281
1113
  /**
1282
- * The real web-research worker. Conforms to the loop's `ResearchWorker`
1283
- * contract: given the open gaps, it returns the sources it found plus a
1284
- * `buildPages` that emits citing pages from the sources the driver accepted.
1114
+ * Bridge from `AnalystFinding` (agent-eval) to knowledge proposals.
1115
+ *
1116
+ * Closes the failure wiki side of the recursive-self-improvement
1117
+ * loop: a knowledge-gap or knowledge-poisoning finding produced by an
1118
+ * analyst becomes a concrete proposal an operator (or auto-merge bot)
1119
+ * can review and apply. The bridge is intentionally lossless on the
1120
+ * fail-loud side — a finding the parser can't classify returns a
1121
+ * `KnowledgeProposalParseError` rather than a silent skip, so the
1122
+ * loop never accepts an underspecified edit.
1123
+ *
1124
+ * Subject grammar this bridge understands (analyst-side convention,
1125
+ * stamped in the kind prompts):
1126
+ *
1127
+ * agent-knowledge:wiki:<page-slug> create / update page
1128
+ * agent-knowledge:wiki:<page-slug>#<heading> insert section under page
1129
+ * agent-knowledge:claim:<topic> draft claim row
1130
+ * agent-knowledge:raw:<source-id> lift raw → curated
1131
+ * agent-knowledge:stale:<page-slug> mark page superseded
1132
+ *
1133
+ * Anything else (websearch:outdated:*, tool-doc:*, system-prompt:*,
1134
+ * memory:*) is NOT a knowledge-base concern and returns `null` so the
1135
+ * loop's improvement-applier handles it.
1285
1136
  */
1286
- declare function createWebResearchWorker(options?: WebResearchWorkerOptions): ResearchWorker;
1287
- interface VerifyingDriverOptions {
1288
- router?: RouterClient;
1289
- router_options?: TangleRouterOptions;
1137
+
1138
+ interface KnowledgeProposal {
1290
1139
  /**
1291
- * When the LLM verdict can't be parsed, default to REJECT (fail-closed) so a
1292
- * model hiccup never poisons the KB with an unverified source. Set `true` to
1293
- * accept-on-parse-failure only if you have a reason to. Default false.
1140
+ * Stable id derived from the finding so cross-run diffs share an
1141
+ * identity. Re-proposing the same finding produces the same id.
1294
1142
  */
1295
- acceptOnParseFailure?: boolean;
1143
+ id: string;
1144
+ /** The finding that generated this proposal — useful for audit + revert. */
1145
+ sourceFindingId: string;
1146
+ /** What the proposal does. */
1147
+ kind: 'create-page' | 'update-page' | 'append-section' | 'create-claim' | 'lift-raw' | 'mark-stale';
1148
+ /** Locus on disk (page slug or claim topic). */
1149
+ locus: string;
1150
+ /**
1151
+ * Page write blocks the standard `applyKnowledgeWriteBlocks` consumer
1152
+ * accepts. Empty for proposals that don't change page text (e.g.
1153
+ * `create-claim` produces a `claim` field instead).
1154
+ */
1155
+ writeBlocks: KnowledgeWriteBlock[];
1156
+ /**
1157
+ * Granular claim draft for proposals whose unit-of-change is a claim
1158
+ * row rather than a whole page. `status: 'draft'` until reviewed.
1159
+ */
1160
+ claim?: KnowledgeClaim;
1161
+ /** Per-proposal metadata: severity, confidence, source span. */
1162
+ metadata: {
1163
+ severity: AnalystSeverity;
1164
+ confidence: number;
1165
+ evidence_uri?: string;
1166
+ analyst_id: string;
1167
+ };
1168
+ }
1169
+ declare class KnowledgeProposalParseError extends Error {
1170
+ readonly findingId: string;
1171
+ readonly subject: string;
1172
+ constructor(findingId: string, subject: string, message: string);
1296
1173
  }
1297
1174
  /**
1298
- * The verifying driver: a real LLM pass that judges each candidate source's
1299
- * on-topic relevance to the goal + open gaps and whether it duplicates material
1300
- * already accepted this round. This is the differentiated coordinator role — it
1301
- * GATES the worker's additions; it adds nothing itself.
1175
+ * Convert one `AnalystFinding` into a knowledge proposal. Returns
1176
+ * `null` when the finding's locus isn't a knowledge-base concern
1177
+ * (`websearch:outdated:*`, `tool-doc:*`, `system-prompt:*`,
1178
+ * `memory:*`, missing subject). Throws when the locus IS a
1179
+ * knowledge-base concern but is malformed — that's a bug in the
1180
+ * analyst prompt and should fail loud.
1302
1181
  *
1303
- * The loop already dedups exact-uri duplicates and only calls this on genuinely
1304
- * new candidates, so the verifier focuses on relevance + near-duplicate
1305
- * judgement, not bookkeeping.
1182
+ * Caller convention: feed the function the analyst's full findings
1183
+ * list and filter out the `null`s; the orchestrator passes the
1184
+ * remaining proposals to the existing review / apply pipeline.
1306
1185
  */
1307
- declare function createVerifyingResearchDriver(options?: VerifyingDriverOptions): ResearchDriver;
1186
+ declare function proposeFromFinding(finding: AnalystFinding): KnowledgeProposal | null;
1187
+ /**
1188
+ * Plural convenience: filter + map across an entire findings batch
1189
+ * with one call. Parse errors collect into `errors[]`; the loop
1190
+ * decides per-error whether to abort or continue.
1191
+ */
1192
+ interface ProposeFromFindingsResult {
1193
+ proposals: KnowledgeProposal[];
1194
+ skipped: number;
1195
+ errors: KnowledgeProposalParseError[];
1196
+ }
1197
+ declare function proposeFromFindings(findings: ReadonlyArray<AnalystFinding>): ProposeFromFindingsResult;
1198
+
1199
+ interface KnowledgeReleaseReport {
1200
+ release: KnowledgeRelease;
1201
+ scorecard: ReleaseConfidenceScorecard;
1202
+ candidateRuns: RunRecord[];
1203
+ baselineRuns: RunRecord[];
1204
+ }
1205
+ /**
1206
+ * Campaign-native release report. The caller (a consumer's KB self-improvement
1207
+ * loop) supplies the candidate/baseline `RunRecord[]` (e.g. via
1208
+ * `campaignToRunRecords`) + optional per-instance `ReleaseTraceEvidence` + the
1209
+ * gate decision; this folds them into a `ReleaseConfidenceScorecard` + a
1210
+ * `KnowledgeRelease`. Release confidence is computed from run records + traces,
1211
+ * independent of any optimizer result shape.
1212
+ */
1213
+ interface KnowledgeReleaseInput {
1214
+ candidateId: string;
1215
+ baselineId?: string;
1216
+ candidateRuns: RunRecord[];
1217
+ baselineRuns?: RunRecord[];
1218
+ traces?: ReleaseTraceEvidence[];
1219
+ gateDecision?: GateDecision | null;
1220
+ /**
1221
+ * True when a held-out split was evaluated (drives the holdout threshold).
1222
+ *
1223
+ * Constraint: the substrate gate keys the holdout requirement off a scenario
1224
+ * corpus, which this run-only report does not carry. With `hasHoldout: true`
1225
+ * the gate fails closed (`missing_holdout_split`) even when holdout RunRecords
1226
+ * are supplied. Callers that gate on a real held-out split should drive
1227
+ * `evaluateReleaseConfidence` directly with a dataset/scenarios.
1228
+ */
1229
+ hasHoldout?: boolean;
1230
+ /** Candidate is the search-best variant — a promotion precondition. Default true. */
1231
+ promotedIsBest?: boolean;
1232
+ createdAt?: string;
1233
+ minScore?: number;
1234
+ }
1235
+ declare function knowledgeReleaseReport(input: KnowledgeReleaseInput): KnowledgeReleaseReport;
1236
+
1237
+ interface ValidateKnowledgeOptions {
1238
+ strict?: boolean;
1239
+ }
1240
+ interface ValidateKnowledgeResult {
1241
+ ok: boolean;
1242
+ findings: KnowledgeLintFinding[];
1243
+ }
1244
+ declare function validateKnowledgeIndex(index: KnowledgeIndex, options?: ValidateKnowledgeOptions): ValidateKnowledgeResult;
1245
+
1246
+ interface KnowledgeResearchLoopContext {
1247
+ root: string;
1248
+ goal: string;
1249
+ iteration: number;
1250
+ index: KnowledgeIndex;
1251
+ lintFindings: KnowledgeLintFinding[];
1252
+ validation: ValidateKnowledgeResult;
1253
+ readiness?: EvalKnowledgeBundleBuildResult;
1254
+ previousSteps: KnowledgeResearchLoopStep[];
1255
+ signal?: AbortSignal;
1256
+ }
1257
+ interface KnowledgeResearchLoopDecision {
1258
+ /**
1259
+ * Free-form notes from the researcher. Keep this human-readable; products can
1260
+ * store it as the research transcript.
1261
+ */
1262
+ notes?: string;
1263
+ /**
1264
+ * Local files to register as immutable sources before applying proposals.
1265
+ */
1266
+ sourcePaths?: string[];
1267
+ /**
1268
+ * Textual source artifacts discovered by an agent, browser worker, connector,
1269
+ * or deep-research process.
1270
+ */
1271
+ sourceTexts?: AddSourceTextInput[];
1272
+ /**
1273
+ * Safe write protocol text. The loop parses and applies only accepted
1274
+ * `---FILE: knowledge/...---` blocks.
1275
+ */
1276
+ proposalText?: string;
1277
+ /**
1278
+ * The researcher decides when the wiki is good enough. The loop deliberately
1279
+ * does not encode a domain-specific definition of "done".
1280
+ */
1281
+ done?: boolean;
1282
+ metadata?: Record<string, unknown>;
1283
+ }
1284
+ interface KnowledgeResearchLoopStep {
1285
+ iteration: number;
1286
+ notes?: string;
1287
+ addedSources: SourceRecord[];
1288
+ applied?: ApplyWriteBlocksResult;
1289
+ lintFindings: KnowledgeLintFinding[];
1290
+ validation: ValidateKnowledgeResult;
1291
+ readiness?: EvalKnowledgeBundleBuildResult;
1292
+ event: KnowledgeEvent;
1293
+ done: boolean;
1294
+ metadata?: Record<string, unknown>;
1295
+ }
1296
+ interface RunKnowledgeResearchLoopOptions {
1297
+ root: string;
1298
+ goal: string;
1299
+ maxIterations?: number;
1300
+ actor?: string;
1301
+ strict?: ValidateKnowledgeOptions['strict'];
1302
+ readinessSpecs?: KnowledgeReadinessSpec[];
1303
+ readinessTaskId?: string;
1304
+ readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
1305
+ sourceOptions?: Pick<AddSourceOptions, 'adapters' | 'now'>;
1306
+ signal?: AbortSignal;
1307
+ step(context: KnowledgeResearchLoopContext): Promise<KnowledgeResearchLoopDecision> | KnowledgeResearchLoopDecision;
1308
+ onStep?: (step: KnowledgeResearchLoopStep) => Promise<void> | void;
1309
+ }
1310
+ interface KnowledgeResearchLoopResult {
1311
+ root: string;
1312
+ goal: string;
1313
+ iterations: number;
1314
+ done: boolean;
1315
+ index: KnowledgeIndex;
1316
+ lintFindings: KnowledgeLintFinding[];
1317
+ validation: ValidateKnowledgeResult;
1318
+ readiness?: EvalKnowledgeBundleBuildResult;
1319
+ steps: KnowledgeResearchLoopStep[];
1320
+ }
1321
+ type KnowledgeControlLoopState = KnowledgeResearchLoopContext;
1322
+ type KnowledgeControlLoopAction = KnowledgeResearchLoopDecision;
1323
+ type KnowledgeControlLoopActionResult = KnowledgeResearchLoopStep;
1324
+ interface KnowledgeControlLoopAdapterOptions {
1325
+ root: string;
1326
+ goal: string;
1327
+ actor?: string;
1328
+ strict?: ValidateKnowledgeOptions['strict'];
1329
+ readinessSpecs?: KnowledgeReadinessSpec[];
1330
+ readinessTaskId?: string;
1331
+ readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
1332
+ sourceOptions?: Pick<AddSourceOptions, 'adapters' | 'now'>;
1333
+ }
1334
+ type KnowledgeControlLoopAdapter = Pick<ControlRuntimeConfig<KnowledgeControlLoopState, KnowledgeControlLoopAction, KnowledgeControlLoopActionResult, ControlEvalResult>, 'intent' | 'observe' | 'validate' | 'act' | 'shouldStop'>;
1335
+ /**
1336
+ * Adapter for running knowledge growth through `agent-eval`'s generic control
1337
+ * runtime. The caller still owns `decide`: that can be a proposer agent,
1338
+ * reviewer agent, deterministic policy, or a composition of all three.
1339
+ */
1340
+ declare function createKnowledgeControlLoopAdapter(options: KnowledgeControlLoopAdapterOptions): KnowledgeControlLoopAdapter;
1341
+ declare function runKnowledgeResearchLoop(options: RunKnowledgeResearchLoopOptions): Promise<KnowledgeResearchLoopResult>;
1342
+
1343
+ /**
1344
+ * Default standing instructions for a research supervisor. It does NOT solve the
1345
+ * research itself — it DECOMPOSES the goal into sub-topics and spawns one
1346
+ * researcher per sub-topic over the live `Scope`, widening/narrowing until the
1347
+ * readiness gate (the deliverable check) reports the knowledge base is ready.
1348
+ */
1349
+ declare const RESEARCH_SUPERVISOR_SYSTEM_PROMPT: string;
1350
+ interface ResearchSupervisorOptions {
1351
+ /** Where the knowledge base lives. The deliverable check reads readiness here. */
1352
+ root: string;
1353
+ /** The research goal handed to the supervisor as its task. */
1354
+ goal: string;
1355
+ /**
1356
+ * Readiness specs define DONE: the supervisor's deliverable check passes once
1357
+ * `scoreKnowledgeReadiness` reports no blocking gaps over the live KB. Required
1358
+ * — without a gate the supervisor would never know when to stop.
1359
+ */
1360
+ readinessSpecs: KnowledgeReadinessSpec[];
1361
+ readinessTaskId?: string;
1362
+ readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
1363
+ /** The conserved compute pool for the whole supervised run. */
1364
+ budget: Budget;
1365
+ /**
1366
+ * WHERE researcher workers run — the caller's real backend seam (sandbox with
1367
+ * a `sandboxClient`, or router/cli/bridge). The supervisor spawns researchers
1368
+ * on this backend; assembling the seam (creds, sandbox SDK) is the caller's,
1369
+ * so there is no fabricated default.
1370
+ */
1371
+ backend: ExecutorConfig;
1372
+ /** Harness for the researcher worker profile (default `researcherProfile`'s). */
1373
+ harness?: string;
1374
+ /** The supervisor brain's router model. */
1375
+ supervisorModel?: string;
1376
+ /** Custom supervisor instructions. Defaults to {@link RESEARCH_SUPERVISOR_SYSTEM_PROMPT}. */
1377
+ supervisorSystemPrompt?: string;
1378
+ /** Extra `supervise()` knobs (perWorker, maxLiveWorkers, router, allowedModels, …). */
1379
+ superviseOptions?: Partial<Omit<SuperviseOptions, 'budget' | 'backend' | 'deliverable'>>;
1380
+ }
1381
+ /**
1382
+ * Build the deliverable (completion oracle) for a research supervisor: "settled
1383
+ * ⟺ the knowledge base is ready". It re-reads the KB from disk and runs the
1384
+ * readiness gate, so the supervisor stops on the REAL grounded state — not on a
1385
+ * worker's self-report.
1386
+ */
1387
+ declare function knowledgeReadinessDeliverable(options: Pick<ResearchSupervisorOptions, 'root' | 'goal' | 'readinessSpecs' | 'readinessTaskId' | 'readiness'>): DeliverableSpec<unknown>;
1388
+ /**
1389
+ * A research supervisor: a SUPERVISOR brain creates the topology dynamically —
1390
+ * how many researchers, split by sub-topic — over the `Scope`, with the
1391
+ * knowledge-readiness gate as its stop condition.
1392
+ *
1393
+ * This is a thin wrapper over `supervise(profile, goal, opts)`. It composes the
1394
+ * existing pieces and builds nothing new: the researcher `AgentProfile` (from
1395
+ * `researcherProfile`) is the worker shape the supervisor spawns, and
1396
+ * `knowledgeReadinessDeliverable` (over `scoreKnowledgeReadiness`) is the
1397
+ * completion oracle that decides when the KB is ready.
1398
+ *
1399
+ * Needs creds (a real supervisor router brain + a worker backend), so it is the
1400
+ * LIVE path. The offline two-agent loop is `runTwoAgentResearchLoop`.
1401
+ */
1402
+ declare function runResearchSupervisor(options: ResearchSupervisorOptions): Promise<SupervisedResult<unknown>>;
1403
+
1404
+ declare const SourceAnchorSchema: z.ZodObject<{
1405
+ id: z.ZodString;
1406
+ sourceId: z.ZodString;
1407
+ label: z.ZodOptional<z.ZodString>;
1408
+ page: z.ZodOptional<z.ZodNumber>;
1409
+ lineStart: z.ZodOptional<z.ZodNumber>;
1410
+ lineEnd: z.ZodOptional<z.ZodNumber>;
1411
+ charStart: z.ZodOptional<z.ZodNumber>;
1412
+ charEnd: z.ZodOptional<z.ZodNumber>;
1413
+ timestampMs: z.ZodOptional<z.ZodNumber>;
1414
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1415
+ }, z.core.$strip>;
1416
+ declare const SourceRecordSchema: z.ZodObject<{
1417
+ id: z.ZodString;
1418
+ uri: z.ZodString;
1419
+ title: z.ZodOptional<z.ZodString>;
1420
+ mediaType: z.ZodOptional<z.ZodString>;
1421
+ contentHash: z.ZodString;
1422
+ text: z.ZodOptional<z.ZodString>;
1423
+ anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
1424
+ id: z.ZodString;
1425
+ sourceId: z.ZodString;
1426
+ label: z.ZodOptional<z.ZodString>;
1427
+ page: z.ZodOptional<z.ZodNumber>;
1428
+ lineStart: z.ZodOptional<z.ZodNumber>;
1429
+ lineEnd: z.ZodOptional<z.ZodNumber>;
1430
+ charStart: z.ZodOptional<z.ZodNumber>;
1431
+ charEnd: z.ZodOptional<z.ZodNumber>;
1432
+ timestampMs: z.ZodOptional<z.ZodNumber>;
1433
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1434
+ }, z.core.$strip>>>;
1435
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1436
+ createdAt: z.ZodString;
1437
+ }, z.core.$strip>;
1438
+ declare const KnowledgePageSchema: z.ZodObject<{
1439
+ id: z.ZodString;
1440
+ path: z.ZodString;
1441
+ title: z.ZodString;
1442
+ text: z.ZodString;
1443
+ frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
1444
+ sourceIds: z.ZodArray<z.ZodString>;
1445
+ tags: z.ZodArray<z.ZodString>;
1446
+ outLinks: z.ZodArray<z.ZodString>;
1447
+ }, z.core.$strip>;
1448
+ declare const KnowledgeGraphNodeSchema: z.ZodObject<{
1449
+ id: z.ZodString;
1450
+ title: z.ZodString;
1451
+ path: z.ZodString;
1452
+ tags: z.ZodArray<z.ZodString>;
1453
+ sourceIds: z.ZodArray<z.ZodString>;
1454
+ outDegree: z.ZodNumber;
1455
+ inDegree: z.ZodNumber;
1456
+ }, z.core.$strip>;
1457
+ declare const KnowledgeGraphEdgeSchema: z.ZodObject<{
1458
+ source: z.ZodString;
1459
+ target: z.ZodString;
1460
+ weight: z.ZodNumber;
1461
+ reasons: z.ZodArray<z.ZodString>;
1462
+ }, z.core.$strip>;
1463
+ declare const KnowledgeIndexSchema: z.ZodObject<{
1464
+ root: z.ZodString;
1465
+ generatedAt: z.ZodString;
1466
+ sources: z.ZodArray<z.ZodObject<{
1467
+ id: z.ZodString;
1468
+ uri: z.ZodString;
1469
+ title: z.ZodOptional<z.ZodString>;
1470
+ mediaType: z.ZodOptional<z.ZodString>;
1471
+ contentHash: z.ZodString;
1472
+ text: z.ZodOptional<z.ZodString>;
1473
+ anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
1474
+ id: z.ZodString;
1475
+ sourceId: z.ZodString;
1476
+ label: z.ZodOptional<z.ZodString>;
1477
+ page: z.ZodOptional<z.ZodNumber>;
1478
+ lineStart: z.ZodOptional<z.ZodNumber>;
1479
+ lineEnd: z.ZodOptional<z.ZodNumber>;
1480
+ charStart: z.ZodOptional<z.ZodNumber>;
1481
+ charEnd: z.ZodOptional<z.ZodNumber>;
1482
+ timestampMs: z.ZodOptional<z.ZodNumber>;
1483
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1484
+ }, z.core.$strip>>>;
1485
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1486
+ createdAt: z.ZodString;
1487
+ }, z.core.$strip>>;
1488
+ pages: z.ZodArray<z.ZodObject<{
1489
+ id: z.ZodString;
1490
+ path: z.ZodString;
1491
+ title: z.ZodString;
1492
+ text: z.ZodString;
1493
+ frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
1494
+ sourceIds: z.ZodArray<z.ZodString>;
1495
+ tags: z.ZodArray<z.ZodString>;
1496
+ outLinks: z.ZodArray<z.ZodString>;
1497
+ }, z.core.$strip>>;
1498
+ graph: z.ZodObject<{
1499
+ nodes: z.ZodArray<z.ZodObject<{
1500
+ id: z.ZodString;
1501
+ title: z.ZodString;
1502
+ path: z.ZodString;
1503
+ tags: z.ZodArray<z.ZodString>;
1504
+ sourceIds: z.ZodArray<z.ZodString>;
1505
+ outDegree: z.ZodNumber;
1506
+ inDegree: z.ZodNumber;
1507
+ }, z.core.$strip>>;
1508
+ edges: z.ZodArray<z.ZodObject<{
1509
+ source: z.ZodString;
1510
+ target: z.ZodString;
1511
+ weight: z.ZodNumber;
1512
+ reasons: z.ZodArray<z.ZodString>;
1513
+ }, z.core.$strip>>;
1514
+ }, z.core.$strip>;
1515
+ }, z.core.$strip>;
1516
+ declare const KnowledgeEventSchema: z.ZodObject<{
1517
+ id: z.ZodString;
1518
+ type: z.ZodEnum<{
1519
+ "source.added": "source.added";
1520
+ "proposal.applied": "proposal.applied";
1521
+ "index.built": "index.built";
1522
+ "lint.run": "lint.run";
1523
+ "optimization.run": "optimization.run";
1524
+ "release.promoted": "release.promoted";
1525
+ "release.rejected": "release.rejected";
1526
+ }>;
1527
+ createdAt: z.ZodString;
1528
+ actor: z.ZodOptional<z.ZodString>;
1529
+ target: z.ZodOptional<z.ZodString>;
1530
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1531
+ }, z.core.$strip>;
1532
+ declare const KnowledgeBaseCandidateSchema: z.ZodObject<{
1533
+ id: z.ZodString;
1534
+ units: z.ZodArray<z.ZodObject<{
1535
+ id: z.ZodString;
1536
+ title: z.ZodString;
1537
+ text: z.ZodString;
1538
+ claims: z.ZodOptional<z.ZodArray<z.ZodObject<{
1539
+ id: z.ZodString;
1540
+ text: z.ZodString;
1541
+ refs: z.ZodArray<z.ZodObject<{
1542
+ sourceId: z.ZodString;
1543
+ anchorId: z.ZodOptional<z.ZodString>;
1544
+ quote: z.ZodOptional<z.ZodString>;
1545
+ }, z.core.$strip>>;
1546
+ confidence: z.ZodOptional<z.ZodNumber>;
1547
+ status: z.ZodOptional<z.ZodEnum<{
1548
+ draft: "draft";
1549
+ active: "active";
1550
+ superseded: "superseded";
1551
+ rejected: "rejected";
1552
+ }>>;
1553
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1554
+ }, z.core.$strip>>>;
1555
+ relations: z.ZodOptional<z.ZodArray<z.ZodObject<{
1556
+ sourceId: z.ZodString;
1557
+ targetId: z.ZodString;
1558
+ predicate: z.ZodString;
1559
+ weight: z.ZodOptional<z.ZodNumber>;
1560
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1561
+ }, z.core.$strip>>>;
1562
+ sourceIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
1563
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1564
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1565
+ updatedAt: z.ZodOptional<z.ZodString>;
1566
+ }, z.core.$strip>>;
1567
+ retrievalPolicy: z.ZodOptional<z.ZodString>;
1568
+ synthesisPolicy: z.ZodOptional<z.ZodString>;
1569
+ questionPolicy: z.ZodOptional<z.ZodString>;
1570
+ updatePolicy: z.ZodOptional<z.ZodString>;
1571
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1572
+ }, z.core.$strip>;
1573
+
1574
+ declare function searchKnowledge(index: KnowledgeIndex, query: string, limit?: number): KnowledgeSearchResult[];
1575
+ declare function tokenizeQuery(query: string): string[];
1576
+ declare function reciprocalRankFusion(rankLists: string[][], k?: number): Map<string, number>;
1577
+
1578
+ interface KnowledgeLayout {
1579
+ root: string;
1580
+ knowledgeDir: string;
1581
+ rawSourcesDir: string;
1582
+ sourceRegistryPath: string;
1583
+ indexPath: string;
1584
+ logPath: string;
1585
+ cacheDir: string;
1586
+ }
1587
+ declare function layoutFor(root: string): KnowledgeLayout;
1588
+ /**
1589
+ * Filenames that `initKnowledgeBase` writes as human-navigation scaffolding.
1590
+ * These are excluded from the page index — they exist on disk so authors can
1591
+ * curate their vault, but they are not searchable content.
1592
+ *
1593
+ * Add new scaffold filenames here (and only here) to keep lint, validate, viz,
1594
+ * and the indexer consistent.
1595
+ */
1596
+ declare const SCAFFOLD_PAGE_BASENAMES: readonly string[];
1597
+ /**
1598
+ * True when a knowledge-relative path points at a scaffold file rather than
1599
+ * authored content. Accepts both repo-relative paths (`knowledge/index.md`)
1600
+ * and any nested `<dir>/index.md` or `<dir>/log.md` (for example
1601
+ * `knowledge/concepts/index.md`) so subdirectory README-style scaffolds are
1602
+ * also excluded.
1603
+ */
1604
+ declare function isScaffoldPath(path: string): boolean;
1605
+ declare function initKnowledgeBase(root: string): Promise<KnowledgeLayout>;
1606
+ declare function loadKnowledgePages(root: string): Promise<KnowledgePage[]>;
1607
+ declare function writeJson(path: string, value: unknown): Promise<void>;
1308
1608
 
1309
1609
  declare const WIKILINK_REGEX: RegExp;
1310
1610
  declare function extractWikilinks(content: string): string[];
@@ -1313,4 +1613,4 @@ declare function normalizeLinkTarget(target: string): string;
1313
1613
  declare function isSafeKnowledgePath(path: string, allowedPrefixes?: string[]): boolean;
1314
1614
  declare function parseKnowledgeWriteBlocks(text: string, allowedPrefixes?: string[]): KnowledgeWriteParseResult;
1315
1615
 
1316
- export { type AddSourceOptions, type AddSourceTextInput, type ApplyWriteBlocksResult, type BuildEvalKnowledgeBundleOptions, type ChunkingOptions, type D1Adapter, type DefineReadinessSpecInput, type DetectChangesOptions, type DetectChangesResult, type DiscoveryResult, type DiscoveryTask, type DriverResearchContext, type EvalKnowledgeBundleBuildResult, type FileSystemFreshnessStoreOptions, FileSystemKbStore, type FreshnessKey, type FreshnessMark, type FreshnessRecord, type FreshnessTtl, type KbStore, KnowledgeBaseCandidateSchema, type KnowledgeChange, type KnowledgeChangeKind, type KnowledgeChunk, KnowledgeClaim, type KnowledgeControlLoopAction, type KnowledgeControlLoopActionResult, type KnowledgeControlLoopAdapter, type KnowledgeControlLoopAdapterOptions, type KnowledgeControlLoopState, type KnowledgeDiscoveryDispatcher, type KnowledgeDiscoveryWorker, KnowledgeEvent, type KnowledgeEventQuery, KnowledgeEventSchema, KnowledgeEventType, type KnowledgeExplanation, KnowledgeFragment, type KnowledgeFreshnessStore, type KnowledgeGap, KnowledgeGraph, KnowledgeGraphEdgeSchema, KnowledgeGraphNodeSchema, KnowledgeIndex, KnowledgeIndexSchema, type KnowledgeInspection, type KnowledgeLayout, KnowledgeLintFinding, KnowledgePage, KnowledgePageSchema, type KnowledgeProposal, KnowledgeProposalParseError, type KnowledgeReadinessSpec, KnowledgeRelease, type KnowledgeReleaseInput, type KnowledgeReleaseReport, type KnowledgeResearchLoopContext, type KnowledgeResearchLoopDecision, type KnowledgeResearchLoopResult, type KnowledgeResearchLoopStep, KnowledgeSearchResult, KnowledgeWriteBlock, KnowledgeWriteParseResult, MemoryKbStore, type ParsedFrontmatter, type ProposeFromFindingsResult, READINESS_SPEC_DEFAULTS, RESEARCH_SUPERVISOR_SYSTEM_PROMPT, type RejectedSource, type ResearchContribution, type ResearchDriver, type ResearchSourceProposal, type ResearchSupervisorOptions, type ResearchWorker, type RouterClient, RouterError, type RunKnowledgeResearchLoopOptions, SCAFFOLD_PAGE_BASENAMES, type SourceAdapter, type SourceAdapterInput, type SourceAdapterOutput, SourceAnchorSchema, type SourceFreshnessInspection, SourceRecord, SourceRecordSchema, SourceRegistry, type SourceVerdict, type SourceVerificationContext, type TangleRouterOptions, type TwoAgentResearchLoopOptions, type TwoAgentResearchLoopResult, type TwoAgentResearchRound, type ValidateKnowledgeOptions, type ValidateKnowledgeResult, type VerifyingDriverOptions, WIKILINK_REGEX, type WebResearchWorkerOptions, type WebSearchHit, type WorkerResearchContext, addSourcePath, addSourceText, applyKnowledgeWriteBlocks, applyKnowledgeWriteBlocksFile, buildEvalKnowledgeBundle, buildKnowledgeGraph, buildKnowledgeIndex, chunkMarkdown, createD1FreshnessStoreStub, createFileSystemFreshnessStore, createKnowledgeControlLoopAdapter, createKnowledgeEvent, createLocalDiscoveryDispatcher, createTangleRouterClient, createVerifyingResearchDriver, createWebResearchWorker, defineReadinessSpec, detectChanges, explainKnowledgeTarget, extractWikilinks, formatFrontmatter, initKnowledgeBase, inspectKnowledgeIndex, isSafeKnowledgePath, isScaffoldPath, knowledgeReadinessDeliverable, knowledgeReleaseReport, layoutFor, lintKnowledgeIndex, loadKnowledgePages, loadSourceRegistry, mediaTypeFor, normalizeLinkTarget, parseFrontmatter, parseKnowledgeWriteBlocks, proposeFromFinding, proposeFromFindings, reciprocalRankFusion, runKnowledgeResearchLoop, runResearchSupervisor, runTwoAgentResearchLoop, searchKnowledge, sha256, slugify, sourceMatchesGaps, sourceRegistryPath, stableId, stripFrontmatter, textSourceAdapter, tokenizeQuery, validateKnowledgeIndex, writeJson, writeKnowledgeIndex, writeSourceRegistry };
1616
+ export { type AdaptiveDecision, type AdaptiveDriverOptions, type AdaptiveResearchDriver, type AdaptiveStats, type AddSourceOptions, type AddSourceTextInput, type ApplyWriteBlocksResult, type BuildEvalKnowledgeBundleOptions, type ChunkingOptions, type ClaimGroundingDriverOptions, type D1Adapter, type DedupReason, type DefineReadinessSpecInput, type DetectChangesOptions, type DetectChangesResult, type DiscoveryResult, type DiscoveryTask, type DriverResearchContext, type EvalKnowledgeBundleBuildResult, type FileSystemFreshnessStoreOptions, FileSystemKbStore, type FreshnessKey, type FreshnessMark, type FreshnessRecord, type FreshnessTtl, type GroundClaimOptions, type GroundingResult, type KbStore, KnowledgeBaseCandidateSchema, type KnowledgeChange, type KnowledgeChangeKind, type KnowledgeChunk, KnowledgeClaim, type KnowledgeControlLoopAction, type KnowledgeControlLoopActionResult, type KnowledgeControlLoopAdapter, type KnowledgeControlLoopAdapterOptions, type KnowledgeControlLoopState, type KnowledgeDiscoveryDispatcher, type KnowledgeDiscoveryWorker, KnowledgeEvent, type KnowledgeEventQuery, KnowledgeEventSchema, KnowledgeEventType, type KnowledgeExplanation, KnowledgeFragment, type KnowledgeFreshnessStore, type KnowledgeGap, KnowledgeGraph, KnowledgeGraphEdgeSchema, KnowledgeGraphNodeSchema, KnowledgeIndex, KnowledgeIndexSchema, type KnowledgeInspection, type KnowledgeLayout, KnowledgeLintFinding, KnowledgePage, KnowledgePageSchema, type KnowledgeProposal, KnowledgeProposalParseError, type KnowledgeReadinessSpec, KnowledgeRelease, type KnowledgeReleaseInput, type KnowledgeReleaseReport, type KnowledgeResearchLoopContext, type KnowledgeResearchLoopDecision, type KnowledgeResearchLoopResult, type KnowledgeResearchLoopStep, KnowledgeSearchResult, KnowledgeWriteBlock, KnowledgeWriteParseResult, MemoryKbStore, type ParsedFrontmatter, type ProposeFromFindingsResult, READINESS_SPEC_DEFAULTS, RESEARCH_SUPERVISOR_SYSTEM_PROMPT, type RejectedSource, type ResearchContribution, type ResearchDriver, type ResearchSourceProposal, type ResearchSupervisorOptions, type ResearchWorker, type RouterClient, RouterError, type RouterUsage, type RunKnowledgeResearchLoopOptions, SCAFFOLD_PAGE_BASENAMES, type SourceAdapter, type SourceAdapterInput, type SourceAdapterOutput, SourceAnchorSchema, type SourceFreshnessInspection, SourceRecord, SourceRecordSchema, SourceRegistry, type SourceVerdict, type SourceVerificationContext, type TangleRouterOptions, type TriageClass, type TwoAgentResearchLoopOptions, type TwoAgentResearchLoopResult, type TwoAgentResearchRound, type ValidateKnowledgeOptions, type ValidateKnowledgeResult, type VerifyingDriverOptions, WIKILINK_REGEX, type WebResearchWorkerOptions, type WebSearchHit, type WorkerClaimDecorationOptions, type WorkerResearchContext, addSourcePath, addSourceText, applyKnowledgeWriteBlocks, applyKnowledgeWriteBlocksFile, buildEvalKnowledgeBundle, buildKnowledgeGraph, buildKnowledgeIndex, canonicalizeUrl, chunkMarkdown, citedClaimKey, citedClaimOf, contentKey, createAdaptiveResearchDriver, createClaimDecorator, createClaimGroundingVerifier, createD1FreshnessStoreStub, createFileSystemFreshnessStore, createKnowledgeControlLoopAdapter, createKnowledgeEvent, createLocalDiscoveryDispatcher, createTangleRouterClient, createVerifyingResearchDriver, createWebResearchWorker, defineReadinessSpec, detectChanges, explainKnowledgeTarget, extractWikilinks, formatFrontmatter, groundClaimInText, initKnowledgeBase, inspectKnowledgeIndex, isSafeKnowledgePath, isScaffoldPath, knowledgeReadinessDeliverable, knowledgeReleaseReport, layoutFor, lintKnowledgeIndex, loadKnowledgePages, loadSourceRegistry, mediaTypeFor, normalizeLinkTarget, parseFrontmatter, parseKnowledgeWriteBlocks, proposeFromFinding, proposeFromFindings, reciprocalRankFusion, runKnowledgeResearchLoop, runResearchSupervisor, runTwoAgentResearchLoop, searchKnowledge, sha256, slugify, sourceMatchesGaps, sourceRegistryPath, stableId, stripFrontmatter, textSourceAdapter, tokenizeQuery, triageSource, validateKnowledgeIndex, withCitedClaim, writeJson, writeKnowledgeIndex, writeSourceRegistry };