@tangle-network/agent-knowledge 17.1.10 → 18.0.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.js CHANGED
@@ -1,10 +1,11 @@
1
- import { $ as formatKnowledgeCitationReference, $t as inspectPendingKnowledgeMutation, A as addSourceText, An as writeJsonDurableWithinRoot, At as ResearchSourceVersionSchema, B as KB_STORE_DIR, Bt as claimId, C as gradeClaims, Cn as renameDurable, Ct as KnowledgePageSchema, D as buildKnowledgeIndex, Dn as writeFileDurable, Dt as ResearchClaimEvidenceSchema, E as verifyGradeableEvidence, En as withSafeDirectory, Et as KnowledgeRelationSchema, F as ClaimLedgerMigrationRequiredError, Ft as assertDeepQuestionIntegrity, G as KnowledgeCitationAuditError, Gt as materializeRegisteredClaimEvidence, H as assertClaimLedgerId, Ht as deepQuestionId, I as FileSystemKbStore, It as assertResearchClaimEvidenceIntegrity, J as assertKnowledgeCitationAudit, Jt as normalizeClaimText, K as KnowledgeCitationResolutionError, Kt as mergeClaimLedgers, L as KB_CLAIM_LEDGER_DIR, Lt as assertResearchClaimLedgerIntegrity, M as snapshotSourceTextInput, Mn as textSourceAdapter, Mt as SourceRecordSchema, N as sourceRegistryPath, Nt as KNOWLEDGE_EVENT_TYPES, O as writeKnowledgeIndex, On as writeFileDurableWithinRoot, Ot as ResearchClaimLedgerSchema, P as writeSourceRegistry, Pt as ClaimLedgerGoalConflictError, Q as auditKnowledgeCitations, Qt as acquireDurableFileLock, R as KB_EVENTS_PATH, Rt as assertTrackedClaimIntegrity, S as claimCheckKey, Sn as removeDurable, St as KnowledgePageInvalidationSchema, T as verdictFor, Tn as withSafeDescendant, Tt as KnowledgeRelationNodeSchema, U as buildKnowledgeGraph, Ut as emptyClaimLedger, V as MemoryKbStore, Vt as claimSourceHost, W as knowledgePageRelations, Wt as linkClaimContradictions, X as assertRunScopedCitationsResolved, Xt as formatFrontmatter, Y as assertKnowledgeCitationsResolved, Yt as researchSourceVersionKey, Z as auditCurrentRunCitations, Zt as parseFrontmatter, _ as lintKnowledgeIndex, _n as isMissingFile, _t as KnowledgeBaseCandidateSchema, a as applyKnowledgeWriteBlocksFile, an as applyKnowledgeFileTransaction, at as SCAFFOLD_PAGE_BASENAMES, b as UncheckableClaimError, bn as readRegularFileWithinRoot, bt as KnowledgeGraphNodeSchema, c as KnowledgeDuplicateIntakeError, cn as finishKnowledgeFileTransaction, ct as isScaffoldPath, d as createFileRunLineageAuthority, dn as rollbackKnowledgeFileTransaction, dt as loadKnowledgePages, en as isKnowledgeMutationHeld, et as parseKnowledgeCitationReference, f as createRunScopedStores, fn as DEFAULT_PAGES_DIRECTORY, ft as writeJson, g as validateKnowledgeIndex, gn as isKernelAnchoredPath, gt as DeepQuestionSchema, h as normalizePageText, hn as canonicalRelativeWithinRoot, ht as normalizeLinkTarget, i as applyKnowledgeWriteBlocks, in as withKnowledgeRead, it as resolveRunScopedCitations, j as loadSourceRegistry, jn as mediaTypeFor, jt as SourceAnchorSchema, k as addSourcePath, kn as writeJsonDurable, kt as ResearchClaimRecordSchema, l as assertKnowledgeWriteIntake, ln as knowledgeFileTransactionPlanHash, lt as knowledgePageFromMarkdown, m as detectNearDuplicatePages, mn as canonicalPathsEqual, mt as extractWikilinks, n as inspectKnowledgeIndex, nn as runInKnowledgeMutationScope, nt as resolveKnowledgeCitations, o as isSafeKnowledgePath, on as assertKnowledgeMutationPath, ot as initKnowledgeBase, p as originatedPages, pn as normalizePagesDirectory, pt as WIKILINK_REGEX, q as assertCurrentRunCitationsResolved, qt as mergeTrackedClaims, r as stringMetadata, rn as withKnowledgeMutation, rt as resolveRunScopedCitation, s as parseKnowledgeWriteBlocks, sn as commitKnowledgeFileMutations, st as isKnowledgePagePath, t as explainKnowledgeTarget, tn as recoverPendingKnowledgeMutation, tt as resolveKnowledgeCitation, u as RUN_LINEAGE_BASENAME, un as prepareKnowledgeFileTransaction, ut as layoutFor, v as CHECKABLE_RUNG_THRESHOLD, vn as listRegularFilesWithinRoot, vt as KnowledgeEventSchema, w as gradeFor, wn as syncDirectory, wt as KnowledgeRelationGraphSchema, x as assertGradeableEvidence, xn as relativeWithinRoot, xt as KnowledgeIndexSchema, y as DEADLINE_EXIT_CODE, yn as readRegularFileNoFollow, yt as KnowledgeGraphEdgeSchema, z as KB_INDEX_PATH, zt as claimEvidenceId } from "./inspect-DrJnvx5Z.js";
1
+ import { $ as formatKnowledgeCitationReference, $t as inspectPendingKnowledgeMutation, A as addSourceText, An as writeJsonDurableWithinRoot, At as ResearchSourceVersionSchema, B as KB_STORE_DIR, Bt as claimId, C as gradeClaims, Cn as renameDurable, Ct as KnowledgePageSchema, D as buildKnowledgeIndex, Dn as writeFileDurable, Dt as ResearchClaimEvidenceSchema, E as verifyGradeableEvidence, En as withSafeDirectory, Et as KnowledgeRelationSchema, F as ClaimLedgerMigrationRequiredError, Ft as assertDeepQuestionIntegrity, G as KnowledgeCitationAuditError, Gt as materializeRegisteredClaimEvidence, H as assertClaimLedgerId, Ht as deepQuestionId, I as FileSystemKbStore, It as assertResearchClaimEvidenceIntegrity, J as assertKnowledgeCitationAudit, Jt as normalizeClaimText, K as KnowledgeCitationResolutionError, Kt as mergeClaimLedgers, L as KB_CLAIM_LEDGER_DIR, Lt as assertResearchClaimLedgerIntegrity, M as snapshotSourceTextInput, Mn as textSourceAdapter, Mt as SourceRecordSchema, N as sourceRegistryPath, Nt as KNOWLEDGE_EVENT_TYPES, O as writeKnowledgeIndex, On as writeFileDurableWithinRoot, Ot as ResearchClaimLedgerSchema, P as writeSourceRegistry, Pt as ClaimLedgerGoalConflictError, Q as auditKnowledgeCitations, Qt as acquireDurableFileLock, R as KB_EVENTS_PATH, Rt as assertTrackedClaimIntegrity, S as claimCheckKey, Sn as removeDurable, St as KnowledgePageInvalidationSchema, T as verdictFor, Tn as withSafeDescendant, Tt as KnowledgeRelationNodeSchema, U as buildKnowledgeGraph, Ut as emptyClaimLedger, V as MemoryKbStore, Vt as claimSourceHost, W as knowledgePageRelations, Wt as linkClaimContradictions, X as assertRunScopedCitationsResolved, Xt as formatFrontmatter, Y as assertKnowledgeCitationsResolved, Yt as researchSourceVersionKey, Z as auditCurrentRunCitations, Zt as parseFrontmatter, _ as lintKnowledgeIndex, _n as isMissingFile, _t as KnowledgeBaseCandidateSchema, a as applyKnowledgeWriteBlocksFile, an as applyKnowledgeFileTransaction, at as SCAFFOLD_PAGE_BASENAMES, b as UncheckableClaimError, bn as readRegularFileWithinRoot, bt as KnowledgeGraphNodeSchema, c as KnowledgeDuplicateIntakeError, cn as finishKnowledgeFileTransaction, ct as isScaffoldPath, d as createFileRunLineageAuthority, dn as rollbackKnowledgeFileTransaction, dt as loadKnowledgePages, en as isKnowledgeMutationHeld, et as parseKnowledgeCitationReference, f as createRunScopedStores, fn as DEFAULT_PAGES_DIRECTORY, ft as writeJson, g as validateKnowledgeIndex, gn as isKernelAnchoredPath, gt as DeepQuestionSchema, h as normalizePageText, hn as canonicalRelativeWithinRoot, ht as normalizeLinkTarget, i as applyKnowledgeWriteBlocks, in as withKnowledgeRead, it as resolveRunScopedCitations, j as loadSourceRegistry, jn as mediaTypeFor, jt as SourceAnchorSchema, k as addSourcePath, kn as writeJsonDurable, kt as ResearchClaimRecordSchema, l as assertKnowledgeWriteIntake, ln as knowledgeFileTransactionPlanHash, lt as knowledgePageFromMarkdown, m as detectNearDuplicatePages, mn as canonicalPathsEqual, mt as extractWikilinks, n as inspectKnowledgeIndex, nn as runInKnowledgeMutationScope, nt as resolveKnowledgeCitations, o as isSafeKnowledgePath, on as assertKnowledgeMutationPath, ot as initKnowledgeBase, p as originatedPages, pn as normalizePagesDirectory, pt as WIKILINK_REGEX, q as assertCurrentRunCitationsResolved, qt as mergeTrackedClaims, r as stringMetadata, rn as withKnowledgeMutation, rt as resolveRunScopedCitation, s as parseKnowledgeWriteBlocks, sn as commitKnowledgeFileMutations, st as isKnowledgePagePath, t as explainKnowledgeTarget, tn as recoverPendingKnowledgeMutation, tt as resolveKnowledgeCitation, u as RUN_LINEAGE_BASENAME, un as prepareKnowledgeFileTransaction, ut as layoutFor, v as CHECKABLE_RUNG_THRESHOLD, vn as listRegularFilesWithinRoot, vt as KnowledgeEventSchema, w as gradeFor, wn as syncDirectory, wt as KnowledgeRelationGraphSchema, x as assertGradeableEvidence, xn as relativeWithinRoot, xt as KnowledgeIndexSchema, y as DEADLINE_EXIT_CODE, yn as readRegularFileNoFollow, yt as KnowledgeGraphEdgeSchema, z as KB_INDEX_PATH, zt as claimEvidenceId } from "./inspect-BVT9JWDL.js";
2
2
  import { i as textSourceId, n as slugify, r as stableId, t as sha256 } from "./ids-Bevz_pXV.js";
