@tangle-network/agent-knowledge 1.7.0 → 1.9.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/README.md +77 -1
- package/dist/chunk-MU5CEOGE.js +387 -0
- package/dist/chunk-MU5CEOGE.js.map +1 -0
- package/dist/{chunk-HKYD765Q.js → chunk-WWY5FTKQ.js} +11 -14
- package/dist/chunk-WWY5FTKQ.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +382 -2
- package/dist/index.js +552 -9
- package/dist/index.js.map +1 -1
- package/dist/profiles/index.d.ts +1 -1
- package/dist/profiles/index.js +4 -361
- package/dist/profiles/index.js.map +1 -1
- package/docs/recursive-research-leaf.md +86 -0
- package/package.json +4 -4
- package/dist/chunk-HKYD765Q.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -33,13 +33,14 @@ import {
|
|
|
33
33
|
reciprocalRankFusion,
|
|
34
34
|
searchKnowledge,
|
|
35
35
|
sourceRegistryPath,
|
|
36
|
+
stringMetadata,
|
|
36
37
|
textSourceAdapter,
|
|
37
38
|
tokenizeQuery,
|
|
38
39
|
validateKnowledgeIndex,
|
|
39
40
|
writeJson,
|
|
40
41
|
writeKnowledgeIndex,
|
|
41
42
|
writeSourceRegistry
|
|
42
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-WWY5FTKQ.js";
|
|
43
44
|
import {
|
|
44
45
|
AgentMemoryHitSchema,
|
|
45
46
|
AgentMemoryKindSchema,
|
|
@@ -72,6 +73,9 @@ import {
|
|
|
72
73
|
slugify,
|
|
73
74
|
stableId
|
|
74
75
|
} from "./chunk-YMKHCTS2.js";
|
|
76
|
+
import {
|
|
77
|
+
researcherProfile
|
|
78
|
+
} from "./chunk-MU5CEOGE.js";
|
|
75
79
|
|
|
76
80
|
// src/changes.ts
|
|
77
81
|
function detectChanges(prev, next, options = {}) {
|
|
@@ -421,10 +425,6 @@ function sourceFreshness(sources, now) {
|
|
|
421
425
|
expiredSourceIds
|
|
422
426
|
};
|
|
423
427
|
}
|
|
424
|
-
function stringMetadata(metadata, key) {
|
|
425
|
-
const value = metadata?.[key];
|
|
426
|
-
return typeof value === "string" ? value : void 0;
|
|
427
|
-
}
|
|
428
428
|
function isIsoDate(value) {
|
|
429
429
|
return Boolean(value && Number.isFinite(Date.parse(value)));
|
|
430
430
|
}
|
|
@@ -837,6 +837,11 @@ function knowledgeReleaseReport(input) {
|
|
|
837
837
|
gateDecision: input.gateDecision ?? null,
|
|
838
838
|
thresholds: {
|
|
839
839
|
requireCorpus: false,
|
|
840
|
+
// The report gates on RunRecord-level outcomes, not on a separate scenario
|
|
841
|
+
// corpus (KnowledgeReleaseInput carries no dataset/scenarios). The scenario
|
|
842
|
+
// floor must therefore be 0 — otherwise the corpus axis fails closed on a
|
|
843
|
+
// dimension the report has no way to satisfy, masking the run-based gate.
|
|
844
|
+
minScenarioCount: 0,
|
|
840
845
|
requireHoldout: input.hasHoldout ?? false,
|
|
841
846
|
minHoldoutRuns: input.hasHoldout ? 1 : 0,
|
|
842
847
|
minSearchRuns: 1,
|
|
@@ -859,6 +864,19 @@ import {
|
|
|
859
864
|
blockingKnowledgeEval,
|
|
860
865
|
objectiveEval
|
|
861
866
|
} from "@tangle-network/agent-eval";
|
|
867
|
+
|
|
868
|
+
// src/readiness-helpers.ts
|
|
869
|
+
function readinessFor(options, index) {
|
|
870
|
+
if (!options.readinessSpecs?.length) return void 0;
|
|
871
|
+
return buildEvalKnowledgeBundle({
|
|
872
|
+
...options.readiness ?? {},
|
|
873
|
+
taskId: options.readinessTaskId ?? options.goal,
|
|
874
|
+
index,
|
|
875
|
+
specs: options.readinessSpecs
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// src/research-loop.ts
|
|
862
880
|
function createKnowledgeControlLoopAdapter(options) {
|
|
863
881
|
let initialized = false;
|
|
864
882
|
const appliedSteps = [];
|
|
@@ -1005,15 +1023,531 @@ async function applyKnowledgeResearchDecision(options, decision, iteration = 1)
|
|
|
1005
1023
|
metadata: decision.metadata
|
|
1006
1024
|
};
|
|
1007
1025
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1026
|
+
|
|
1027
|
+
// src/research-supervisor.ts
|
|
1028
|
+
import {
|
|
1029
|
+
supervise
|
|
1030
|
+
} from "@tangle-network/agent-runtime/loops";
|
|
1031
|
+
var RESEARCH_SUPERVISOR_SYSTEM_PROMPT = [
|
|
1032
|
+
"You are a research SUPERVISOR. You do not answer the research question yourself \u2014",
|
|
1033
|
+
"you create and manage a team of researcher workers that grow ONE shared",
|
|
1034
|
+
"knowledge base until it is ready.",
|
|
1035
|
+
"",
|
|
1036
|
+
"Each round:",
|
|
1037
|
+
" 1. Read the goal and the gaps the readiness gate still reports.",
|
|
1038
|
+
" 2. DECOMPOSE the open work into independent sub-topics. Spawn ONE researcher",
|
|
1039
|
+
" per sub-topic (spawn_agent) \u2014 split by sub-topic, never duplicate work.",
|
|
1040
|
+
" Spawn more researchers for a broad goal, fewer for a narrow one.",
|
|
1041
|
+
" 3. Wait for the researchers to settle (await_event). Steer or re-spawn for",
|
|
1042
|
+
" the sub-topics that came back thin.",
|
|
1043
|
+
" 4. STOP as soon as the deliverable check reports the knowledge base is ready.",
|
|
1044
|
+
"",
|
|
1045
|
+
"Conserve your budget: every spawn spends from one shared pool. Prefer a small",
|
|
1046
|
+
"number of well-scoped researchers over a large fanout that re-covers the same",
|
|
1047
|
+
"ground."
|
|
1048
|
+
].join("\n");
|
|
1049
|
+
function knowledgeReadinessDeliverable(options) {
|
|
1050
|
+
return {
|
|
1051
|
+
describe: `knowledge base at ${options.root} is ready for: ${options.goal}`,
|
|
1052
|
+
async check() {
|
|
1053
|
+
const index = await buildKnowledgeIndex(options.root);
|
|
1054
|
+
const bundle = readinessFor(options, index);
|
|
1055
|
+
return (bundle?.report.blockingMissingRequirements.length ?? 0) === 0;
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
async function runResearchSupervisor(options) {
|
|
1060
|
+
await initKnowledgeBase(options.root);
|
|
1061
|
+
const { profile: workerProfile } = researcherProfile({ harness: options.harness });
|
|
1062
|
+
const baseInstructions = options.supervisorSystemPrompt ?? RESEARCH_SUPERVISOR_SYSTEM_PROMPT;
|
|
1063
|
+
const workerContract = workerProfile.prompt?.systemPrompt;
|
|
1064
|
+
const systemPrompt = workerContract ? `${baseInstructions}
|
|
1065
|
+
|
|
1066
|
+
Each researcher worker you spawn follows this contract:
|
|
1067
|
+
${workerContract}` : baseInstructions;
|
|
1068
|
+
const profile = {
|
|
1069
|
+
name: "research-supervisor",
|
|
1070
|
+
model: options.supervisorModel,
|
|
1071
|
+
systemPrompt
|
|
1072
|
+
};
|
|
1073
|
+
return supervise(profile, options.goal, {
|
|
1074
|
+
...options.superviseOptions,
|
|
1075
|
+
budget: options.budget,
|
|
1076
|
+
backend: options.backend,
|
|
1077
|
+
deliverable: knowledgeReadinessDeliverable(options)
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/two-agent-research-loop.ts
|
|
1082
|
+
async function runTwoAgentResearchLoop(options) {
|
|
1083
|
+
const maxRounds = Math.max(1, options.maxRounds ?? 3);
|
|
1084
|
+
await initKnowledgeBase(options.root);
|
|
1085
|
+
const steps = [];
|
|
1086
|
+
let index = await buildKnowledgeIndex(options.root);
|
|
1087
|
+
let readiness = readinessFor(options, index);
|
|
1088
|
+
let ready = isReady(readiness?.report);
|
|
1089
|
+
let steer;
|
|
1090
|
+
for (let round2 = 1; round2 <= maxRounds && !ready; round2++) {
|
|
1091
|
+
if (options.signal?.aborted) throw new Error("Two-agent research loop aborted");
|
|
1092
|
+
const gaps = gapsFromReadiness(readiness);
|
|
1093
|
+
const workerContribution = await options.worker({
|
|
1094
|
+
root: options.root,
|
|
1095
|
+
goal: options.goal,
|
|
1096
|
+
round: round2,
|
|
1097
|
+
index,
|
|
1098
|
+
gaps,
|
|
1099
|
+
steer,
|
|
1100
|
+
readiness: requireReadiness(readiness, options),
|
|
1101
|
+
signal: options.signal
|
|
1102
|
+
});
|
|
1103
|
+
const accepted = [];
|
|
1104
|
+
const rejectedWorkerSources = [];
|
|
1105
|
+
const existingUris = new Set(
|
|
1106
|
+
index.sources.flatMap(
|
|
1107
|
+
(source) => typeof source.metadata?.originalUri === "string" ? [source.metadata.originalUri] : []
|
|
1108
|
+
)
|
|
1109
|
+
);
|
|
1110
|
+
for (const source of workerContribution.sources ?? []) {
|
|
1111
|
+
if (isDuplicate(source, existingUris, accepted)) {
|
|
1112
|
+
rejectedWorkerSources.push({ source, reason: "duplicate: already in the knowledge base" });
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
const verdict = await options.driver.verifySource(source, {
|
|
1116
|
+
root: options.root,
|
|
1117
|
+
goal: options.goal,
|
|
1118
|
+
round: round2,
|
|
1119
|
+
index,
|
|
1120
|
+
gaps,
|
|
1121
|
+
acceptedThisRound: accepted,
|
|
1122
|
+
signal: options.signal
|
|
1123
|
+
});
|
|
1124
|
+
if (verdict.accept) accepted.push(source);
|
|
1125
|
+
else rejectedWorkerSources.push({ source, reason: verdict.reason });
|
|
1126
|
+
}
|
|
1127
|
+
const acceptedWorkerSources = await registerSources(options, accepted);
|
|
1128
|
+
const writtenPages = [];
|
|
1129
|
+
writtenPages.push(
|
|
1130
|
+
...await applyPages(options.root, workerContribution, acceptedWorkerSources)
|
|
1131
|
+
);
|
|
1132
|
+
index = await buildKnowledgeIndex(options.root);
|
|
1133
|
+
readiness = readinessFor(options, index);
|
|
1134
|
+
let driverSources = [];
|
|
1135
|
+
let driverNotes;
|
|
1136
|
+
if (options.driverResearches && options.driver.research) {
|
|
1137
|
+
const remainingGaps2 = gapsFromReadiness(readiness);
|
|
1138
|
+
const driverContribution = await options.driver.research({
|
|
1139
|
+
root: options.root,
|
|
1140
|
+
goal: options.goal,
|
|
1141
|
+
round: round2,
|
|
1142
|
+
index,
|
|
1143
|
+
remainingGaps: remainingGaps2,
|
|
1144
|
+
readiness: requireReadiness(readiness, options),
|
|
1145
|
+
signal: options.signal
|
|
1146
|
+
});
|
|
1147
|
+
driverNotes = driverContribution.notes;
|
|
1148
|
+
driverSources = await registerSources(options, driverContribution.sources ?? []);
|
|
1149
|
+
writtenPages.push(...await applyPages(options.root, driverContribution, driverSources));
|
|
1150
|
+
index = await buildKnowledgeIndex(options.root);
|
|
1151
|
+
readiness = readinessFor(options, index);
|
|
1152
|
+
}
|
|
1153
|
+
ready = isReady(readiness?.report);
|
|
1154
|
+
const remainingGaps = gapsFromReadiness(readiness);
|
|
1155
|
+
steer = ready ? void 0 : foldGaps(options.driver, remainingGaps);
|
|
1156
|
+
const step = {
|
|
1157
|
+
round: round2,
|
|
1158
|
+
gaps,
|
|
1159
|
+
acceptedWorkerSources,
|
|
1160
|
+
rejectedWorkerSources,
|
|
1161
|
+
driverSources,
|
|
1162
|
+
writtenPages,
|
|
1163
|
+
readiness,
|
|
1164
|
+
ready,
|
|
1165
|
+
event: createKnowledgeEvent({
|
|
1166
|
+
type: "research.iteration",
|
|
1167
|
+
actor: options.actor,
|
|
1168
|
+
target: options.root,
|
|
1169
|
+
metadata: {
|
|
1170
|
+
goal: options.goal,
|
|
1171
|
+
round: round2,
|
|
1172
|
+
ready,
|
|
1173
|
+
acceptedWorkerSourceCount: acceptedWorkerSources.length,
|
|
1174
|
+
rejectedWorkerSourceCount: rejectedWorkerSources.length,
|
|
1175
|
+
driverSourceCount: driverSources.length,
|
|
1176
|
+
writtenPageCount: writtenPages.length,
|
|
1177
|
+
remainingGapCount: remainingGaps.length
|
|
1178
|
+
}
|
|
1179
|
+
}),
|
|
1180
|
+
notes: { worker: workerContribution.notes, driver: driverNotes }
|
|
1181
|
+
};
|
|
1182
|
+
steps.push(step);
|
|
1183
|
+
await options.onRound?.(step);
|
|
1184
|
+
}
|
|
1185
|
+
return {
|
|
1186
|
+
root: options.root,
|
|
1187
|
+
goal: options.goal,
|
|
1188
|
+
rounds: steps.length,
|
|
1189
|
+
ready,
|
|
1190
|
+
index,
|
|
1191
|
+
readiness,
|
|
1192
|
+
steps
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function isReady(report) {
|
|
1196
|
+
if (!report) return false;
|
|
1197
|
+
return report.blockingMissingRequirements.length === 0;
|
|
1198
|
+
}
|
|
1199
|
+
function gapsFromReadiness(readiness) {
|
|
1200
|
+
if (!readiness) return [];
|
|
1201
|
+
const blocking = readiness.report.blockingMissingRequirements.map(
|
|
1202
|
+
(requirement) => gapFor(requirement, readiness, true)
|
|
1203
|
+
);
|
|
1204
|
+
const nonBlocking = readiness.report.nonBlockingGaps.map(
|
|
1205
|
+
(requirement) => gapFor(requirement, readiness, false)
|
|
1206
|
+
);
|
|
1207
|
+
return [...blocking, ...nonBlocking];
|
|
1208
|
+
}
|
|
1209
|
+
function gapFor(requirement, readiness, blocking) {
|
|
1210
|
+
const spec = readiness.requirements.find((entry) => entry.id === requirement.id);
|
|
1211
|
+
const query = typeof spec?.metadata?.query === "string" ? spec.metadata.query : requirement.description;
|
|
1212
|
+
return { id: requirement.id, description: requirement.description, query, blocking };
|
|
1213
|
+
}
|
|
1214
|
+
function foldGaps(driver, gaps) {
|
|
1215
|
+
if (gaps.length === 0) return void 0;
|
|
1216
|
+
if (driver.foldGaps) return driver.foldGaps(gaps);
|
|
1217
|
+
return [
|
|
1218
|
+
"The knowledge base is still missing the following. Prioritise these next round:",
|
|
1219
|
+
...gaps.map(
|
|
1220
|
+
(gap) => `- (${gap.blocking ? "blocking" : "soft"}) ${gap.description} [${gap.id}]`
|
|
1221
|
+
)
|
|
1222
|
+
].join("\n");
|
|
1223
|
+
}
|
|
1224
|
+
function isDuplicate(source, existingUris, accepted) {
|
|
1225
|
+
return existingUris.has(source.uri) || accepted.some((candidate) => candidate.uri === source.uri);
|
|
1226
|
+
}
|
|
1227
|
+
async function registerSources(options, sources) {
|
|
1228
|
+
const records = [];
|
|
1229
|
+
for (const source of sources) {
|
|
1230
|
+
records.push(await addSourceText(options.root, source, options.sourceOptions));
|
|
1231
|
+
}
|
|
1232
|
+
return records;
|
|
1233
|
+
}
|
|
1234
|
+
async function applyPages(root, contribution, acceptedSources) {
|
|
1235
|
+
if (acceptedSources.length === 0) return [];
|
|
1236
|
+
const parts = [];
|
|
1237
|
+
if (contribution.proposalText) parts.push(contribution.proposalText);
|
|
1238
|
+
const built = contribution.buildPages?.(acceptedSources);
|
|
1239
|
+
if (built) parts.push(built);
|
|
1240
|
+
if (parts.length === 0) return [];
|
|
1241
|
+
const applied = await applyKnowledgeWriteBlocks(root, parts.join("\n"));
|
|
1242
|
+
return applied.written;
|
|
1243
|
+
}
|
|
1244
|
+
function requireReadiness(readiness, options) {
|
|
1245
|
+
if (readiness) return readiness;
|
|
1010
1246
|
return buildEvalKnowledgeBundle({
|
|
1011
1247
|
...options.readiness ?? {},
|
|
1012
1248
|
taskId: options.readinessTaskId ?? options.goal,
|
|
1013
|
-
index,
|
|
1014
|
-
specs:
|
|
1249
|
+
index: emptyIndex(options.root),
|
|
1250
|
+
specs: []
|
|
1015
1251
|
});
|
|
1016
1252
|
}
|
|
1253
|
+
function emptyIndex(root) {
|
|
1254
|
+
return {
|
|
1255
|
+
root,
|
|
1256
|
+
generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1257
|
+
sources: [],
|
|
1258
|
+
pages: [],
|
|
1259
|
+
graph: { nodes: [], edges: [] }
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
function sourceMatchesGaps(source, index, gaps) {
|
|
1263
|
+
const haystack = `${source.title ?? ""}
|
|
1264
|
+
${source.text}`.toLowerCase();
|
|
1265
|
+
const hits = [];
|
|
1266
|
+
for (const gap of gaps) {
|
|
1267
|
+
for (const token of gap.query.toLowerCase().split(/\s+/).filter(Boolean)) {
|
|
1268
|
+
if (haystack.includes(token)) {
|
|
1269
|
+
hits.push(...searchKnowledge(index, gap.query, 1));
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return hits;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/web-research-worker.ts
|
|
1278
|
+
var DEFAULT_MODEL = "glm-5.2";
|
|
1279
|
+
var DEFAULT_BASE_URL = "https://router.tangle.tools/v1";
|
|
1280
|
+
var MIN_MAX_TOKENS = 1200;
|
|
1281
|
+
var RouterError = class extends Error {
|
|
1282
|
+
constructor(status, message) {
|
|
1283
|
+
super(`router ${status}: ${message}`);
|
|
1284
|
+
this.status = status;
|
|
1285
|
+
this.name = "RouterError";
|
|
1286
|
+
}
|
|
1287
|
+
status;
|
|
1288
|
+
};
|
|
1289
|
+
function createTangleRouterClient(options = {}) {
|
|
1290
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
1291
|
+
const apiKey = options.apiKey ?? process.env.TANGLE_API_KEY;
|
|
1292
|
+
if (!apiKey) {
|
|
1293
|
+
throw new RouterError(401, "no TANGLE_API_KEY (pass apiKey or set the env var)");
|
|
1294
|
+
}
|
|
1295
|
+
const model = options.model ?? DEFAULT_MODEL;
|
|
1296
|
+
const headers = {
|
|
1297
|
+
"Content-Type": "application/json",
|
|
1298
|
+
Authorization: `Bearer ${apiKey}`
|
|
1299
|
+
};
|
|
1300
|
+
return {
|
|
1301
|
+
async search(query, opts) {
|
|
1302
|
+
const res = await fetch(`${baseUrl}/search`, {
|
|
1303
|
+
method: "POST",
|
|
1304
|
+
headers,
|
|
1305
|
+
signal: options.signal,
|
|
1306
|
+
body: JSON.stringify({
|
|
1307
|
+
query,
|
|
1308
|
+
...options.searchProvider ? { provider: options.searchProvider } : {},
|
|
1309
|
+
...opts?.maxResults != null ? { maxResults: opts.maxResults } : {}
|
|
1310
|
+
})
|
|
1311
|
+
});
|
|
1312
|
+
if (!res.ok) {
|
|
1313
|
+
throw new RouterError(res.status, await res.text().catch(() => res.statusText));
|
|
1314
|
+
}
|
|
1315
|
+
const body = await res.json();
|
|
1316
|
+
return (body.data ?? []).filter((hit) => typeof hit?.url === "string" && hit.url.length > 0).map((hit) => ({ title: hit.title ?? hit.url, url: hit.url, snippet: hit.snippet }));
|
|
1317
|
+
},
|
|
1318
|
+
async chat(messages, maxTokens) {
|
|
1319
|
+
const max_tokens = Math.max(MIN_MAX_TOKENS, maxTokens ?? MIN_MAX_TOKENS);
|
|
1320
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
1321
|
+
method: "POST",
|
|
1322
|
+
headers,
|
|
1323
|
+
signal: options.signal,
|
|
1324
|
+
body: JSON.stringify({ model, messages, max_tokens, temperature: 0.2, stream: false })
|
|
1325
|
+
});
|
|
1326
|
+
if (!res.ok) {
|
|
1327
|
+
throw new RouterError(res.status, await res.text().catch(() => res.statusText));
|
|
1328
|
+
}
|
|
1329
|
+
const body = await res.json();
|
|
1330
|
+
return body.choices?.[0]?.message?.content ?? "";
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
function resolveRouter(opts) {
|
|
1335
|
+
return opts.router ?? createTangleRouterClient(opts.router_options);
|
|
1336
|
+
}
|
|
1337
|
+
function createWebResearchWorker(options = {}) {
|
|
1338
|
+
const queriesPerGap = Math.max(1, options.queriesPerGap ?? 2);
|
|
1339
|
+
const resultsPerQuery = Math.max(1, options.resultsPerQuery ?? 3);
|
|
1340
|
+
const maxSourcesPerRound = Math.max(1, options.maxSourcesPerRound ?? 6);
|
|
1341
|
+
const minTextChars = Math.max(1, options.minTextChars ?? 200);
|
|
1342
|
+
const maxTextChars = Math.max(minTextChars, options.maxTextChars ?? 4e3);
|
|
1343
|
+
return async (ctx) => {
|
|
1344
|
+
const router = resolveRouter(options);
|
|
1345
|
+
const targetGaps = ctx.gaps.filter((gap) => gap.blocking);
|
|
1346
|
+
const gaps = targetGaps.length > 0 ? targetGaps : ctx.gaps;
|
|
1347
|
+
if (gaps.length === 0) {
|
|
1348
|
+
return { sources: [], notes: "no open gaps to research" };
|
|
1349
|
+
}
|
|
1350
|
+
const queries = await formSearchQueries(router, ctx, gaps, queriesPerGap);
|
|
1351
|
+
const proposals = [];
|
|
1352
|
+
const seenUris = /* @__PURE__ */ new Set();
|
|
1353
|
+
for (const query of queries) {
|
|
1354
|
+
if (proposals.length >= maxSourcesPerRound) break;
|
|
1355
|
+
if (ctx.signal?.aborted) break;
|
|
1356
|
+
let hits;
|
|
1357
|
+
try {
|
|
1358
|
+
hits = await router.search(query, { maxResults: resultsPerQuery });
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
if (error.name === "AbortError") break;
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
for (const hit of hits) {
|
|
1364
|
+
if (proposals.length >= maxSourcesPerRound) break;
|
|
1365
|
+
if (seenUris.has(hit.url)) continue;
|
|
1366
|
+
const fetched = await politeFetch(hit.url, {
|
|
1367
|
+
signal: ctx.signal,
|
|
1368
|
+
cacheDir: options.cacheDir
|
|
1369
|
+
});
|
|
1370
|
+
if (!fetched.verifiable) continue;
|
|
1371
|
+
const text = htmlToText(fetched.body).slice(0, maxTextChars);
|
|
1372
|
+
if (text.length < minTextChars) continue;
|
|
1373
|
+
seenUris.add(hit.url);
|
|
1374
|
+
proposals.push({
|
|
1375
|
+
uri: hit.url,
|
|
1376
|
+
title: hit.title || hit.url,
|
|
1377
|
+
text,
|
|
1378
|
+
// We just fetched + verified this page, so stamp `lastVerifiedAt` with
|
|
1379
|
+
// fetch time. Do NOT set `validUntil`: a live page has no inherent
|
|
1380
|
+
// future expiry, and the readiness freshness check treats any
|
|
1381
|
+
// `validUntil <= now` as EXPIRED (score 0). The page's `Last-Modified`
|
|
1382
|
+
// is a PAST date, so writing it here would mark every real source stale
|
|
1383
|
+
// and zero out coverage. Record it as provenance metadata instead.
|
|
1384
|
+
lastVerifiedAt: fetched.fetchedAt,
|
|
1385
|
+
metadata: {
|
|
1386
|
+
discoveredVia: query,
|
|
1387
|
+
snippet: hit.snippet ?? "",
|
|
1388
|
+
goal: ctx.goal,
|
|
1389
|
+
sourceUpdatedAt: fetched.sourceUpdatedAt
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
return {
|
|
1395
|
+
sources: proposals,
|
|
1396
|
+
buildPages: buildCitingPages(proposals),
|
|
1397
|
+
notes: `web-research worker: ${queries.length} queries \u2192 ${proposals.length} fetched sources`
|
|
1398
|
+
};
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
async function formSearchQueries(router, ctx, gaps, queriesPerGap) {
|
|
1402
|
+
const gapLines = gaps.map((gap, i) => `${i + 1}. ${gap.description} (readiness query: "${gap.query}")`).join("\n");
|
|
1403
|
+
const want = gaps.length * queriesPerGap;
|
|
1404
|
+
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.";
|
|
1405
|
+
const user = [
|
|
1406
|
+
`Research goal: ${ctx.goal}`,
|
|
1407
|
+
ctx.steer ? `Steer from the coordinator:
|
|
1408
|
+
${ctx.steer}` : "",
|
|
1409
|
+
`Open knowledge gaps:
|
|
1410
|
+
${gapLines}`,
|
|
1411
|
+
`Return up to ${want} search query strings as a JSON array (e.g. ["query one","query two"]).`
|
|
1412
|
+
].filter(Boolean).join("\n\n");
|
|
1413
|
+
let raw = "";
|
|
1414
|
+
try {
|
|
1415
|
+
raw = await router.chat(
|
|
1416
|
+
[
|
|
1417
|
+
{ role: "system", content: system },
|
|
1418
|
+
{ role: "user", content: user }
|
|
1419
|
+
],
|
|
1420
|
+
MIN_MAX_TOKENS
|
|
1421
|
+
);
|
|
1422
|
+
} catch {
|
|
1423
|
+
raw = "";
|
|
1424
|
+
}
|
|
1425
|
+
const parsed = parseQueryList(raw);
|
|
1426
|
+
const fromLlm = parsed.slice(0, want);
|
|
1427
|
+
if (fromLlm.length > 0) return dedupeStrings(fromLlm);
|
|
1428
|
+
return dedupeStrings(gaps.map((gap) => gap.query || gap.description));
|
|
1429
|
+
}
|
|
1430
|
+
function parseQueryList(raw) {
|
|
1431
|
+
const text = raw.trim();
|
|
1432
|
+
if (!text) return [];
|
|
1433
|
+
const candidates = [];
|
|
1434
|
+
const arrayMatch = text.match(/\[[\s\S]*\]/);
|
|
1435
|
+
if (arrayMatch) {
|
|
1436
|
+
try {
|
|
1437
|
+
const arr = JSON.parse(arrayMatch[0]);
|
|
1438
|
+
for (const item of arr)
|
|
1439
|
+
if (typeof item === "string" && item.trim()) candidates.push(item.trim());
|
|
1440
|
+
} catch {
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (candidates.length === 0) {
|
|
1444
|
+
for (const line of text.split("\n")) {
|
|
1445
|
+
const cleaned = line.replace(/^\s*(?:[-*]|\d+[.)])\s*/, "").replace(/^["']|["']$/g, "").trim();
|
|
1446
|
+
if (cleaned && cleaned.length > 2 && !cleaned.startsWith("{")) candidates.push(cleaned);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return candidates;
|
|
1450
|
+
}
|
|
1451
|
+
function dedupeStrings(values) {
|
|
1452
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1453
|
+
const out = [];
|
|
1454
|
+
for (const value of values) {
|
|
1455
|
+
const key = value.toLowerCase().trim();
|
|
1456
|
+
if (key && !seen.has(key)) {
|
|
1457
|
+
seen.add(key);
|
|
1458
|
+
out.push(value.trim());
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
return out;
|
|
1462
|
+
}
|
|
1463
|
+
function buildCitingPages(proposals) {
|
|
1464
|
+
return (acceptedSources) => {
|
|
1465
|
+
if (acceptedSources.length === 0) return void 0;
|
|
1466
|
+
const blocks = acceptedSources.map((record) => {
|
|
1467
|
+
const proposal = proposals.find((p) => p.uri === record.metadata?.originalUri);
|
|
1468
|
+
const uri = proposal?.uri ?? record.id;
|
|
1469
|
+
const slug = uri.replace(/^https?:\/\//, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 120) || record.id;
|
|
1470
|
+
const title = proposal?.title ?? record.id;
|
|
1471
|
+
const body = proposal?.text ?? "";
|
|
1472
|
+
return [
|
|
1473
|
+
`---FILE: knowledge/${slug}.md---`,
|
|
1474
|
+
"---",
|
|
1475
|
+
`title: ${escapeYaml(title)}`,
|
|
1476
|
+
`sources: ["${record.id}"]`,
|
|
1477
|
+
`source_url: ${uri}`,
|
|
1478
|
+
"---",
|
|
1479
|
+
`# ${title}`,
|
|
1480
|
+
"",
|
|
1481
|
+
body,
|
|
1482
|
+
"",
|
|
1483
|
+
`Source: ${uri}`,
|
|
1484
|
+
"---END FILE---"
|
|
1485
|
+
].join("\n");
|
|
1486
|
+
});
|
|
1487
|
+
return blocks.join("\n");
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function escapeYaml(value) {
|
|
1491
|
+
return value.replace(/[\r\n]+/g, " ").replace(/"/g, "'").trim();
|
|
1492
|
+
}
|
|
1493
|
+
function createVerifyingResearchDriver(options = {}) {
|
|
1494
|
+
const acceptOnParseFailure = options.acceptOnParseFailure ?? false;
|
|
1495
|
+
return {
|
|
1496
|
+
async verifySource(source, ctx) {
|
|
1497
|
+
const router = resolveRouter(options);
|
|
1498
|
+
const gapLines = ctx.gaps.map((gap) => `- ${gap.description} (query: "${gap.query}")`).join("\n");
|
|
1499
|
+
const acceptedTitles = ctx.acceptedThisRound.map((accepted) => `- ${accepted.title ?? accepted.uri}`).join("\n");
|
|
1500
|
+
const excerpt = source.text.slice(0, 1500);
|
|
1501
|
+
const system = 'You verify whether a fetched web source belongs in a curated knowledge base. Accept a source ONLY if it is genuinely on-topic for the research goal and helps close one of the open gaps, AND it is not a near-duplicate of an already-accepted source. Reject spam, listicles, off-topic pages, marketing, and near-duplicates. Respond with ONLY a JSON object: {"accept": true|false, "reason": "<short reason>"}.';
|
|
1502
|
+
const user = [
|
|
1503
|
+
`Research goal: ${ctx.goal}`,
|
|
1504
|
+
`Open gaps:
|
|
1505
|
+
${gapLines || "(none specified)"}`,
|
|
1506
|
+
acceptedTitles ? `Already accepted this round:
|
|
1507
|
+
${acceptedTitles}` : "Nothing accepted yet this round.",
|
|
1508
|
+
`Candidate source:
|
|
1509
|
+
URL: ${source.uri}
|
|
1510
|
+
Title: ${source.title ?? "(none)"}
|
|
1511
|
+
Excerpt:
|
|
1512
|
+
${excerpt}`,
|
|
1513
|
+
'Verdict as JSON {"accept": boolean, "reason": string}:'
|
|
1514
|
+
].join("\n\n");
|
|
1515
|
+
let raw = "";
|
|
1516
|
+
try {
|
|
1517
|
+
raw = await router.chat(
|
|
1518
|
+
[
|
|
1519
|
+
{ role: "system", content: system },
|
|
1520
|
+
{ role: "user", content: user }
|
|
1521
|
+
],
|
|
1522
|
+
MIN_MAX_TOKENS
|
|
1523
|
+
);
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
if (error.name === "AbortError") throw error;
|
|
1526
|
+
return acceptOnParseFailure ? { accept: true } : { accept: false, reason: `verifier unavailable: ${error.message}` };
|
|
1527
|
+
}
|
|
1528
|
+
const verdict = parseVerdict(raw);
|
|
1529
|
+
if (verdict) return verdict;
|
|
1530
|
+
return acceptOnParseFailure ? { accept: true } : { accept: false, reason: "verifier returned an unparseable verdict" };
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
function parseVerdict(raw) {
|
|
1535
|
+
const text = raw.trim();
|
|
1536
|
+
if (!text) return null;
|
|
1537
|
+
const objMatch = text.match(/\{[\s\S]*\}/);
|
|
1538
|
+
if (!objMatch) return null;
|
|
1539
|
+
try {
|
|
1540
|
+
const parsed = JSON.parse(objMatch[0]);
|
|
1541
|
+
if (typeof parsed.accept !== "boolean") return null;
|
|
1542
|
+
if (parsed.accept) return { accept: true };
|
|
1543
|
+
return {
|
|
1544
|
+
accept: false,
|
|
1545
|
+
reason: typeof parsed.reason === "string" && parsed.reason.trim() ? parsed.reason.trim() : "rejected by verifier"
|
|
1546
|
+
};
|
|
1547
|
+
} catch {
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1017
1551
|
export {
|
|
1018
1552
|
AgentMemoryHitSchema,
|
|
1019
1553
|
AgentMemoryKindSchema,
|
|
@@ -1033,6 +1567,8 @@ export {
|
|
|
1033
1567
|
MemoryKbStore,
|
|
1034
1568
|
POLITE_USER_AGENT,
|
|
1035
1569
|
READINESS_SPEC_DEFAULTS,
|
|
1570
|
+
RESEARCH_SUPERVISOR_SYSTEM_PROMPT,
|
|
1571
|
+
RouterError,
|
|
1036
1572
|
SCAFFOLD_PAGE_BASENAMES,
|
|
1037
1573
|
SourceAnchorSchema,
|
|
1038
1574
|
SourceRecordSchema,
|
|
@@ -1055,6 +1591,9 @@ export {
|
|
|
1055
1591
|
createLocalDiscoveryDispatcher,
|
|
1056
1592
|
createNeo4jAgentMemoryAdapter,
|
|
1057
1593
|
createStateSosSource,
|
|
1594
|
+
createTangleRouterClient,
|
|
1595
|
+
createVerifyingResearchDriver,
|
|
1596
|
+
createWebResearchWorker,
|
|
1058
1597
|
defaultGetMemoryContext,
|
|
1059
1598
|
defineReadinessSpec,
|
|
1060
1599
|
detectChanges,
|
|
@@ -1069,6 +1608,7 @@ export {
|
|
|
1069
1608
|
inspectKnowledgeIndex,
|
|
1070
1609
|
isSafeKnowledgePath,
|
|
1071
1610
|
isScaffoldPath,
|
|
1611
|
+
knowledgeReadinessDeliverable,
|
|
1072
1612
|
knowledgeReleaseReport,
|
|
1073
1613
|
layoutFor,
|
|
1074
1614
|
lintKnowledgeIndex,
|
|
@@ -1087,9 +1627,12 @@ export {
|
|
|
1087
1627
|
reciprocalRankFusion,
|
|
1088
1628
|
renderMemoryContext,
|
|
1089
1629
|
runKnowledgeResearchLoop,
|
|
1630
|
+
runResearchSupervisor,
|
|
1631
|
+
runTwoAgentResearchLoop,
|
|
1090
1632
|
searchKnowledge,
|
|
1091
1633
|
sha256,
|
|
1092
1634
|
slugify,
|
|
1635
|
+
sourceMatchesGaps,
|
|
1093
1636
|
sourceRegistryPath,
|
|
1094
1637
|
stableId,
|
|
1095
1638
|
stripFrontmatter,
|