3
- import { A as runSerializedKnowledgeOptimization, C as deterministicRng, D as toOffPolicyTrajectory, E as retrievalHoldoutConfigHash, M as assertImmutableRef, O as jsonCandidateCodec, S as applySessionStickyRetrievalHoldout, T as resetRetrievalHoldoutRegistry, _ as createAgentMemoryBranch, a as createNeo4jAgentMemoryAdapter, b as renderMemoryContext, c as runAgentMemoryImprovement, d as compareAgentMemoryLearning, f as runAgentMemoryLearningExperiment, g as buildAgentMemorySequencesFromBenchmarkCases, h as buildAgentMemorySequenceScenarios, i as AgentMemoryWriteInputSchema, j as scenarioContentFingerprint, k as jsonObjectCandidateCodec, l as createGraphitiMemoryAdapter, m as agentMemorySequenceJudge, n as AgentMemoryKindSchema, o as createMem0MemoryAdapter, p as runAgentMemoryExperiment, r as AgentMemoryScopeSchema, s as mem0MemoryAdapterIdentity, t as AgentMemoryHitSchema, u as graphitiMemoryAdapterIdentity, v as forkAgentMemoryBranchSnapshot, w as emitRetrievalHoldoutBypass, x as applyRetrievalHoldout, y as defaultGetMemoryContext } from "./memory-Bn1Xs1hY.js";
3
+ import { A as runSerializedKnowledgeOptimization, C as deterministicRng, D as toOffPolicyTrajectory, E as retrievalHoldoutConfigHash, M as assertImmutableRef, O as jsonCandidateCodec, S as applySessionStickyRetrievalHoldout, T as resetRetrievalHoldoutRegistry, _ as createAgentMemoryBranch, a as createNeo4jAgentMemoryAdapter, b as renderMemoryContext, c as runAgentMemoryImprovement, d as compareAgentMemoryLearning, f as runAgentMemoryLearningExperiment, g as buildAgentMemorySequencesFromBenchmarkCases, h as buildAgentMemorySequenceScenarios, i as AgentMemoryWriteInputSchema, j as scenarioContentFingerprint, k as jsonObjectCandidateCodec, l as createGraphitiMemoryAdapter, m as agentMemorySequenceJudge, n as AgentMemoryKindSchema, o as createMem0MemoryAdapter, p as runAgentMemoryExperiment, r as AgentMemoryScopeSchema, s as mem0MemoryAdapterIdentity, t as AgentMemoryHitSchema, u as graphitiMemoryAdapterIdentity, v as forkAgentMemoryBranchSnapshot, w as emitRetrievalHoldoutBypass, x as applyRetrievalHoldout, y as defaultGetMemoryContext } from "./memory-BYWNir2t.js";
4
4
  import { C as sleepForMemoryRecovery, F as buildIndustryRagBenchmarkSmokeCases, G as buildRetrievalEvalDispatch, H as createNoopMemoryBenchmarkAdapter, I as respondToIndustryMemoryBenchmarkSmokeCase, J as retrievalConfigSurface, K as partitionRetrievalScenarios, L as respondToIndustryRagBenchmarkSmokeCase, M as INDUSTRY_RAG_BENCHMARKS, N as buildFirstPartyMemoryLifecycleBenchmarkCases, P as buildIndustryMemoryBenchmarkSmokeCases, R as isKnowledgeMemoryBenchmarkCase, S as runBoundedMemoryLifecycle, U as memoryHitToSourceRecord, V as createInMemoryBenchmarkAdapter, W as memoryWriteResultToSourceRecord, X as scoreRetrievalArtifact, Y as retrievalRecallJudge, Z as mean, a as buildKnowledgeBenchmarkScenarios, b as memoryRecoveryDelayMs, c as runKnowledgeBenchmarkSuite, d as summarizeKnowledgeBenchmarkCampaign, f as acquireAgentMemoryRunLease, h as DEFAULT_MEMORY_CLEANUP_TIMEOUT_MS, i as runMemoryAdapterBenchmark, j as INDUSTRY_MEMORY_BENCHMARKS, l as scoreKnowledgeBenchmarkArtifact, m as AgentMemoryLifecycleUnsafeError, n as parseKnowledgeBenchmarkJsonl, o as knowledgeBenchmarkJudge, p as AgentMemoryLifecycleTimeoutError, q as retrievalConfigFromSurface, r as parseKnowledgeBenchmarkQrels, s as renderKnowledgeBenchmarkReportMarkdown, t as buildRetrievalBenchmarkCasesFromQrels, u as scoreMemoryBenchmarkArtifact, x as resolveMemoryCleanupTimeoutMs, y as createMemoryExecutionPool } from "./benchmarks-B3kU-Yxz.js";
5
5
  import { a as buildKnowledgeLexicalIndex, c as tokenizeText, i as searchKnowledgePages, n as reciprocalRankFusion, o as scoreBm25, r as searchKnowledge, s as tokenizeQuery, t as KNOWLEDGE_SEARCH_RETRIEVER_ID } from "./search-Cbyj2shJ.js";
6
6
  import { IRS_DIMENSION_HINTS, MAX_RESPONSE_BYTES, MIN_REQUEST_GAP_MS, POLITE_USER_AGENT, __resetHttpThrottle, createCornellLiiSource, createIrsPublicationsSource, createStateSosSource, extractLinks, firstMatch, htmlToText, innerHtmlById, looksLikeBlockPage, politeFetch } from "./sources/index.js";
7
7
  import { createHash } from "node:crypto";
8
+ import { TCloudClient, TCloudError } from "@tangle-network/tcloud";
8
9
  import { cp, lstat, mkdir, mkdtemp, readFile, rm, stat } from "node:fs/promises";
9
10
  import { dirname, join } from "node:path";
10
11
  import { agentImprovementActivationResultSchema, agentImprovementActivationSchema, canonicalCandidateBytes, canonicalCandidateDigest, canonicalCandidateJson, omitTopLevelDigest, sha256Bytes, sha256DigestSchema } from "@tangle-network/agent-interface";
@@ -39,10 +40,9 @@ import { pathToFileURL } from "node:url";
39
40
  * worker ADDS; the driver GATES. Together they build a cleaner knowledge base
40
41
  * than a single agent at the same compute budget.
41
42
  *
42
- * Dependency-free on purpose: it talks to the router over `fetch` directly with
43
- * the published OpenAI-compatible chat shape and the `/v1/search` shape, so it
44
- * works whether or not the `tcloud` CLI is installed. Point it at any router by
45
- * passing `baseUrl`; supply the key via `apiKey` or `TANGLE_API_KEY`.
43
+ * Model and search requests use the published TCloud SDK. Knowledge owns the
44
+ * research policy, not HTTP, bearer headers, retries or provider rate cards.
45
+ * Point it at any router with `baseUrl`; supply `apiKey` or `TANGLE_API_KEY`.
46
46
  */
47
47
  /** Default router model. Plain id (no namespace) — see CLAUDE/creds notes. */
48
48
  const DEFAULT_MODEL = "glm-5.2";
@@ -54,35 +54,7 @@ const DEFAULT_BASE_URL = "https://router.tangle.tools/v1";
54
54
  * reasoning model never silently yields nothing. (Verified creds note.)
55
55
  */
56
56
  const MIN_MAX_TOKENS = 1200;
57
- /** Transient upstream statuses worth a retry (capacity / rate-limit / gateway). */
58
- const transientStatuses = /* @__PURE__ */ new Set([
59
- 429,
60
- 502,
61
- 503,
62
- 504
63
- ]);
64
- /**
65
- * POST with bounded exponential backoff on transient upstream statuses. Returns
66
- * the first `res.ok` response, or the LAST response (so the caller throws the
67
- * real status). A non-transient failure returns immediately — only 502/503/504/
68
- * 429 are retried. Aborts propagate at once.
69
- */
70
- async function fetchWithRetry(url, init, opts) {
71
- let lastRes;
72
- for (let attempt = 0; attempt <= opts.maxRetries; attempt += 1) {
73
- if (opts.signal?.aborted) throw new RouterError(0, "aborted");
74
- const res = await fetch(url, init);
75
- if (res.ok || !transientStatuses.has(res.status)) return res;
76
- lastRes = res;
77
- if (attempt === opts.maxRetries) break;
78
- await res.text().catch(() => "");
79
- const jitter = opts.retryBaseMs * 2 ** attempt * (.75 + Math.random() * .5);
80
- await new Promise((resolve) => setTimeout(resolve, jitter));
81
- }
82
- if (lastRes) return lastRes;
83
- throw new RouterError(0, "fetchWithRetry produced no response");
84
- }
85
- /** A small error so a failed router call fails loud rather than returning junk. */
57
+ /** Compatibility error facade. HTTP execution and classification belong to TCloud. */
86
58
  var RouterError = class extends Error {
87
59
  status;
88
60
  constructor(status, message) {
@@ -91,26 +63,26 @@ var RouterError = class extends Error {
91
63
  this.name = "RouterError";
92
64
  }
93
65
  };
94
- /**
95
- * Build a dependency-free Tangle router client over `fetch`. This is the same
96
- * wire surface the `tcloud` SDK + `tcloud mcp` use (`/v1/search` for web search,
97
- * `/v1/chat/completions` for chat) so it needs no CLI installed.
98
- */
66
+ /** Adapt Knowledge's public contract to the published SDK, without a second transport. */
99
67
  function createTangleRouterClient(options = {}) {
100
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
101
68
  const apiKey = options.apiKey ?? process.env.TANGLE_API_KEY;
102
69
  if (!apiKey) throw new RouterError(401, "no TANGLE_API_KEY (pass apiKey or set the env var)");
103
- const model = options.model ?? DEFAULT_MODEL;
104
- const maxRetries = Math.max(0, options.maxRetries ?? 4);
105
- const retryBaseMs = Math.max(1, options.retryBaseMs ?? 1500);
106
- const headers = {
107
- "Content-Type": "application/json",
108
- Authorization: `Bearer ${apiKey}`
109
- };
110
- const price = {
111
- prompt: .95 / 1e6,
112
- completion: 3 / 1e6
113
- };
70
+ const client = new TCloudClient({
71
+ baseURL: (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
72
+ apiKey,
73
+ model: options.model ?? DEFAULT_MODEL,
74
+ timeout: 0,
75
+ retry: {
76
+ maxRetries: Math.max(0, options.maxRetries ?? 4),
77
+ initialBackoffMs: Math.max(1, options.retryBaseMs ?? 1500),
78
+ retryableStatuses: [
79
+ 429,
80
+ 502,
81
+ 503,
82
+ 504
83
+ ]
84
+ }
85
+ });
114
86
  const acc = {
115
87
  chatCalls: 0,
116
88
  searchCalls: 0,
@@ -119,70 +91,79 @@ function createTangleRouterClient(options = {}) {
119
91
  usd: 0,
120
92
  wallMs: 0
121
93
  };
94
+ const recordCost = (cost) => {
95
+ acc.usd += typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : NaN;
96
+ };
97
+ const translate = (error) => {
98
+ options.signal?.throwIfAborted();
99
+ if (error instanceof TCloudError) throw new RouterError(error.status, error.message);
100
+ throw error;
101
+ };
122
102
  return {
123
103
  async search(query, opts) {
124
- const t0 = Date.now();
125
- const res = await fetchWithRetry(`${baseUrl}/search`, {
126
- method: "POST",
127
- headers,
128
- signal: options.signal,
129
- body: JSON.stringify({
104
+ options.signal?.throwIfAborted();
105
+ const started = Date.now();
106
+ try {
107
+ const response = await client.search({
130
108
  query,
131
109
  ...options.searchProvider ? { provider: options.searchProvider } : {},
132
- ...opts?.maxResults != null ? { maxResults: opts.maxResults } : {}
133
- })
134
- }, {
135
- maxRetries,
136
- retryBaseMs,
137
- signal: options.signal
138
- });
139
- acc.searchCalls += 1;
140
- acc.wallMs += Date.now() - t0;
141
- if (!res.ok) throw new RouterError(res.status, await res.text().catch(() => res.statusText));
142
- return ((await res.json()).data ?? []).filter((hit) => typeof hit?.url === "string" && hit.url.length > 0).map((hit) => ({
143
- title: hit.title ?? hit.url,
144
- url: hit.url,
145
- snippet: hit.snippet
146
- }));
110
+ ...opts?.maxResults != null ? { maxResults: opts.maxResults } : {},
111
+ signal: options.signal
112
+ });
113
+ recordCost(response.usage?.billed_cost);
114
+ return (response.data ?? []).filter((hit) => typeof hit?.url === "string" && hit.url.length > 0).map((hit) => ({
115
+ title: hit.title ?? hit.url,
116
+ url: hit.url,
117
+ snippet: hit.snippet
118
+ }));
119
+ } catch (error) {
120
+ return translate(error);
121
+ } finally {
122
+ acc.searchCalls += 1;
123
+ acc.wallMs += Date.now() - started;
124
+ }
147
125
  },
148
126
  async chat(messages, maxTokens) {
149
- const max_tokens = Math.max(MIN_MAX_TOKENS, maxTokens ?? MIN_MAX_TOKENS);
150
- const t0 = Date.now();
151
- const res = await fetchWithRetry(`${baseUrl}/chat/completions`, {
152
- method: "POST",
153
- headers,
154
- signal: options.signal,
155
- body: JSON.stringify({
156
- model,
127
+ options.signal?.throwIfAborted();
128
+ const started = Date.now();
129
+ try {
130
+ const response = await client.chat({
157
131
  messages,
158
- max_tokens,
132
+ maxTokens: Math.max(MIN_MAX_TOKENS, maxTokens ?? MIN_MAX_TOKENS),
159
133
  temperature: .2,
160
- stream: false
161
- })
162
- }, {
163
- maxRetries,
164
- retryBaseMs,
165
- signal: options.signal
166
- });
167
- if (!res.ok) throw new RouterError(res.status, await res.text().catch(() => res.statusText));
168
- const body = await res.json();
169
- const promptTokens = body.usage?.prompt_tokens ?? 0;
170
- const completionTokens = body.usage?.completion_tokens ?? 0;
171
- acc.chatCalls += 1;
172
- acc.promptTokens += promptTokens;
173
- acc.completionTokens += completionTokens;
174
- acc.usd += promptTokens * price.prompt + completionTokens * price.completion;
175
- acc.wallMs += Date.now() - t0;
176
- return body.choices?.[0]?.message?.content ?? "";
134
+ signal: options.signal
135
+ });
136
+ acc.chatCalls += 1;
137
+ acc.promptTokens += response.usage?.prompt_tokens ?? 0;
138
+ acc.completionTokens += response.usage?.completion_tokens ?? 0;
139
+ recordCost(response.tangle?.costSource === "receipt" ? response.tangle.costUsd : void 0);
140
+ return response.choices?.[0]?.message?.content ?? "";
141
+ } catch (error) {
142
+ return translate(error);
143
+ } finally {
144
+ acc.wallMs += Date.now() - started;
145
+ }
177
146
  },
178
147
  usage() {
179
148
  return { ...acc };
180
149
  }
181
150
  };
182
151
  }
183
- /** Resolve the router client lazily so a worker with an injected client never reads env. */
184
- function resolveRouter(opts) {
185
- return opts.router ?? createTangleRouterClient(opts.router_options);
152
+ /** Bind run cancellation to the default router without changing an injected client. */
153
+ function resolveRouter(opts, signal) {
154
+ if (opts.router) return opts.router;
155
+ const requestSignal = combineRouterSignals(signal, opts.router_options?.signal);
156
+ return createTangleRouterClient({
157
+ ...opts.router_options,
158
+ signal: requestSignal
159
+ });
160
+ }
161
+ function combineRouterSignals(runSignal, configuredSignal) {
162
+ return runSignal && configuredSignal ? AbortSignal.any([runSignal, configuredSignal]) : runSignal ?? configuredSignal;
163
+ }
164
+ function throwIfRouterAborted(runSignal, opts) {
165
+ runSignal?.throwIfAborted();
166
+ if (!opts.router) opts.router_options?.signal?.throwIfAborted();
186
167
  }
187
168
  /**
188
169
  * The real web-research worker. Conforms to the loop's `ResearchWorker`
@@ -196,33 +177,41 @@ function createWebResearchWorker(options = {}) {
196
177
  const minTextChars = Math.max(1, options.minTextChars ?? 200);
197
178
  const maxTextChars = Math.max(minTextChars, options.maxTextChars ?? 4e3);
198
179
  return async (ctx) => {
199
- const router = resolveRouter(options);
180
+ const assertActive = () => throwIfRouterAborted(ctx.signal, options);
181
+ assertActive();
182
+ const router = resolveRouter(options, ctx.signal);
183
+ const fetchSignal = options.router ? ctx.signal : combineRouterSignals(ctx.signal, options.router_options?.signal);
200
184
  const targetGaps = ctx.gaps.filter((gap) => gap.blocking);
201
185
  const gaps = targetGaps.length > 0 ? targetGaps : ctx.gaps;
202
186
  if (gaps.length === 0) return {
203
187
  sources: [],
204
188
  notes: "no open gaps to research"
205
189
  };
206
- const queries = await formSearchQueries(router, ctx, gaps, queriesPerGap);
190
+ const queries = await formSearchQueries(router, ctx, gaps, queriesPerGap, assertActive);
191
+ assertActive();
207
192
  const proposals = [];
208
193
  const seenUris = /* @__PURE__ */ new Set();
209
194
  for (const query of queries) {
210
195
  if (proposals.length >= maxSourcesPerRound) break;
211
- if (ctx.signal?.aborted) break;
196
+ assertActive();
212
197
  let hits;
213
198
  try {
214
199
  hits = await router.search(query, { maxResults: resultsPerQuery });
215
200
  } catch (error) {
216
- if (error.name === "AbortError") break;
201
+ assertActive();
202
+ if (error?.name === "AbortError") throw error;
217
203
  continue;
218
204
  }
205
+ assertActive();
219
206
  for (const hit of hits) {
207
+ assertActive();
220
208
  if (proposals.length >= maxSourcesPerRound) break;
221
209
  if (seenUris.has(hit.url)) continue;
222
210
  const fetched = await politeFetch(hit.url, {
223
- signal: ctx.signal,
211
+ signal: fetchSignal,
224
212
  cacheDir: options.cacheDir
225
213
  });
214
+ assertActive();
226
215
  if (!fetched.verifiable) continue;
227
216
  const text = htmlToText(fetched.body).slice(0, maxTextChars);
228
217
  if (text.length < minTextChars) continue;
@@ -241,6 +230,7 @@ function createWebResearchWorker(options = {}) {
241
230
  });
242
231
  }
243
232
  }
233
+ assertActive();
244
234
  return {
245
235
  sources: proposals,
246
236
  buildPages: buildCitingPages(proposals),
@@ -253,7 +243,7 @@ function createWebResearchWorker(options = {}) {
253
243
  * to the gap's own readiness query if the model returns nothing parseable, so a
254
244
  * model hiccup degrades to a sane search rather than an empty round.
255
245
  */
256
- async function formSearchQueries(router, ctx, gaps, queriesPerGap) {
246
+ async function formSearchQueries(router, ctx, gaps, queriesPerGap, assertActive) {
257
247
  const gapLines = gaps.map((gap, i) => `${i + 1}. ${gap.description} (readiness query: "${gap.query}")`).join("\n");
258
248
  const want = gaps.length * queriesPerGap;
259
249
  const system = "You are a research librarian. Turn knowledge gaps into precise web search queries that will surface authoritative primary sources (papers, docs, standards, official pages). Return ONLY a JSON array of query strings, no prose.";
@@ -272,7 +262,10 @@ async function formSearchQueries(router, ctx, gaps, queriesPerGap) {
272
262
  role: "user",
273
263
  content: user
274
264
  }], MIN_MAX_TOKENS);
275
- } catch {
265
+ assertActive();
266
+ } catch (error) {
267
+ assertActive();
268
+ if (error?.name === "AbortError") throw error;
276
269
  raw = "";
277
270
  }
278
271
  const fromLlm = parseQueryList(raw).slice(0, want);
@@ -354,7 +347,9 @@ function escapeYaml(value) {
354
347
  function createVerifyingResearchDriver(options = {}) {
355
348
  const acceptOnParseFailure = options.acceptOnParseFailure ?? false;
356
349
  return { async verifySource(source, ctx) {
357
- const router = resolveRouter(options);
350
+ const assertActive = () => throwIfRouterAborted(ctx.signal, options);
351
+ assertActive();
352
+ const router = resolveRouter(options, ctx.signal);
358
353
  const gapLines = ctx.gaps.map((gap) => `- ${gap.description} (query: "${gap.query}")`).join("\n");
359
354
  const acceptedTitles = ctx.acceptedThisRound.map((accepted) => `- ${accepted.title ?? accepted.uri}`).join("\n");
360
355
  const excerpt = source.text.slice(0, 1500);
@@ -375,8 +370,10 @@ function createVerifyingResearchDriver(options = {}) {
375
370
  role: "user",
376
371
  content: user
377
372
  }], MIN_MAX_TOKENS);
373
+ assertActive();
378
374
  } catch (error) {
379
- if (error.name === "AbortError") throw error;
375
+ assertActive();
376
+ if (error?.name === "AbortError") throw error;
380
377
  return acceptOnParseFailure ? { accept: true } : {
381
378
  accept: false,
382
379
  reason: `verifier unavailable: ${error.message}`
@@ -494,7 +491,8 @@ const trackingParams = /* @__PURE__ */ new Set([
494
491
  * mirror/syndication of an already-accepted source even when the URL differs.
495
492
  */
496
493
  function contentKey(text) {
497
- return sha256(text.toLowerCase().replace(/[^\p{L}\p{N}\s]+/gu, " ").replace(/\s+/g, " ").trim());
494
+ const normalized = text.toLowerCase().replace(/[^\p{L}\p{N}\s]+/gu, " ").replace(/\s+/g, " ").trim();
495
+ return sha256(normalized);
498
496
  }
499
497
  /** Default authoritative host suffixes — papers, official docs, standards. */
500
498
  const defaultAuthoritativeHosts = [
@@ -934,7 +932,8 @@ function knowledgeImprovementRunDir(root, runId) {
934
932
  return runDir;
935
933
  }
936
934
  async function withKnowledgeImprovementRun(root, runId, create, use) {
937
- const relativePath = relativeWithinRoot(root, knowledgeImprovementRunDir(root, runId));
935
+ const runDir = knowledgeImprovementRunDir(root, runId);
936
+ const relativePath = relativeWithinRoot(root, runDir);
938
937
  if (!relativePath) throw new Error("knowledge improvement run directory escaped its root");
939
938
  return withSafeDirectory(root, relativePath, create, async (openedRunDir) => {
940
939
  const result = await use(openedRunDir);
@@ -1005,8 +1004,9 @@ function parseKnowledgeImprovementEvent(value) {
1005
1004
  return event;
1006
1005
  }
1007
1006
  async function acquireRunLease(runDir, options) {
1007
+ const path = join(runDir, "run.lock.durable");
1008
1008
  const acquired = await acquireDurableFileLock(runDir, {
1009
- lockfilePath: join(runDir, "run.lock.durable"),
1009
+ lockfilePath: path,
1010
1010
  staleMs: options.ttlMs
1011
1011
  });
1012
1012
  return {
@@ -2461,100 +2461,114 @@ async function runRagKnowledgeImprovementPhases(options, previous) {
2461
2461
  let knowledgeUpdate = previous?.knowledgeUpdate;
2462
2462
  let answerQuality = previous?.answerQuality;
2463
2463
  let promotion = previous?.promotion;
2464
- if (phaseEnabled(options, "gap-diagnosis")) if (options.diagnose) findings = [...await runPhase(phases, now, "gap-diagnosis", async () => {
2465
- assertNotAborted(options.signal);
2466
- return options.diagnose({
2467
- goal: options.goal,
2468
- phases,
2469
- optimization: selectRagOptimization(optimization),
2470
- signal: options.signal,
2471
- retrieval: selectRetrievalOptimization(retrieval)
2472
- });
2473
- }, (diagnosed) => `${diagnosed.length} finding(s)`)];
2474
- else skipPhase(phases, now, "gap-diagnosis", "no diagnosis hook provided");
2475
- if (phaseEnabled(options, "knowledge-acquisition")) if (options.acquireKnowledge) acquisition = await runPhase(phases, now, "knowledge-acquisition", async () => {
2476
- assertNotAborted(options.signal);
2477
- return options.acquireKnowledge({
2478
- goal: options.goal,
2479
- phases,
2480
- optimization: selectRagOptimization(optimization),
2481
- signal: options.signal,
2482
- retrieval: selectRetrievalOptimization(retrieval),
2483
- findings
2484
- });
2485
- }, summarizeAcquisitionDecision);
2486
- else skipPhase(phases, now, "knowledge-acquisition", "no acquisition hook provided");
2487
- if (phaseEnabled(options, "knowledge-update")) if (options.updateKnowledge) knowledgeUpdate = await runPhase(phases, now, "knowledge-update", async () => {
2488
- assertNotAborted(options.signal);
2489
- return options.updateKnowledge({
2490
- goal: options.goal,
2491
- phases,
2492
- optimization: selectRagOptimization(optimization),
2493
- signal: options.signal,
2494
- retrieval: selectRetrievalOptimization(retrieval),
2495
- findings,
2496
- acquisition
2497
- });
2498
- }, (result) => result.summary);
2499
- else if (options.knowledgeResearch) knowledgeUpdate = await runPhase(phases, now, "knowledge-update", async () => {
2500
- assertNotAborted(options.signal);
2501
- return runKnowledgeResearchUpdate(options, acquisition);
2502
- }, (result) => result.summary);
2503
- else skipPhase(phases, now, "knowledge-update", "no update hook or research loop provided");
2504
- if (phaseEnabled(options, "rag-optimization") && (options.optimization || options.enabledPhases?.includes("rag-optimization") || options.requiredPhases?.includes("rag-optimization"))) if (options.optimization) optimization = await runPhase(phases, now, "rag-optimization", async () => {
2505
- assertNotAborted(options.signal);
2506
- return runRagOptimization(options.optimization);
2507
- }, summarizeRagOptimization);
2508
- else skipPhase(phases, now, "rag-optimization", "no full RAG optimization options provided");
2509
- if (phaseEnabled(options, "retrieval-tuning")) if (options.retrieval) retrieval = await runPhase(phases, now, "retrieval-tuning", async () => {
2510
- assertNotAborted(options.signal);
2511
- return runRetrievalImprovementLoop(options.retrieval);
2512
- }, summarizeRetrievalResult);
2513
- else skipPhase(phases, now, "retrieval-tuning", "no retrieval options provided");
2514
- if (phaseEnabled(options, "answer-quality")) if (options.evaluateAnswers) {
2515
- answerQuality = await runPhase(phases, now, "answer-quality", async () => {
2464
+ if (phaseEnabled(options, "gap-diagnosis")) {
2465
+ if (options.diagnose) findings = [...await runPhase(phases, now, "gap-diagnosis", async () => {
2516
2466
  assertNotAborted(options.signal);
2517
- const result = await options.evaluateAnswers({
2467
+ return options.diagnose({
2518
2468
  goal: options.goal,
2519
2469
  phases,
2520
2470
  optimization: selectRagOptimization(optimization),
2521
2471
  signal: options.signal,
2472
+ retrieval: selectRetrievalOptimization(retrieval)
2473
+ });
2474
+ }, (diagnosed) => `${diagnosed.length} finding(s)`)];
2475
+ else skipPhase(phases, now, "gap-diagnosis", "no diagnosis hook provided");
2476
+ }
2477
+ if (phaseEnabled(options, "knowledge-acquisition")) {
2478
+ if (options.acquireKnowledge) acquisition = await runPhase(phases, now, "knowledge-acquisition", async () => {
2479
+ assertNotAborted(options.signal);
2480
+ return options.acquireKnowledge({
2481
+ goal: options.goal,
2482
+ phases,
2483
+ optimization: selectRagOptimization(optimization),
2484
+ signal: options.signal,
2485
+ retrieval: selectRetrievalOptimization(retrieval),
2486
+ findings
2487
+ });
2488
+ }, summarizeAcquisitionDecision);
2489
+ else skipPhase(phases, now, "knowledge-acquisition", "no acquisition hook provided");
2490
+ }
2491
+ if (phaseEnabled(options, "knowledge-update")) {
2492
+ if (options.updateKnowledge) knowledgeUpdate = await runPhase(phases, now, "knowledge-update", async () => {
2493
+ assertNotAborted(options.signal);
2494
+ return options.updateKnowledge({
2495
+ goal: options.goal,
2496
+ phases,
2497
+ optimization: selectRagOptimization(optimization),
2498
+ signal: options.signal,
2499
+ retrieval: selectRetrievalOptimization(retrieval),
2500
+ findings,
2501
+ acquisition
2502
+ });
2503
+ }, (result) => result.summary);
2504
+ else if (options.knowledgeResearch) knowledgeUpdate = await runPhase(phases, now, "knowledge-update", async () => {
2505
+ assertNotAborted(options.signal);
2506
+ return runKnowledgeResearchUpdate(options, acquisition);
2507
+ }, (result) => result.summary);
2508
+ else skipPhase(phases, now, "knowledge-update", "no update hook or research loop provided");
2509
+ }
2510
+ if (phaseEnabled(options, "rag-optimization") && (options.optimization || options.enabledPhases?.includes("rag-optimization") || options.requiredPhases?.includes("rag-optimization"))) {
2511
+ if (options.optimization) optimization = await runPhase(phases, now, "rag-optimization", async () => {
2512
+ assertNotAborted(options.signal);
2513
+ return runRagOptimization(options.optimization);
2514
+ }, summarizeRagOptimization);
2515
+ else skipPhase(phases, now, "rag-optimization", "no full RAG optimization options provided");
2516
+ }
2517
+ if (phaseEnabled(options, "retrieval-tuning")) {
2518
+ if (options.retrieval) retrieval = await runPhase(phases, now, "retrieval-tuning", async () => {
2519
+ assertNotAborted(options.signal);
2520
+ return runRetrievalImprovementLoop(options.retrieval);
2521
+ }, summarizeRetrievalResult);
2522
+ else skipPhase(phases, now, "retrieval-tuning", "no retrieval options provided");
2523
+ }
2524
+ if (phaseEnabled(options, "answer-quality")) {
2525
+ if (options.evaluateAnswers) {
2526
+ answerQuality = await runPhase(phases, now, "answer-quality", async () => {
2527
+ assertNotAborted(options.signal);
2528
+ const result = await options.evaluateAnswers({
2529
+ goal: options.goal,
2530
+ phases,
2531
+ optimization: selectRagOptimization(optimization),
2532
+ signal: options.signal,
2533
+ retrieval: selectRetrievalOptimization(retrieval),
2534
+ findings,
2535
+ acquisition,
2536
+ knowledgeUpdate
2537
+ });
2538
+ assertRagAnswerEvidence(result);
2539
+ return result;
2540
+ }, summarizeAnswerQuality);
2541
+ findings = [...findings, ...answerQuality.findings ?? []];
2542
+ } else skipPhase(phases, now, "answer-quality", "no answer-quality hook provided");
2543
+ }
2544
+ if (phaseEnabled(options, "promotion")) {
2545
+ if (options.decidePromotion) promotion = await runPhase(phases, now, "promotion", async () => {
2546
+ assertNotAborted(options.signal);
2547
+ const evidenceRejection = rejectUnsafePromotionEvidence({
2548
+ optimization: optimization?.comparison,
2549
+ optimizationCostCeiling: options.optimization?.costCeiling,
2550
+ retrieval: retrieval?.comparison,
2551
+ retrievalCostCeiling: options.retrieval?.costCeiling,
2552
+ answerQuality,
2553
+ answerQualityCostCeiling: options.answerQualityCostCeiling
2554
+ });
2555
+ if (evidenceRejection) return evidenceRejection;
2556
+ return options.decidePromotion({
2557
+ goal: options.goal,
2558
+ phases,
2559
+ optimization: selectRagOptimization(optimization),
2560
+ optimizationComparison: optimization?.comparison,
2561
+ signal: options.signal,
2522
2562
  retrieval: selectRetrievalOptimization(retrieval),
2563
+ retrievalComparison: retrieval?.comparison,
2523
2564
  findings,
2524
2565
  acquisition,
2525
- knowledgeUpdate
2566
+ knowledgeUpdate,
2567
+ answerQuality
2526
2568
  });
2527
- assertRagAnswerEvidence(result);
2528
- return result;
2529
- }, summarizeAnswerQuality);
2530
- findings = [...findings, ...answerQuality.findings ?? []];
2531
- } else skipPhase(phases, now, "answer-quality", "no answer-quality hook provided");
2532
- if (phaseEnabled(options, "promotion")) if (options.decidePromotion) promotion = await runPhase(phases, now, "promotion", async () => {
2533
- assertNotAborted(options.signal);
2534
- const evidenceRejection = rejectUnsafePromotionEvidence({
2535
- optimization: optimization?.comparison,
2536
- optimizationCostCeiling: options.optimization?.costCeiling,
2537
- retrieval: retrieval?.comparison,
2538
- retrievalCostCeiling: options.retrieval?.costCeiling,
2539
- answerQuality,
2540
- answerQualityCostCeiling: options.answerQualityCostCeiling
2541
- });
2542
- if (evidenceRejection) return evidenceRejection;
2543
- return options.decidePromotion({
2544
- goal: options.goal,
2545
- phases,
2546
- optimization: selectRagOptimization(optimization),
2547
- optimizationComparison: optimization?.comparison,
2548
- signal: options.signal,
2549
- retrieval: selectRetrievalOptimization(retrieval),
2550
- retrievalComparison: retrieval?.comparison,
2551
- findings,
2552
- acquisition,
2553
- knowledgeUpdate,
2554
- answerQuality
2555
- });
2556
- }, (result) => `${result.promoted ? "promoted" : "held"}: ${result.reason}`);
2557
- else skipPhase(phases, now, "promotion", "no promotion decision hook provided");
2569
+ }, (result) => `${result.promoted ? "promoted" : "held"}: ${result.reason}`);
2570
+ else skipPhase(phases, now, "promotion", "no promotion decision hook provided");
2571
+ }
2558
2572
  return {
2559
2573
  goal: options.goal,
2560
2574
  phases,
@@ -2801,9 +2815,11 @@ async function withMeasuredCandidateSnapshot(liveRoot, runDir, state, candidateR
2801
2815
  await assertStateIdentity(liveRoot, candidateRef, state);
2802
2816
  const candidate = state.candidates.find((entry) => entry.candidateId === candidateRef.candidateId);
2803
2817
  if (!candidate) throw new Error(`knowledge candidate '${candidateRef.candidateId}' does not exist`);
2804
- if (canonicalJson(candidateRefFor(candidateRef.runId, state, candidate)) !== canonicalJson(candidateRef)) throw new Error("knowledge candidate approval does not match the measured candidate");
2818
+ const expectedRef = candidateRefFor(candidateRef.runId, state, candidate);
2819
+ if (canonicalJson(expectedRef) !== canonicalJson(candidateRef)) throw new Error("knowledge candidate approval does not match the measured candidate");
2805
2820
  const evidence = await assertCandidateEvidence(runDir, candidateRef, state.implementationRef);
2806
- return withSafeDirectory(runDir, join("candidates", candidate.candidateId, "snapshots", candidateRef.candidateHash), false, async (root) => {
2821
+ const relativePath = join("candidates", candidate.candidateId, "snapshots", candidateRef.candidateHash);
2822
+ return withSafeDirectory(runDir, relativePath, false, async (root) => {
2807
2823
  if (await hashKnowledgeBase(root, state.stateScope) !== candidateRef.candidateHash) throw new Error("knowledge candidate snapshot changed after approval");
2808
2824
  const result = await use({
2809
2825
  root,
@@ -2906,7 +2922,8 @@ async function withBaselineSnapshot(runDir, expectedHash, use, scope) {
2906
2922
  });
2907
2923
  }
2908
2924
  async function withFrozenCandidateWorkspace(runDir, candidate, candidateRoot, use, scope) {
2909
- return withSafeDirectory(runDir, join("candidates", safePathSegmentSchema.parse(candidate.candidateId), "snapshots"), true, async (snapshotsDir) => {
2925
+ const snapshotsPath = join("candidates", safePathSegmentSchema.parse(candidate.candidateId), "snapshots");
2926
+ return withSafeDirectory(runDir, snapshotsPath, true, async (snapshotsDir) => {
2910
2927
  const preparation = await mkdtemp(join(snapshotsDir, "prepare-"));
2911
2928
  let activated = false;
2912
2929
  try {
@@ -3325,7 +3342,8 @@ async function knowledgePlanMutations(targetRoot, plan) {
3325
3342
  }));
3326
3343
  }
3327
3344
  function assertCandidateTransitionPlan(plan, candidate, target, scope) {
3328
- const actualPlanHash = knowledgeFileTransactionPlanHash(target === "candidate" ? plan : reverseKnowledgeFilePlan(plan), normalizeKnowledgeStateScope(scope).pagesDirectory, scope?.researchState);
3345
+ const approvedDirection = target === "candidate" ? plan : reverseKnowledgeFilePlan(plan);
3346
+ const actualPlanHash = knowledgeFileTransactionPlanHash(approvedDirection, normalizeKnowledgeStateScope(scope).pagesDirectory, scope?.researchState);
3329
3347
  if (actualPlanHash !== candidate.promotionPlanHash) throw new Error(`knowledge candidate plan changed after approval: expected ${candidate.promotionPlanHash}, got ${actualPlanHash}`);
3330
3348
  }
3331
3349
  function assertCandidateTransitionTransaction(transaction, candidate, target, scope) {
@@ -3929,7 +3947,7 @@ async function improveSelectedKnowledgeCandidate(options) {
3929
3947
  if (sourcePlanHash !== sourceCandidate.promotionPlanHash) throw new Error("source knowledge candidate plan no longer matches its measured identity");
3930
3948
  const changedSourcePlan = sourcePlan.filter(planEntryChanged).filter((entry) => notDerivedPath(entry, scope));
3931
3949
  const selectedPaths = normalizeSelectedPaths(options.selectedPaths, changedSourcePlan, scope);
3932
- const selectionDigest = contentHash(immutableJson({
3950
+ const selectionMaterial = immutableJson({
3933
3951
  kind: "measured-knowledge-change-selection",
3934
3952
  version: 1,
3935
3953
  sourceCandidate,
@@ -3937,7 +3955,8 @@ async function improveSelectedKnowledgeCandidate(options) {
3937
3955
  selectedPaths,
3938
3956
  ...options.rationale?.trim() ? { rationale: options.rationale.trim() } : {},
3939
3957
  ...metadata ? { metadata } : {}
3940
- }));
3958
+ });
3959
+ const selectionDigest = contentHash(selectionMaterial);
3941
3960
  const derivedImplementationRef = immutableRefSchema.parse(`sha256:${contentHash({
3942
3961
  engine: "agent-knowledge/measured-selection-v1",
3943
3962
  implementationRef: requestedImplementationRef,
@@ -4112,7 +4131,8 @@ async function persistSelectionReceipt(root, receipt) {
4112
4131
  const runDir = knowledgeImprovementRunDir(root, receipt.runId);
4113
4132
  const relativePath = join("candidates", safePathSegmentSchema.parse(receipt.derivedCandidateId), "selection.json").replace(/\\/g, "/");
4114
4133
  try {
4115
- if (canonicalJson(measuredSelectionReceiptSchema.parse(JSON.parse((await readRegularFileWithinRoot(runDir, relativePath)).bytes.toString("utf8")))) !== canonicalJson(receipt)) throw new Error("measured knowledge selection receipt conflicts with durable content");
4134
+ const existing = measuredSelectionReceiptSchema.parse(JSON.parse((await readRegularFileWithinRoot(runDir, relativePath)).bytes.toString("utf8")));
4135
+ if (canonicalJson(existing) !== canonicalJson(receipt)) throw new Error("measured knowledge selection receipt conflicts with durable content");
4116
4136
  return;
4117
4137
  } catch (error) {
4118
4138
  if (!isMissingFile(error)) throw error;
@@ -5527,17 +5547,22 @@ async function materialFactsSurfaced(kb, checklist) {
5527
5547
  * and `searchKnowledge` — and reinvents none of them.
5528
5548
  */
5529
5549
  async function runVerifiedResearchLoop(options) {
5550
+ const assertActive = () => options.signal?.throwIfAborted();
5551
+ assertActive();
5530
5552
  const maxRounds = Math.max(1, options.maxRounds ?? 3);
5531
5553
  await initKnowledgeBase(options.root);
5554
+ assertActive();
5532
5555
  const store = new FileSystemKbStore({ root: options.root });
5533
5556
  const steps = [];
5534
5557
  let index = await buildKnowledgeIndex(options.root);
5558
+ assertActive();
5535
5559
  await confirmRegisteredSources(options.driver, index.sources);
5560
+ assertActive();
5536
5561
  let readiness = readinessFor(options, index);
5537
5562
  let ready = isReady(readiness?.report) && (options.driver.isComplete?.() ?? true);
5538
5563
  let steer;
5539
5564
  for (let round = 1; round <= maxRounds && !ready; round++) {
5540
- if (options.signal?.aborted) throw new Error("Verified research loop aborted");
5565
+ assertActive();
5541
5566
  const gaps = gapsFromReadiness(readiness);
5542
5567
  const workerContribution = await options.worker({
5543
5568
  root: options.root,
@@ -5549,6 +5574,7 @@ async function runVerifiedResearchLoop(options) {
5549
5574
  readiness: requireReadiness(readiness, options),
5550
5575
  signal: options.signal
5551
5576
  });
5577
+ assertActive();
5552
5578
  const accepted = [];
5553
5579
  const rejectedWorkerSources = [];
5554
5580
  const existingUris = new Set(index.sources.flatMap((source) => typeof source.metadata?.originalUri === "string" ? [source.metadata.originalUri] : []));
@@ -5570,6 +5596,7 @@ async function runVerifiedResearchLoop(options) {
5570
5596
  acceptedThisRound: accepted,
5571
5597
  signal: options.signal
5572
5598
  });
5599
+ assertActive();
5573
5600
  if (verdict.accept) accepted.push(source);
5574
5601
  else rejectedWorkerSources.push({
5575
5602
  source,
@@ -5577,10 +5604,14 @@ async function runVerifiedResearchLoop(options) {
5577
5604
  });
5578
5605
  }
5579
5606
  const acceptedWorkerSources = await registerSources(options, accepted);
5607
+ assertActive();
5580
5608
  await confirmRegisteredSources(options.driver, acceptedWorkerSources);
5609
+ assertActive();
5581
5610
  const writtenPages = [];
5582
5611
  writtenPages.push(...await applyPages(options.root, workerContribution, acceptedWorkerSources));
5612
+ assertActive();
5583
5613
  index = await buildKnowledgeIndex(options.root);
5614
+ assertActive();
5584
5615
  readiness = readinessFor(options, index);
5585
5616
  let driverSources = [];
5586
5617
  let driverNotes;
@@ -5595,11 +5626,16 @@ async function runVerifiedResearchLoop(options) {
5595
5626
  readiness: requireReadiness(readiness, options),
5596
5627
  signal: options.signal
5597
5628
  });
5629
+ assertActive();
5598
5630
  driverNotes = driverContribution.notes;
5599
5631
  driverSources = await registerSources(options, driverContribution.sources ?? []);
5632
+ assertActive();
5600
5633
  await confirmRegisteredSources(options.driver, driverSources);
5634
+ assertActive();
5601
5635
  writtenPages.push(...await applyPages(options.root, driverContribution, driverSources));
5636
+ assertActive();
5602
5637
  index = await buildKnowledgeIndex(options.root);
5638
+ assertActive();
5603
5639
  readiness = readinessFor(options, index);
5604
5640
  }
5605
5641
  const driverComplete = options.driver.isComplete?.() ?? true;
@@ -5608,6 +5644,7 @@ async function runVerifiedResearchLoop(options) {
5608
5644
  if (ready || remainingGaps.length === 0 && driverComplete) steer = void 0;
5609
5645
  else {
5610
5646
  await options.driver.prepareFold?.();
5647
+ assertActive();
5611
5648
  steer = foldGaps(options.driver, remainingGaps);
5612
5649
  }
5613
5650
  const step = {
@@ -5640,10 +5677,14 @@ async function runVerifiedResearchLoop(options) {
5640
5677
  }
5641
5678
  };
5642
5679
  await options.driver.checkpoint?.();
5680
+ assertActive();
5643
5681
  await store.putEvent(step.event);
5682
+ assertActive();
5644
5683
  steps.push(step);
5645
5684
  await options.onRound?.(step);
5685
+ assertActive();
5646
5686
  }
5687
+ assertActive();
5647
5688
  return {
5648
5689
  root: options.root,
5649
5690
  goal: options.goal,
@@ -5685,8 +5726,10 @@ function isDuplicate(source, existingUris, accepted) {
5685
5726
  async function registerSources(options, sources) {
5686
5727
  const records = [];
5687
5728
  for (const candidate of sources) {
5729
+ options.signal?.throwIfAborted();
5688
5730
  const source = snapshotSourceTextInput(candidate);
5689
5731
  records.push(await addSourceText(options.root, source, options.sourceOptions));
5732
+ options.signal?.throwIfAborted();
5690
5733
  }
5691
5734
  return records;
5692
5735
  }
@@ -5859,7 +5902,7 @@ function thesisSynthesisMessages(input, kbText) {
5859
5902
  async function writeThesisPage(root, input, thesis) {
5860
5903
  return withKnowledgeMutation(root, async () => {
5861
5904
  const relativePath = `knowledge/thesis-${slugify(input.ticker)}.md`;
5862
- await writeFileDurableWithinRoot(root, relativePath, [
5905
+ const body = [
5863
5906
  "---",
5864
5907
  `title: Investment thesis — ${input.company} (${input.ticker})`,
5865
5908
  `ticker: ${input.ticker}`,
@@ -5870,7 +5913,8 @@ async function writeThesisPage(root, input, thesis) {
5870
5913
  "",
5871
5914
  thesis.trim(),
5872
5915
  ""
5873
- ].join("\n"), { encoding: "utf8" });
5916
+ ].join("\n");
5917
+ await writeFileDurableWithinRoot(root, relativePath, body, { encoding: "utf8" });
5874
5918
  return join(root, relativePath);
5875
5919
  });
5876
5920
  }
@@ -5942,7 +5986,7 @@ function buildKnowledgeBrief(visiblePages, question, options = {}) {
5942
5986
  origin: entry.origin
5943
5987
  });
5944
5988
  const originals = /* @__PURE__ */ new Map();
5945
- const ranked = searchKnowledgePages(visiblePages.map((entry) => {
5989
+ const pages = visiblePages.map((entry) => {
5946
5990
  const page = {
5947
5991
  ...entry.page,
5948
5992
  id: qualified(entry),
@@ -5959,7 +6003,8 @@ function buildKnowledgeBrief(visiblePages, question, options = {}) {
5959
6003
  };
5960
6004
  originals.set(page, entry);
5961
6005
  return page;
5962
- }), question, {
6006
+ });
6007
+ const ranked = searchKnowledgePages(pages, question, {
5963
6008
  limit,
5964
6009
  excludeInvalidated,
5965
6010
  ...options.tags === void 0 ? {} : { tags: options.tags },
@@ -6296,7 +6341,7 @@ function verifyKnowledgeRetrievalReceipt(receipt) {
6296
6341
  if (receipt.digestAlgorithm !== "rfc8785-sha256") throw new Error(`unsupported knowledge retrieval digestAlgorithm '${receipt.digestAlgorithm}'`);
6297
6342
  const visibility = normalizeVisibilityRef(receipt.visibility);
6298
6343
  validateReceiptResultShape(receipt.results, visibility.pageCount);
6299
- if (canonicalCandidateDigest(retrievalMaterial({
6344
+ const material = retrievalMaterial({
6300
6345
  createdAt: isoTimestamp(receipt.createdAt, "knowledge retrieval createdAt"),
6301
6346
  runId: nonEmpty$1(receipt.runId, "knowledge retrieval runId"),
6302
6347
  actorId: optionalText(receipt.actorId, "knowledge retrieval actorId"),
@@ -6308,7 +6353,8 @@ function verifyKnowledgeRetrievalReceipt(receipt) {
6308
6353
  results: receipt.results,
6309
6354
  evidenceRefs: normalizeEvidenceRefs(receipt.evidenceRefs),
6310
6355
  attributes: normalizeAttributes(receipt.attributes)
6311
- })) !== receipt.receiptDigest) throw new Error("knowledge retrieval receipt digest mismatch");
6356
+ });
6357
+ if (canonicalCandidateDigest(material) !== receipt.receiptDigest) throw new Error("knowledge retrieval receipt digest mismatch");
6312
6358
  return receipt;
6313
6359
  }
6314
6360
  /**
@@ -6400,7 +6446,7 @@ function verifyKnowledgeUseReceipt(receipt, retrieval) {
6400
6446
  if (receipt.retrievalReceiptDigest !== verifiedRetrieval.receiptDigest) throw new Error("knowledge use receipt references a different retrieval receipt");
6401
6447
  const selected = verifiedRetrieval.results.find((result) => result.rank === receipt.used.rank);
6402
6448
  if (!selected || selected.pageId !== receipt.used.pageId || selected.origin !== receipt.used.origin || selected.path !== receipt.used.path || selected.pageDigest !== receipt.used.pageDigest) throw new Error("knowledge use receipt selected result does not match the retrieval receipt");
6403
- if (canonicalCandidateDigest(useMaterial({
6449
+ const material = useMaterial({
6404
6450
  createdAt: isoTimestamp(receipt.createdAt, "knowledge use createdAt"),
6405
6451
  retrieval: verifiedRetrieval,
6406
6452
  used: receipt.used,
@@ -6408,7 +6454,8 @@ function verifyKnowledgeUseReceipt(receipt, retrieval) {
6408
6454
  consumer: normalizeConsumer(receipt.consumer),
6409
6455
  evidenceRefs: normalizeEvidenceRefs(receipt.evidenceRefs),
6410
6456
  attributes: normalizeAttributes(receipt.attributes)
6411
- })) !== receipt.receiptDigest) throw new Error("knowledge use receipt digest mismatch");
6457
+ });
6458
+ if (canonicalCandidateDigest(material) !== receipt.receiptDigest) throw new Error("knowledge use receipt digest mismatch");
6412
6459
  return receipt;
6413
6460
  }
6414
6461
  /** Record that one retrieval led to no use, with no rank to select. */
@@ -6435,14 +6482,15 @@ function verifyKnowledgeRetrievalDisposition(disposition, retrieval) {
6435
6482
  if (disposition.kind !== "knowledge-retrieval-disposition") throw new Error(`knowledge retrieval disposition kind must be 'knowledge-retrieval-disposition'`);
6436
6483
  if (disposition.digestAlgorithm !== "rfc8785-sha256") throw new Error(`unsupported knowledge retrieval disposition digestAlgorithm '${disposition.digestAlgorithm}'`);
6437
6484
  if (disposition.retrievalReceiptDigest !== verifiedRetrieval.receiptDigest) throw new Error("knowledge retrieval disposition references a different retrieval receipt");
6438
- if (canonicalCandidateDigest(dispositionMaterial({
6485
+ const material = dispositionMaterial({
6439
6486
  createdAt: isoTimestamp(disposition.createdAt, "knowledge retrieval disposition createdAt"),
6440
6487
  retrieval: verifiedRetrieval,
6441
6488
  relation: validateDispositionRelation(disposition.relation),
6442
6489
  consumer: normalizeConsumer(disposition.consumer),
6443
6490
  evidenceRefs: normalizeEvidenceRefs(disposition.evidenceRefs),
6444
6491
  attributes: normalizeAttributes(disposition.attributes)
6445
- })) !== disposition.receiptDigest) throw new Error("knowledge retrieval disposition digest mismatch");
6492
+ });
6493
+ if (canonicalCandidateDigest(material) !== disposition.receiptDigest) throw new Error("knowledge retrieval disposition digest mismatch");
6446
6494
  return disposition;
6447
6495
  }
6448
6496
  function assertReceiptJoinsVisibility(receipt, index) {
@@ -6856,7 +6904,8 @@ async function promoteRunScopedPages(stores, runId, options) {
6856
6904
  const existing = await loadKnowledgePages(sharedRoot, { pagesDirectory });
6857
6905
  assertNoIdentityConflict(travellers, existing);
6858
6906
  const promotedPaths = new Set(travellers.map((traveller) => traveller.entry.page.path));
6859
- assertKnowledgeCitationsResolved(originatedPages([...existing.filter((page) => !promotedPaths.has(page.path)), ...travellers.map((traveller) => traveller.entry.page)], "shared"), travellers.flatMap((traveller) => (traveller.entry.page.cites ?? []).map((persisted) => parseKnowledgeCitationReference(persisted))));
6907
+ const promotedView = originatedPages([...existing.filter((page) => !promotedPaths.has(page.path)), ...travellers.map((traveller) => traveller.entry.page)], "shared");
6908
+ assertKnowledgeCitationsResolved(promotedView, travellers.flatMap((traveller) => (traveller.entry.page.cites ?? []).map((persisted) => parseKnowledgeCitationReference(persisted))));
6860
6909
  await commitKnowledgeFileMutations({
6861
6910
  root: sharedRoot,
6862
6911
  transactionRoot: lock.transactionRoot,
@@ -7672,8 +7721,18 @@ function buildDriver(options, persistence, restored) {
7672
7721
  }
7673
7722
  if (goal !== nextGoal) throw new Error(`claim ledger '${persistence?.ledgerId ?? "in-memory"}' accumulated evidence for goal '${goal}' and cannot be reused for '${nextGoal}'`);
7674
7723
  }
7675
- function resolveRouter() {
7676
- return options.router ?? createTangleRouterClient(options.router_options);
7724
+ function resolveRouter(signal) {
7725
+ if (options.router) return options.router;
7726
+ const configuredSignal = options.router_options?.signal;
7727
+ const requestSignal = configuredSignal && signal ? AbortSignal.any([configuredSignal, signal]) : configuredSignal ?? signal;
7728
+ return createTangleRouterClient({
7729
+ ...options.router_options,
7730
+ signal: requestSignal
7731
+ });
7732
+ }
7733
+ function throwIfRouterAborted(signal) {
7734
+ signal?.throwIfAborted();
7735
+ if (!options.router) options.router_options?.signal?.throwIfAborted();
7677
7736
  }
7678
7737
  /** Record a claim from a source, growing its independent-source support. */
7679
7738
  function recordClaim(extracted, sourceUri, round) {
@@ -7835,12 +7894,14 @@ function buildDriver(options, persistence, restored) {
7835
7894
  * because such a source cannot drive the research and pollutes the KB.
7836
7895
  */
7837
7896
  async verifySource(source, ctx) {
7897
+ throwIfRouterAborted(ctx.signal);
7838
7898
  const sourceSnapshot = snapshotSourceTextInput(source);
7839
7899
  const goalSnapshot = ctx.goal;
7840
7900
  const roundSnapshot = ctx.round;
7841
7901
  const sourceVersion = sourceVersionOfProposal(sourceSnapshot);
7842
7902
  bindGoal(goalSnapshot);
7843
- const extracted = await extractClaims(sourceSnapshot, goalSnapshot);
7903
+ const extracted = await extractClaims(sourceSnapshot, goalSnapshot, ctx.signal);
7904
+ throwIfRouterAborted(ctx.signal);
7844
7905
  if (extracted.length === 0) return {
7845
7906
  accept: false,
7846
7907
  reason: "no extractable claim: source yields nothing to drive the research deeper"
@@ -7855,6 +7916,7 @@ function buildDriver(options, persistence, restored) {
7855
7916
  if (registered && researchSourceVersionKey(registered) === researchSourceVersionKey(sourceVersion)) markAddressed(materializeEvidenceFor(/* @__PURE__ */ new Set([researchSourceVersionKey(sourceVersion)])));
7856
7917
  }
7857
7918
  await persist();
7919
+ throwIfRouterAborted(ctx.signal);
7858
7920
  return { accept: true };
7859
7921
  },
7860
7922
  /**
@@ -7898,8 +7960,9 @@ function buildDriver(options, persistence, restored) {
7898
7960
  commitSources,
7899
7961
  toLedger
7900
7962
  };
7901
- async function extractClaims(source, goal) {
7902
- const fromLlm = await extractClaimsWithLlm(source, goal, claimsForExtraction());
7963
+ async function extractClaims(source, goal, signal) {
7964
+ const fromLlm = await extractClaimsWithLlm(source, goal, claimsForExtraction(), signal);
7965
+ throwIfRouterAborted(signal);
7903
7966
  if (fromLlm.length > 0) return fromLlm.slice(0, maxClaimsPerSource);
7904
7967
  if (deterministicFallback) return deterministicClaims(source).slice(0, maxClaimsPerSource);
7905
7968
  return [];
@@ -7920,11 +7983,13 @@ function buildDriver(options, persistence, restored) {
7920
7983
  }
7921
7984
  return [...known.values()];
7922
7985
  }
7923
- async function extractClaimsWithLlm(source, goal, ledger) {
7986
+ async function extractClaimsWithLlm(source, goal, ledger, signal) {
7924
7987
  let router;
7925
7988
  try {
7926
- router = resolveRouter();
7927
- } catch {
7989
+ router = resolveRouter(signal);
7990
+ } catch (error) {
7991
+ throwIfRouterAborted(signal);
7992
+ if (error?.name === "AbortError") throw error;
7928
7993
  return [];
7929
7994
  }
7930
7995
  const excerpt = source.text.slice(0, 1800);
@@ -7946,9 +8011,12 @@ function buildDriver(options, persistence, restored) {
7946
8011
  role: "user",
7947
8012
  content: user
7948
8013
  }], 1200);
7949
- } catch {
8014
+ } catch (error) {
8015
+ throwIfRouterAborted(signal);
8016
+ if (error?.name === "AbortError") throw error;
7950
8017
  return [];
7951
8018
  }
8019
+ throwIfRouterAborted(signal);
7952
8020
  return parseExtractedClaims(raw, ledger);
7953
8021
  }
7954
8022
  /**