@tangle-network/agent-runtime 0.89.0 → 0.90.1
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 +51 -18
- package/dist/agent.d.ts +53 -2
- package/dist/agent.js +168 -6
- package/dist/agent.js.map +1 -1
- package/dist/analyst-loop.d.ts +1 -1
- package/dist/chunk-5QOB7H74.js +387 -0
- package/dist/chunk-5QOB7H74.js.map +1 -0
- package/dist/{chunk-4WXGK6GV.js → chunk-7ON74BQO.js} +2 -2
- package/dist/{chunk-7LO5GMAO.js → chunk-CMYMTRGA.js} +14 -5
- package/dist/chunk-CMYMTRGA.js.map +1 -0
- package/dist/{chunk-UD4BHQMI.js → chunk-IVGYLCFH.js} +2 -2
- package/dist/{chunk-UD4BHQMI.js.map → chunk-IVGYLCFH.js.map} +1 -1
- package/dist/{chunk-VNOOH22O.js → chunk-OOL3675H.js} +85 -4
- package/dist/chunk-OOL3675H.js.map +1 -0
- package/dist/{chunk-5AVV7KAH.js → chunk-QK4DV5PR.js} +2 -2
- package/dist/chunk-R2VAJGR3.js +214 -0
- package/dist/chunk-R2VAJGR3.js.map +1 -0
- package/dist/{chunk-N7EJV7N3.js → chunk-RYBVU4M3.js} +3592 -3562
- package/dist/chunk-RYBVU4M3.js.map +1 -0
- package/dist/{chunk-BQPFZE2C.js → chunk-WRUSWK4F.js} +5 -5
- package/dist/{chunk-PIPPLSOF.js → chunk-ZV4LXYCJ.js} +451 -24
- package/dist/chunk-ZV4LXYCJ.js.map +1 -0
- package/dist/{worktree-harness-CX_McRLp.d.ts → completion-gate-DkAnUmpb.d.ts} +45 -45
- package/dist/{coordination-CuDLO8wj.d.ts → coordination-rRj5hjJK.d.ts} +415 -227
- package/dist/environment-provider.d.ts +3 -3
- package/dist/index.d.ts +255 -16
- package/dist/index.js +463 -72
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +228 -84
- package/dist/intelligence.js +259 -166
- package/dist/intelligence.js.map +1 -1
- package/dist/knowledge.d.ts +120 -0
- package/dist/knowledge.js +28 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/lifecycle.d.ts +2 -2
- package/dist/lifecycle.js +2 -2
- package/dist/{local-harness-sI0S_XNA.d.ts → local-harness-dcD5WTTr.d.ts} +3 -0
- package/dist/{loop-runner-bin-hQDUL8Ld.d.ts → loop-runner-bin-DTbZVGfM.d.ts} +2 -2
- package/dist/loop-runner-bin.d.ts +6 -6
- package/dist/loop-runner-bin.js +7 -7
- package/dist/loops.d.ts +34 -369
- package/dist/loops.js +31 -5
- package/dist/mcp/bin.js +5 -5
- package/dist/mcp/index.d.ts +10 -11
- package/dist/mcp/index.js +10 -10
- package/dist/{mcp-serve-verifier-FL7-ZEb_.d.ts → mcp-serve-verifier-XsX8rkB9.d.ts} +12 -1
- package/dist/{openai-tools-BnrOmGjN.d.ts → openai-tools-C4ZfUD4L.d.ts} +1 -1
- package/dist/profiles.d.ts +196 -5
- package/dist/profiles.js +8 -0
- package/dist/profiles.js.map +1 -1
- package/dist/{router-client-r8y_VFVM.d.ts → router-client-DJImUDlm.d.ts} +1 -1
- package/dist/structural-rollout-MwlpgQ-6.d.ts +446 -0
- package/dist/supervise-DPmYPk0j.d.ts +131 -0
- package/dist/{types-ESeMOj94.d.ts → types-SyuwunY_.d.ts} +1 -1
- package/dist/{types-Driepl87.d.ts → types-eMNgWgFi.d.ts} +2 -2
- package/dist/{worktree-fanout-D6xR2CIA.d.ts → worktree-fanout-BDFQIO-Y.d.ts} +234 -234
- package/package.json +12 -4
- package/skills/agent-runtime-adoption/SKILL.md +74 -24
- package/skills/build-with-agent-runtime/SKILL.md +23 -20
- package/skills/loop-writer/SKILL.md +6 -5
- package/skills/supervise/SKILL.md +2 -2
- package/dist/chunk-7LO5GMAO.js.map +0 -1
- package/dist/chunk-N7EJV7N3.js.map +0 -1
- package/dist/chunk-PIPPLSOF.js.map +0 -1
- package/dist/chunk-VNOOH22O.js.map +0 -1
- package/dist/otel-export-BKmNwiCb.d.ts +0 -180
- /package/dist/{chunk-4WXGK6GV.js.map → chunk-7ON74BQO.js.map} +0 -0
- /package/dist/{chunk-5AVV7KAH.js.map → chunk-QK4DV5PR.js.map} +0 -0
- /package/dist/{chunk-BQPFZE2C.js.map → chunk-WRUSWK4F.js.map} +0 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// src/profiles/researcher.ts
|
|
2
|
+
var DEFAULT_HARNESS = "opencode/zai-coding-plan/glm-5.1";
|
|
3
|
+
var DEFAULT_CITATION_DENSITY_MIN = 0.7;
|
|
4
|
+
function researcherProfile(options = {}) {
|
|
5
|
+
const harness = options.harness ?? DEFAULT_HARNESS;
|
|
6
|
+
const name = options.name ?? `researcher-${harness}`;
|
|
7
|
+
const systemPrompt = options.systemPrompt ?? DEFAULT_RESEARCHER_SYSTEM_PROMPT;
|
|
8
|
+
const citationDensityMin = options.citationDensityMin ?? DEFAULT_CITATION_DENSITY_MIN;
|
|
9
|
+
const profile = {
|
|
10
|
+
name,
|
|
11
|
+
description: "Source-grounded research agent. Propose-don't-apply.",
|
|
12
|
+
prompt: { systemPrompt },
|
|
13
|
+
model: options.model ? { default: options.model } : void 0,
|
|
14
|
+
tools: { web_search: true, fs: true, shell: true },
|
|
15
|
+
metadata: { backendType: harness, role: "researcher" }
|
|
16
|
+
};
|
|
17
|
+
const output = { parse: parseResearcherEvents };
|
|
18
|
+
const validator = options.task ? createResearcherValidator(options.task, { citationDensityMin }) : createResearcherValidator(
|
|
19
|
+
{ question: "", knowledgeNamespace: "" },
|
|
20
|
+
{ citationDensityMin, namespaceCheck: false }
|
|
21
|
+
);
|
|
22
|
+
const agentRunSpec = {
|
|
23
|
+
name,
|
|
24
|
+
profile,
|
|
25
|
+
taskToPrompt: formatResearcherPrompt
|
|
26
|
+
};
|
|
27
|
+
return { profile, taskToPrompt: formatResearcherPrompt, output, validator, agentRunSpec };
|
|
28
|
+
}
|
|
29
|
+
function multiHarnessResearcherFanout(options = {}) {
|
|
30
|
+
const harnesses = options.harnesses && options.harnesses.length > 0 ? options.harnesses : ["opencode/zai-coding-plan/glm-5.1", "claude-code", "codex"];
|
|
31
|
+
const models = options.models ?? [];
|
|
32
|
+
const agentRuns = harnesses.map((harness, i) => {
|
|
33
|
+
const { agentRunSpec } = researcherProfile({ harness, model: models[i] });
|
|
34
|
+
return agentRunSpec;
|
|
35
|
+
});
|
|
36
|
+
const { output, validator } = researcherProfile({
|
|
37
|
+
citationDensityMin: options.citationDensityMin,
|
|
38
|
+
task: options.task
|
|
39
|
+
});
|
|
40
|
+
const driver = {
|
|
41
|
+
name: "researcher-fanout",
|
|
42
|
+
async plan(task, history) {
|
|
43
|
+
return history.length === 0 ? Array.from({ length: harnesses.length }, () => task) : [];
|
|
44
|
+
},
|
|
45
|
+
// 'done' is a terminal decision in the kernel; the loop finalizes after
|
|
46
|
+
// the single fanout round and selects the winner via `defaultSelectWinner`.
|
|
47
|
+
decide() {
|
|
48
|
+
return "done";
|
|
49
|
+
},
|
|
50
|
+
describePlan() {
|
|
51
|
+
return { kind: "fanout" };
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
return { agentRuns, output, validator, driver };
|
|
55
|
+
}
|
|
56
|
+
function createResearcherValidator(task, config = {}) {
|
|
57
|
+
const citationDensityMin = config.citationDensityMin ?? DEFAULT_CITATION_DENSITY_MIN;
|
|
58
|
+
const namespaceCheck = config.namespaceCheck ?? true;
|
|
59
|
+
return {
|
|
60
|
+
async validate(output) {
|
|
61
|
+
const notes = [];
|
|
62
|
+
const scores = {};
|
|
63
|
+
let pass = true;
|
|
64
|
+
if (!Array.isArray(output.items) || output.items.length === 0) {
|
|
65
|
+
pass = false;
|
|
66
|
+
notes.push("no items");
|
|
67
|
+
scores.items = 0;
|
|
68
|
+
} else {
|
|
69
|
+
scores.items = 1;
|
|
70
|
+
}
|
|
71
|
+
const missingEvidence = (output.items ?? []).filter(
|
|
72
|
+
(item) => !Array.isArray(item.evidence) || item.evidence.length === 0
|
|
73
|
+
);
|
|
74
|
+
if (missingEvidence.length > 0) {
|
|
75
|
+
pass = false;
|
|
76
|
+
notes.push(`${missingEvidence.length} item(s) without evidence`);
|
|
77
|
+
scores.provenance = 0;
|
|
78
|
+
} else {
|
|
79
|
+
scores.provenance = 1;
|
|
80
|
+
}
|
|
81
|
+
if (namespaceCheck) {
|
|
82
|
+
const foreignItems = (output.items ?? []).filter(
|
|
83
|
+
(item) => item.namespace !== task.knowledgeNamespace
|
|
84
|
+
);
|
|
85
|
+
const foreignWrites = (output.proposedWrites ?? []).filter(
|
|
86
|
+
(write) => write.namespace !== task.knowledgeNamespace
|
|
87
|
+
);
|
|
88
|
+
if (foreignItems.length > 0 || foreignWrites.length > 0) {
|
|
89
|
+
pass = false;
|
|
90
|
+
notes.push(
|
|
91
|
+
`namespace violation: ${foreignItems.length} item(s) + ${foreignWrites.length} write(s) outside ${task.knowledgeNamespace}`
|
|
92
|
+
);
|
|
93
|
+
scores.namespace = 0;
|
|
94
|
+
} else {
|
|
95
|
+
scores.namespace = 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const itemCount = Math.max(output.items?.length ?? 0, 1);
|
|
99
|
+
const citationsWithQuote = (output.citations ?? []).filter(
|
|
100
|
+
(citation) => typeof citation.quote === "string" && citation.quote.length > 0
|
|
101
|
+
).length;
|
|
102
|
+
const citationDensity = Math.min(1, citationsWithQuote / itemCount);
|
|
103
|
+
if (citationDensity < citationDensityMin) {
|
|
104
|
+
pass = false;
|
|
105
|
+
notes.push(
|
|
106
|
+
`citation density ${citationDensity.toFixed(2)} below floor ${citationDensityMin.toFixed(2)}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
scores.citation_density = citationDensity;
|
|
110
|
+
const sourceSet = /* @__PURE__ */ new Set();
|
|
111
|
+
for (const item of output.items ?? []) {
|
|
112
|
+
for (const evidence of item.evidence ?? []) {
|
|
113
|
+
if (evidence.source) sourceSet.add(evidence.source);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
scores.source_diversity = Math.min(1, sourceSet.size / itemCount);
|
|
117
|
+
scores.recency_match = recencyMatchScore(output.items ?? [], task.recencyWindow);
|
|
118
|
+
const maxGaps = itemCount;
|
|
119
|
+
const gapCount = output.gaps?.length ?? 0;
|
|
120
|
+
scores.gap_coverage = Math.max(0, 1 - gapCount / maxGaps);
|
|
121
|
+
const score = 0.4 * scores.citation_density + 0.2 * scores.source_diversity + 0.2 * scores.recency_match + 0.2 * scores.gap_coverage;
|
|
122
|
+
const verdict = {
|
|
123
|
+
valid: pass,
|
|
124
|
+
score: Number.isFinite(score) ? score : 0,
|
|
125
|
+
scores
|
|
126
|
+
};
|
|
127
|
+
if (notes.length > 0) verdict.notes = notes.join("; ");
|
|
128
|
+
return verdict;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function recencyMatchScore(items, window) {
|
|
133
|
+
if (!window || window.since === void 0 && window.until === void 0) return 1;
|
|
134
|
+
if (items.length === 0) return 0;
|
|
135
|
+
const sinceMs = window.since?.getTime() ?? Number.NEGATIVE_INFINITY;
|
|
136
|
+
const untilMs = window.until?.getTime() ?? Number.POSITIVE_INFINITY;
|
|
137
|
+
let hits = 0;
|
|
138
|
+
let total = 0;
|
|
139
|
+
for (const item of items) {
|
|
140
|
+
for (const evidence of item.evidence ?? []) {
|
|
141
|
+
if (typeof evidence.capturedAt !== "number") continue;
|
|
142
|
+
total += 1;
|
|
143
|
+
if (evidence.capturedAt >= sinceMs && evidence.capturedAt <= untilMs) hits += 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return total === 0 ? 0 : hits / total;
|
|
147
|
+
}
|
|
148
|
+
var DEFAULT_RESEARCHER_SYSTEM_PROMPT = [
|
|
149
|
+
"You are a research agent. Your job is to answer a research question with",
|
|
150
|
+
"source-grounded knowledge items that the caller will choose whether to",
|
|
151
|
+
"persist to a multi-tenant knowledge base.",
|
|
152
|
+
"",
|
|
153
|
+
"Hard rules:",
|
|
154
|
+
" 1. Every item you emit MUST carry the task's knowledgeNamespace exactly.",
|
|
155
|
+
" Never write to a different namespace.",
|
|
156
|
+
" 2. Every item MUST carry at least one evidence entry with a source.",
|
|
157
|
+
" A quote + url is strongly preferred; capturedAt is unix ms.",
|
|
158
|
+
" 3. You propose writes \u2014 you do NOT apply them. The caller decides.",
|
|
159
|
+
" 4. Self-report confidence honestly in [0, 1]. Do not inflate.",
|
|
160
|
+
" 5. List what you could not answer in `gaps`. Better to admit a gap",
|
|
161
|
+
" than fabricate.",
|
|
162
|
+
"",
|
|
163
|
+
"When you finish, emit a single final structured message of the shape:",
|
|
164
|
+
" ```json",
|
|
165
|
+
' { "items": [{ "id": "...", "namespace": "...", "claim": "...",',
|
|
166
|
+
' "evidence": [{ "source": "...", "quote": "...",',
|
|
167
|
+
' "url": "...", "capturedAt": 0 }],',
|
|
168
|
+
' "confidence": 0.0,',
|
|
169
|
+
' "authoredBy": { "kind": "agent", "id": "..." } }],',
|
|
170
|
+
' "citations": [{ "url": "...", "quote": "...", "confidence": 0.0 }],',
|
|
171
|
+
' "proposedWrites": [{ "kind": "insert", "namespace": "...",',
|
|
172
|
+
' "item": { /* same shape as items[] */ } }],',
|
|
173
|
+
' "gaps": ["..."],',
|
|
174
|
+
' "notes": "free-form commentary" }',
|
|
175
|
+
" ```"
|
|
176
|
+
].join("\n");
|
|
177
|
+
function formatResearcherPrompt(task) {
|
|
178
|
+
const sources = task.sources?.length ? task.sources.join(", ") : "(no preference)";
|
|
179
|
+
const window = formatRecencyWindow(task.recencyWindow);
|
|
180
|
+
return [
|
|
181
|
+
`Question: ${task.question}`,
|
|
182
|
+
`Knowledge namespace (DO NOT cross): ${task.knowledgeNamespace}`,
|
|
183
|
+
`Scope: ${task.scope ?? "(unspecified)"}`,
|
|
184
|
+
`Preferred sources: ${sources}`,
|
|
185
|
+
`Recency window: ${window}`,
|
|
186
|
+
`Max items: ${task.maxItems ?? "(no cap)"}`,
|
|
187
|
+
`Per-item minimum confidence: ${task.minConfidence ?? "(no floor)"}`,
|
|
188
|
+
"",
|
|
189
|
+
"Produce knowledge items with provenance + citations + proposed writes.",
|
|
190
|
+
"List gaps for anything you could not answer. Emit the final JSON",
|
|
191
|
+
"result block exactly as instructed."
|
|
192
|
+
].join("\n");
|
|
193
|
+
}
|
|
194
|
+
function formatRecencyWindow(window) {
|
|
195
|
+
if (!window) return "(none)";
|
|
196
|
+
const since = window.since ? window.since.toISOString() : "-\u221E";
|
|
197
|
+
const until = window.until ? window.until.toISOString() : "now";
|
|
198
|
+
return `${since} .. ${until}`;
|
|
199
|
+
}
|
|
200
|
+
function parseResearcherEvents(events) {
|
|
201
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
202
|
+
const event = events[i];
|
|
203
|
+
if (!event) continue;
|
|
204
|
+
const type = String(event.type ?? "");
|
|
205
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
206
|
+
if (type === "result" || type === "final" || type === "research.result") {
|
|
207
|
+
const direct = coerceResearchOutput(data.result ?? data.output ?? data);
|
|
208
|
+
const finalText = pickString(data.finalText);
|
|
209
|
+
const fenced = finalText ? extractFencedJson(finalText) : void 0;
|
|
210
|
+
const fromFinalText = fenced ? coerceResearchOutput(fenced) : void 0;
|
|
211
|
+
if (direct && !isEmptyOutput(direct)) return direct;
|
|
212
|
+
if (fromFinalText) return fromFinalText;
|
|
213
|
+
if (direct) return direct;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
217
|
+
const event = events[i];
|
|
218
|
+
if (!event) continue;
|
|
219
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
220
|
+
const text = pickString(data.text) ?? pickString(data.delta) ?? pickString(data.finalText);
|
|
221
|
+
if (!text) continue;
|
|
222
|
+
const fenced = extractFencedJson(text);
|
|
223
|
+
if (!fenced) continue;
|
|
224
|
+
const coerced = coerceResearchOutput(fenced);
|
|
225
|
+
if (coerced) return coerced;
|
|
226
|
+
}
|
|
227
|
+
return { items: [], citations: [], proposedWrites: [] };
|
|
228
|
+
}
|
|
229
|
+
function isRecord(value) {
|
|
230
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
231
|
+
}
|
|
232
|
+
function isEmptyOutput(o) {
|
|
233
|
+
return o.items.length === 0 && o.citations.length === 0 && o.proposedWrites.length === 0;
|
|
234
|
+
}
|
|
235
|
+
function pickString(value) {
|
|
236
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
237
|
+
}
|
|
238
|
+
function extractFencedJson(text) {
|
|
239
|
+
const match = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
240
|
+
if (!match) return void 0;
|
|
241
|
+
const body = (match[1] ?? "").trim();
|
|
242
|
+
if (!body) return void 0;
|
|
243
|
+
try {
|
|
244
|
+
return JSON.parse(body);
|
|
245
|
+
} catch {
|
|
246
|
+
return void 0;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function coerceResearchOutput(value) {
|
|
250
|
+
if (!isRecord(value)) return void 0;
|
|
251
|
+
const items = coerceItems(value.items);
|
|
252
|
+
const citations = coerceCitations(value.citations);
|
|
253
|
+
const proposedWrites = coerceProposedWrites(value.proposedWrites);
|
|
254
|
+
if (items === void 0 && citations === void 0 && proposedWrites === void 0) {
|
|
255
|
+
return void 0;
|
|
256
|
+
}
|
|
257
|
+
const output = {
|
|
258
|
+
items: items ?? [],
|
|
259
|
+
citations: citations ?? [],
|
|
260
|
+
proposedWrites: proposedWrites ?? []
|
|
261
|
+
};
|
|
262
|
+
if (Array.isArray(value.gaps)) {
|
|
263
|
+
output.gaps = value.gaps.filter((entry) => typeof entry === "string");
|
|
264
|
+
}
|
|
265
|
+
const notes = pickString(value.notes);
|
|
266
|
+
if (notes) output.notes = notes;
|
|
267
|
+
const known = /* @__PURE__ */ new Set(["items", "citations", "proposedWrites", "gaps", "notes"]);
|
|
268
|
+
const extras = {};
|
|
269
|
+
let extrasCount = 0;
|
|
270
|
+
for (const [key, val] of Object.entries(value)) {
|
|
271
|
+
if (known.has(key)) continue;
|
|
272
|
+
extras[key] = val;
|
|
273
|
+
extrasCount += 1;
|
|
274
|
+
}
|
|
275
|
+
if (extrasCount > 0) output.raw = extras;
|
|
276
|
+
return output;
|
|
277
|
+
}
|
|
278
|
+
function coerceItems(value) {
|
|
279
|
+
if (!Array.isArray(value)) return void 0;
|
|
280
|
+
const out = [];
|
|
281
|
+
for (const entry of value) {
|
|
282
|
+
if (!isRecord(entry)) continue;
|
|
283
|
+
const id = pickString(entry.id);
|
|
284
|
+
const namespace = pickString(entry.namespace);
|
|
285
|
+
const claim = pickString(entry.claim);
|
|
286
|
+
const evidence = coerceEvidence(entry.evidence);
|
|
287
|
+
const confidence = toFiniteNumber(entry.confidence);
|
|
288
|
+
const authoredBy = coerceAuthoredBy(entry.authoredBy);
|
|
289
|
+
if (!id || !namespace || !claim || !authoredBy) continue;
|
|
290
|
+
const item = {
|
|
291
|
+
id,
|
|
292
|
+
namespace,
|
|
293
|
+
claim,
|
|
294
|
+
evidence,
|
|
295
|
+
confidence: clamp01(confidence),
|
|
296
|
+
authoredBy
|
|
297
|
+
};
|
|
298
|
+
if (Array.isArray(entry.supersedes)) {
|
|
299
|
+
item.supersedes = entry.supersedes.filter((s) => typeof s === "string");
|
|
300
|
+
}
|
|
301
|
+
const retractedAt = toFiniteNumber(entry.retractedAt);
|
|
302
|
+
if (retractedAt > 0) item.retractedAt = retractedAt;
|
|
303
|
+
out.push(item);
|
|
304
|
+
}
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
function coerceEvidence(value) {
|
|
308
|
+
if (!Array.isArray(value)) return [];
|
|
309
|
+
const out = [];
|
|
310
|
+
for (const entry of value) {
|
|
311
|
+
if (!isRecord(entry)) continue;
|
|
312
|
+
const source = pickString(entry.source);
|
|
313
|
+
if (!source) continue;
|
|
314
|
+
const item = {
|
|
315
|
+
source,
|
|
316
|
+
capturedAt: toFiniteNumber(entry.capturedAt)
|
|
317
|
+
};
|
|
318
|
+
const quote = pickString(entry.quote);
|
|
319
|
+
if (quote) item.quote = quote;
|
|
320
|
+
const url = pickString(entry.url);
|
|
321
|
+
if (url) item.url = url;
|
|
322
|
+
out.push(item);
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
function coerceAuthoredBy(value) {
|
|
327
|
+
if (!isRecord(value)) return void 0;
|
|
328
|
+
const kind = value.kind === "human" || value.kind === "agent" ? value.kind : void 0;
|
|
329
|
+
const id = pickString(value.id);
|
|
330
|
+
if (!kind || !id) return void 0;
|
|
331
|
+
return { kind, id };
|
|
332
|
+
}
|
|
333
|
+
function coerceCitations(value) {
|
|
334
|
+
if (!Array.isArray(value)) return void 0;
|
|
335
|
+
const out = [];
|
|
336
|
+
for (const entry of value) {
|
|
337
|
+
if (!isRecord(entry)) continue;
|
|
338
|
+
const url = pickString(entry.url);
|
|
339
|
+
const quote = pickString(entry.quote);
|
|
340
|
+
if (!url || !quote) continue;
|
|
341
|
+
out.push({ url, quote, confidence: clamp01(toFiniteNumber(entry.confidence)) });
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
function coerceProposedWrites(value) {
|
|
346
|
+
if (!Array.isArray(value)) return void 0;
|
|
347
|
+
const out = [];
|
|
348
|
+
for (const entry of value) {
|
|
349
|
+
if (!isRecord(entry)) continue;
|
|
350
|
+
const namespace = pickString(entry.namespace);
|
|
351
|
+
if (!namespace) continue;
|
|
352
|
+
if (entry.kind === "insert") {
|
|
353
|
+
const items = coerceItems([entry.item]);
|
|
354
|
+
const item = items?.[0];
|
|
355
|
+
if (!item) continue;
|
|
356
|
+
out.push({ kind: "insert", namespace, item });
|
|
357
|
+
} else if (entry.kind === "supersede") {
|
|
358
|
+
const previousId = pickString(entry.previousId);
|
|
359
|
+
const items = coerceItems([entry.item]);
|
|
360
|
+
const item = items?.[0];
|
|
361
|
+
if (!previousId || !item) continue;
|
|
362
|
+
out.push({ kind: "supersede", namespace, previousId, item });
|
|
363
|
+
} else if (entry.kind === "retract") {
|
|
364
|
+
const itemId = pickString(entry.itemId);
|
|
365
|
+
const reason = pickString(entry.reason);
|
|
366
|
+
if (!itemId || !reason) continue;
|
|
367
|
+
out.push({ kind: "retract", namespace, itemId, reason });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
function toFiniteNumber(value) {
|
|
373
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
374
|
+
}
|
|
375
|
+
function clamp01(value) {
|
|
376
|
+
if (!Number.isFinite(value)) return 0;
|
|
377
|
+
if (value < 0) return 0;
|
|
378
|
+
if (value > 1) return 1;
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export {
|
|
383
|
+
researcherProfile,
|
|
384
|
+
multiHarnessResearcherFanout,
|
|
385
|
+
createResearcherValidator
|
|
386
|
+
};
|
|
387
|
+
//# sourceMappingURL=chunk-5QOB7H74.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/profiles/researcher.ts"],"sourcesContent":["/**\n * Opinionated preset for source-grounded research\n * tasks. The agent is told to:\n * - bound its work to a single `knowledgeNamespace`\n * - emit `items[]` carrying provenance + confidence\n * - emit `citations[]` linking quotes back to source urls\n * - emit `proposedWrites[]` — never call materialize itself\n * - describe `gaps` it could not answer\n *\n * The profile is stateless and agent-agnostic. `harness` selects the\n * sandbox-SDK backend. For heterogeneous fanout, use\n * `multiHarnessResearcherFanout`.\n *\n * Propose-don't-apply: the profile NEVER writes to the knowledge base.\n * It produces `proposedWrites: KnowledgeUpdate[]` in the output. The\n * caller (gtm-agent, journey-eval, user) decides whether to feed those\n * updates through `applyKnowledgeWriteBlocks` / a KbStore put.\n *\n * Namespace isolation: every `KnowledgeItem` + `KnowledgeUpdate` in the\n * output carries `namespace`. The validator hard-fails when any item\n * touches a namespace other than `task.knowledgeNamespace`.\n *\n * @experimental\n */\n\nimport type { AgentProfile, SandboxEvent } from '@tangle-network/sandbox'\nimport type {\n AgentRunSpec,\n DefaultVerdict,\n Driver,\n OutputAdapter,\n Validator,\n} from '../runtime/types'\n\n/** Source families a researcher profile may prefer for a task. @experimental */\nexport type ResearchSource = 'web' | 'corpus' | 'twitter' | 'github' | 'docs'\n\n/** Task contract for a source-grounded research agent. @experimental */\nexport interface ResearchTask {\n /** The research question to answer. */\n question: string\n /** Bound: e.g. \"audience for cpg-founder ICP\". */\n scope?: string\n /** Multi-tenant scope (customer-id, workspace-id). Validator enforces. */\n knowledgeNamespace: string\n sources?: ResearchSource[]\n recencyWindow?: { since?: Date; until?: Date }\n maxItems?: number\n /** Per-item minimum confidence in [0, 1]. Validator scores recall vs this. */\n minConfidence?: number\n}\n\n/**\n * Knowledge item emitted by the researcher.\n *\n * Profile-local type. When agent-knowledge promotes `KnowledgeClaim` →\n * top-level `KnowledgeItem` substrate-wide, these fields collapse 1:1.\n *\n * @experimental\n */\nexport interface KnowledgeItem {\n id: string\n /** Multi-tenant scope. MUST equal `task.knowledgeNamespace`. */\n namespace: string\n /** The factual claim, in the researcher's words. */\n claim: string\n /** Provenance — at least one entry required. */\n evidence: Array<{ source: string; quote?: string; url?: string; capturedAt: number }>\n /** Researcher's self-reported confidence in [0, 1]. */\n confidence: number\n /** Prior item ids this supersedes (chain). */\n supersedes?: string[]\n /** Set if the agent is retracting an earlier item. Unix ms. */\n retractedAt?: number\n authoredBy: { kind: 'human' | 'agent'; id: string }\n}\n\n/**\n * A proposed write to the knowledge base. The profile does NOT apply\n * these — the caller decides.\n *\n * @experimental\n */\nexport type KnowledgeUpdate =\n | { kind: 'insert'; namespace: string; item: KnowledgeItem }\n | { kind: 'supersede'; namespace: string; previousId: string; item: KnowledgeItem }\n | { kind: 'retract'; namespace: string; itemId: string; reason: string }\n\n/**\n * Researcher output. Required fields are typed; optional fields preserve\n * the agent's free-form intelligence (`notes`, `raw`). The validator\n * enforces the typed minimum.\n *\n * @experimental\n */\nexport interface ResearchOutput {\n items: KnowledgeItem[]\n citations: Array<{ url: string; quote: string; confidence: number }>\n proposedWrites: KnowledgeUpdate[]\n gaps?: string[]\n notes?: string\n /** Anything the agent emitted beyond the typed fields. */\n raw?: unknown\n}\n\n/** Options for the source-grounded researcher profile preset. @experimental */\nexport interface ResearcherProfileOptions {\n /** Sandbox-SDK backend.type. Default `'opencode/zai-coding-plan/glm-5.1'`. */\n harness?: string\n /** Default model id passed in `AgentProfile.model.default`. */\n model?: string\n /** Custom system prompt replacement. Default = built-in researcher preset. */\n systemPrompt?: string\n /** Stable name for `AgentRunSpec.name`. Default = `researcher-${harness}`. */\n name?: string\n /**\n * Default 0.7. Minimum (citations with quote) / items ratio for `valid=true`.\n * Below this floor, citation_density scores < 1 and the item set is gated.\n */\n citationDensityMin?: number\n}\n\nconst DEFAULT_HARNESS = 'opencode/zai-coding-plan/glm-5.1'\nconst DEFAULT_CITATION_DENSITY_MIN = 0.7\n\n/** Build a source-grounded researcher profile with output parsing and validation. @experimental */\nexport function researcherProfile(\n options: ResearcherProfileOptions & { task?: ResearchTask } = {},\n): {\n profile: AgentProfile\n taskToPrompt: (task: ResearchTask) => string\n output: OutputAdapter<ResearchOutput>\n validator: Validator<ResearchOutput>\n agentRunSpec: AgentRunSpec<ResearchTask>\n} {\n const harness = options.harness ?? DEFAULT_HARNESS\n const name = options.name ?? `researcher-${harness}`\n const systemPrompt = options.systemPrompt ?? DEFAULT_RESEARCHER_SYSTEM_PROMPT\n const citationDensityMin = options.citationDensityMin ?? DEFAULT_CITATION_DENSITY_MIN\n const profile: AgentProfile = {\n name,\n description: \"Source-grounded research agent. Propose-don't-apply.\",\n prompt: { systemPrompt },\n model: options.model ? { default: options.model } : undefined,\n tools: { web_search: true, fs: true, shell: true },\n metadata: { backendType: harness, role: 'researcher' },\n }\n const output: OutputAdapter<ResearchOutput> = { parse: parseResearcherEvents }\n const validator: Validator<ResearchOutput> = options.task\n ? createResearcherValidator(options.task, { citationDensityMin })\n : createResearcherValidator(\n { question: '', knowledgeNamespace: '' },\n { citationDensityMin, namespaceCheck: false },\n )\n const agentRunSpec: AgentRunSpec<ResearchTask> = {\n name,\n profile,\n taskToPrompt: formatResearcherPrompt,\n }\n return { profile, taskToPrompt: formatResearcherPrompt, output, validator, agentRunSpec }\n}\n\n/** @experimental */\nexport interface MultiHarnessResearcherFanoutOptions {\n /** Backend.type identifiers, one per parallel agent. */\n harnesses?: string[]\n /** Optional per-harness model override. Indexed parallel to `harnesses`. */\n models?: (string | undefined)[]\n /** Default citation density floor for the shared validator. */\n citationDensityMin?: number\n /** Optional task — narrows the validator's namespace check. */\n task?: ResearchTask\n}\n\n/**\n * Build a fanout topology over multiple harnesses. The kernel round-robins\n * `agentRuns` across the N parallel iterations and the `FanoutVote` driver\n * picks the highest-scoring valid output.\n *\n * @experimental\n */\nexport function multiHarnessResearcherFanout(options: MultiHarnessResearcherFanoutOptions = {}): {\n agentRuns: AgentRunSpec<ResearchTask>[]\n output: OutputAdapter<ResearchOutput>\n validator: Validator<ResearchOutput>\n driver: Driver<ResearchTask, ResearchOutput, 'done'>\n} {\n const harnesses =\n options.harnesses && options.harnesses.length > 0\n ? options.harnesses\n : ['opencode/zai-coding-plan/glm-5.1', 'claude-code', 'codex']\n const models = options.models ?? []\n const agentRuns = harnesses.map((harness, i) => {\n const { agentRunSpec } = researcherProfile({ harness, model: models[i] })\n return agentRunSpec\n })\n const { output, validator } = researcherProfile({\n citationDensityMin: options.citationDensityMin,\n task: options.task,\n })\n // Single fanout round across the N harnesses, then stop: the kernel\n // round-robins `agentRuns` over the N branches and selects the winner\n // (best valid score) across all iterations via `defaultSelectWinner`.\n const driver: Driver<ResearchTask, ResearchOutput, 'done'> = {\n name: 'researcher-fanout',\n async plan(task, history) {\n return history.length === 0 ? Array.from({ length: harnesses.length }, () => task) : []\n },\n // 'done' is a terminal decision in the kernel; the loop finalizes after\n // the single fanout round and selects the winner via `defaultSelectWinner`.\n decide() {\n return 'done'\n },\n describePlan() {\n return { kind: 'fanout' }\n },\n }\n return { agentRuns, output, validator, driver }\n}\n\n/**\n * Build a validator that closes over a specific `ResearchTask`'s constraints.\n *\n * Checks in order:\n * 1. Items must be non-empty.\n * 2. Every item carries `evidence.length >= 1`.\n * 3. Every item + proposedWrite is scoped to `task.knowledgeNamespace`\n * (hard-fail on any namespace mismatch — defence in depth for the\n * multi-tenant invariant).\n * 4. Citation density (citations with quote / items) >= floor.\n *\n * Aggregate score:\n * 0.4 · citation_density\n * + 0.2 · source_diversity (distinct sources / max(items, 1))\n * + 0.2 · recency_match (mean fraction within `recencyWindow`)\n * + 0.2 · (1 − gaps/maxGaps), maxGaps = max(items, 1)\n *\n * @experimental\n */\nexport function createResearcherValidator(\n task: ResearchTask,\n config: { citationDensityMin?: number; namespaceCheck?: boolean } = {},\n): Validator<ResearchOutput> {\n const citationDensityMin = config.citationDensityMin ?? DEFAULT_CITATION_DENSITY_MIN\n const namespaceCheck = config.namespaceCheck ?? true\n return {\n async validate(output) {\n const notes: string[] = []\n const scores: Record<string, number> = {}\n let pass = true\n\n if (!Array.isArray(output.items) || output.items.length === 0) {\n pass = false\n notes.push('no items')\n scores.items = 0\n } else {\n scores.items = 1\n }\n\n const missingEvidence = (output.items ?? []).filter(\n (item) => !Array.isArray(item.evidence) || item.evidence.length === 0,\n )\n if (missingEvidence.length > 0) {\n pass = false\n notes.push(`${missingEvidence.length} item(s) without evidence`)\n scores.provenance = 0\n } else {\n scores.provenance = 1\n }\n\n if (namespaceCheck) {\n const foreignItems = (output.items ?? []).filter(\n (item) => item.namespace !== task.knowledgeNamespace,\n )\n const foreignWrites = (output.proposedWrites ?? []).filter(\n (write) => write.namespace !== task.knowledgeNamespace,\n )\n if (foreignItems.length > 0 || foreignWrites.length > 0) {\n pass = false\n notes.push(\n `namespace violation: ${foreignItems.length} item(s) + ${foreignWrites.length} write(s) ` +\n `outside ${task.knowledgeNamespace}`,\n )\n scores.namespace = 0\n } else {\n scores.namespace = 1\n }\n }\n\n const itemCount = Math.max(output.items?.length ?? 0, 1)\n const citationsWithQuote = (output.citations ?? []).filter(\n (citation) => typeof citation.quote === 'string' && citation.quote.length > 0,\n ).length\n const citationDensity = Math.min(1, citationsWithQuote / itemCount)\n if (citationDensity < citationDensityMin) {\n pass = false\n notes.push(\n `citation density ${citationDensity.toFixed(2)} below floor ${citationDensityMin.toFixed(2)}`,\n )\n }\n scores.citation_density = citationDensity\n\n const sourceSet = new Set<string>()\n for (const item of output.items ?? []) {\n for (const evidence of item.evidence ?? []) {\n if (evidence.source) sourceSet.add(evidence.source)\n }\n }\n scores.source_diversity = Math.min(1, sourceSet.size / itemCount)\n\n scores.recency_match = recencyMatchScore(output.items ?? [], task.recencyWindow)\n\n const maxGaps = itemCount\n const gapCount = output.gaps?.length ?? 0\n scores.gap_coverage = Math.max(0, 1 - gapCount / maxGaps)\n\n const score =\n 0.4 * scores.citation_density +\n 0.2 * scores.source_diversity +\n 0.2 * scores.recency_match +\n 0.2 * scores.gap_coverage\n\n const verdict: DefaultVerdict = {\n valid: pass,\n score: Number.isFinite(score) ? score : 0,\n scores,\n }\n if (notes.length > 0) verdict.notes = notes.join('; ')\n return verdict\n },\n }\n}\n\nfunction recencyMatchScore(items: KnowledgeItem[], window: ResearchTask['recencyWindow']): number {\n if (!window || (window.since === undefined && window.until === undefined)) return 1\n if (items.length === 0) return 0\n const sinceMs = window.since?.getTime() ?? Number.NEGATIVE_INFINITY\n const untilMs = window.until?.getTime() ?? Number.POSITIVE_INFINITY\n let hits = 0\n let total = 0\n for (const item of items) {\n for (const evidence of item.evidence ?? []) {\n if (typeof evidence.capturedAt !== 'number') continue\n total += 1\n if (evidence.capturedAt >= sinceMs && evidence.capturedAt <= untilMs) hits += 1\n }\n }\n return total === 0 ? 0 : hits / total\n}\n\nconst DEFAULT_RESEARCHER_SYSTEM_PROMPT = [\n 'You are a research agent. Your job is to answer a research question with',\n 'source-grounded knowledge items that the caller will choose whether to',\n 'persist to a multi-tenant knowledge base.',\n '',\n 'Hard rules:',\n \" 1. Every item you emit MUST carry the task's knowledgeNamespace exactly.\",\n ' Never write to a different namespace.',\n ' 2. Every item MUST carry at least one evidence entry with a source.',\n ' A quote + url is strongly preferred; capturedAt is unix ms.',\n ' 3. You propose writes — you do NOT apply them. The caller decides.',\n ' 4. Self-report confidence honestly in [0, 1]. Do not inflate.',\n ' 5. List what you could not answer in `gaps`. Better to admit a gap',\n ' than fabricate.',\n '',\n 'When you finish, emit a single final structured message of the shape:',\n ' ```json',\n ' { \"items\": [{ \"id\": \"...\", \"namespace\": \"...\", \"claim\": \"...\",',\n ' \"evidence\": [{ \"source\": \"...\", \"quote\": \"...\",',\n ' \"url\": \"...\", \"capturedAt\": 0 }],',\n ' \"confidence\": 0.0,',\n ' \"authoredBy\": { \"kind\": \"agent\", \"id\": \"...\" } }],',\n ' \"citations\": [{ \"url\": \"...\", \"quote\": \"...\", \"confidence\": 0.0 }],',\n ' \"proposedWrites\": [{ \"kind\": \"insert\", \"namespace\": \"...\",',\n ' \"item\": { /* same shape as items[] */ } }],',\n ' \"gaps\": [\"...\"],',\n ' \"notes\": \"free-form commentary\" }',\n ' ```',\n].join('\\n')\n\nfunction formatResearcherPrompt(task: ResearchTask): string {\n const sources = task.sources?.length ? task.sources.join(', ') : '(no preference)'\n const window = formatRecencyWindow(task.recencyWindow)\n return [\n `Question: ${task.question}`,\n `Knowledge namespace (DO NOT cross): ${task.knowledgeNamespace}`,\n `Scope: ${task.scope ?? '(unspecified)'}`,\n `Preferred sources: ${sources}`,\n `Recency window: ${window}`,\n `Max items: ${task.maxItems ?? '(no cap)'}`,\n `Per-item minimum confidence: ${task.minConfidence ?? '(no floor)'}`,\n '',\n 'Produce knowledge items with provenance + citations + proposed writes.',\n 'List gaps for anything you could not answer. Emit the final JSON',\n 'result block exactly as instructed.',\n ].join('\\n')\n}\n\nfunction formatRecencyWindow(window: ResearchTask['recencyWindow']): string {\n if (!window) return '(none)'\n const since = window.since ? window.since.toISOString() : '-∞'\n const until = window.until ? window.until.toISOString() : 'now'\n return `${since} .. ${until}`\n}\n\n/**\n * Walk the event stream and return the last structured `research.result`\n * payload. Falls back to scanning text deltas for a fenced JSON block.\n */\nfunction parseResearcherEvents(events: SandboxEvent[]): ResearchOutput {\n for (let i = events.length - 1; i >= 0; i -= 1) {\n const event = events[i]\n if (!event) continue\n const type = String(event.type ?? '')\n const data = isRecord(event.data) ? event.data : {}\n if (type === 'result' || type === 'final' || type === 'research.result') {\n const direct = coerceResearchOutput(data.result ?? data.output ?? data)\n // opencode reports the agent's terminal answer in `finalText` (not result/output).\n // Parse it too, and prefer it when `direct` is an all-empty shape — otherwise an\n // event carrying both `{items:[],citations:[],proposedWrites:[]}` and a rich\n // finalText would silently drop the real answer.\n const finalText = pickString(data.finalText)\n const fenced = finalText ? extractFencedJson(finalText) : undefined\n const fromFinalText = fenced ? coerceResearchOutput(fenced) : undefined\n if (direct && !isEmptyOutput(direct)) return direct\n if (fromFinalText) return fromFinalText\n if (direct) return direct\n }\n }\n for (let i = events.length - 1; i >= 0; i -= 1) {\n const event = events[i]\n if (!event) continue\n const data = isRecord(event.data) ? event.data : {}\n const text = pickString(data.text) ?? pickString(data.delta) ?? pickString(data.finalText)\n if (!text) continue\n const fenced = extractFencedJson(text)\n if (!fenced) continue\n const coerced = coerceResearchOutput(fenced)\n if (coerced) return coerced\n }\n return { items: [], citations: [], proposedWrites: [] }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\n/** A coerced output that carries no items, citations, or proposed writes. */\nfunction isEmptyOutput(o: ResearchOutput): boolean {\n return o.items.length === 0 && o.citations.length === 0 && o.proposedWrites.length === 0\n}\n\nfunction pickString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\nfunction extractFencedJson(text: string): unknown | undefined {\n const match = text.match(/```(?:json)?\\s*([\\s\\S]*?)```/i)\n if (!match) return undefined\n const body = (match[1] ?? '').trim()\n if (!body) return undefined\n try {\n return JSON.parse(body)\n } catch {\n return undefined\n }\n}\n\nfunction coerceResearchOutput(value: unknown): ResearchOutput | undefined {\n if (!isRecord(value)) return undefined\n const items = coerceItems(value.items)\n const citations = coerceCitations(value.citations)\n const proposedWrites = coerceProposedWrites(value.proposedWrites)\n // Reject completely empty payloads — those signal \"no result block\",\n // not \"a valid empty result\".\n if (items === undefined && citations === undefined && proposedWrites === undefined) {\n return undefined\n }\n const output: ResearchOutput = {\n items: items ?? [],\n citations: citations ?? [],\n proposedWrites: proposedWrites ?? [],\n }\n if (Array.isArray(value.gaps)) {\n output.gaps = value.gaps.filter((entry): entry is string => typeof entry === 'string')\n }\n const notes = pickString(value.notes)\n if (notes) output.notes = notes\n // Preserve any extra fields the agent emitted that don't fit the typed\n // surface — callers can inspect `raw` without forcing them through the\n // typed coercion. We only include `raw` when the agent emitted fields\n // beyond the known set.\n const known = new Set(['items', 'citations', 'proposedWrites', 'gaps', 'notes'])\n const extras: Record<string, unknown> = {}\n let extrasCount = 0\n for (const [key, val] of Object.entries(value)) {\n if (known.has(key)) continue\n extras[key] = val\n extrasCount += 1\n }\n if (extrasCount > 0) output.raw = extras\n return output\n}\n\nfunction coerceItems(value: unknown): KnowledgeItem[] | undefined {\n if (!Array.isArray(value)) return undefined\n const out: KnowledgeItem[] = []\n for (const entry of value) {\n if (!isRecord(entry)) continue\n const id = pickString(entry.id)\n const namespace = pickString(entry.namespace)\n const claim = pickString(entry.claim)\n const evidence = coerceEvidence(entry.evidence)\n const confidence = toFiniteNumber(entry.confidence)\n const authoredBy = coerceAuthoredBy(entry.authoredBy)\n if (!id || !namespace || !claim || !authoredBy) continue\n const item: KnowledgeItem = {\n id,\n namespace,\n claim,\n evidence,\n confidence: clamp01(confidence),\n authoredBy,\n }\n if (Array.isArray(entry.supersedes)) {\n item.supersedes = entry.supersedes.filter((s): s is string => typeof s === 'string')\n }\n const retractedAt = toFiniteNumber(entry.retractedAt)\n if (retractedAt > 0) item.retractedAt = retractedAt\n out.push(item)\n }\n return out\n}\n\nfunction coerceEvidence(value: unknown): KnowledgeItem['evidence'] {\n if (!Array.isArray(value)) return []\n const out: KnowledgeItem['evidence'] = []\n for (const entry of value) {\n if (!isRecord(entry)) continue\n const source = pickString(entry.source)\n if (!source) continue\n const item: KnowledgeItem['evidence'][number] = {\n source,\n capturedAt: toFiniteNumber(entry.capturedAt),\n }\n const quote = pickString(entry.quote)\n if (quote) item.quote = quote\n const url = pickString(entry.url)\n if (url) item.url = url\n out.push(item)\n }\n return out\n}\n\nfunction coerceAuthoredBy(value: unknown): KnowledgeItem['authoredBy'] | undefined {\n if (!isRecord(value)) return undefined\n const kind = value.kind === 'human' || value.kind === 'agent' ? value.kind : undefined\n const id = pickString(value.id)\n if (!kind || !id) return undefined\n return { kind, id }\n}\n\nfunction coerceCitations(value: unknown): ResearchOutput['citations'] | undefined {\n if (!Array.isArray(value)) return undefined\n const out: ResearchOutput['citations'] = []\n for (const entry of value) {\n if (!isRecord(entry)) continue\n const url = pickString(entry.url)\n const quote = pickString(entry.quote)\n if (!url || !quote) continue\n out.push({ url, quote, confidence: clamp01(toFiniteNumber(entry.confidence)) })\n }\n return out\n}\n\nfunction coerceProposedWrites(value: unknown): KnowledgeUpdate[] | undefined {\n if (!Array.isArray(value)) return undefined\n const out: KnowledgeUpdate[] = []\n for (const entry of value) {\n if (!isRecord(entry)) continue\n const namespace = pickString(entry.namespace)\n if (!namespace) continue\n if (entry.kind === 'insert') {\n const items = coerceItems([entry.item])\n const item = items?.[0]\n if (!item) continue\n out.push({ kind: 'insert', namespace, item })\n } else if (entry.kind === 'supersede') {\n const previousId = pickString(entry.previousId)\n const items = coerceItems([entry.item])\n const item = items?.[0]\n if (!previousId || !item) continue\n out.push({ kind: 'supersede', namespace, previousId, item })\n } else if (entry.kind === 'retract') {\n const itemId = pickString(entry.itemId)\n const reason = pickString(entry.reason)\n if (!itemId || !reason) continue\n out.push({ kind: 'retract', namespace, itemId, reason })\n }\n }\n return out\n}\n\nfunction toFiniteNumber(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0\n}\n\nfunction clamp01(value: number): number {\n if (!Number.isFinite(value)) return 0\n if (value < 0) return 0\n if (value > 1) return 1\n return value\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildLoopOtelSpans,
|
|
3
3
|
createOtelExporter
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-IVGYLCFH.js";
|
|
5
5
|
|
|
6
6
|
// src/mcp/trace-propagation.ts
|
|
7
7
|
function readTraceContextFromEnv() {
|
|
@@ -49,4 +49,4 @@ export {
|
|
|
49
49
|
createPropagatingTraceEmitter,
|
|
50
50
|
traceContextToEnv
|
|
51
51
|
};
|
|
52
|
-
//# sourceMappingURL=chunk-
|
|
52
|
+
//# sourceMappingURL=chunk-7ON74BQO.js.map
|
|
@@ -3,7 +3,9 @@ import { spawn } from "child_process";
|
|
|
3
3
|
var HARNESS_INVOCATIONS = {
|
|
4
4
|
claude: {
|
|
5
5
|
command: "claude",
|
|
6
|
-
|
|
6
|
+
// `-p` IS headless/print mode; the old `--headless` flag was removed from the CLI.
|
|
7
|
+
// Permission bypass is an explicit per-run opt-in below, never the public default.
|
|
8
|
+
buildArgs: (taskPrompt) => ["-p", taskPrompt],
|
|
7
9
|
modelArgs: (model) => ["-m", model]
|
|
8
10
|
},
|
|
9
11
|
codex: {
|
|
@@ -17,7 +19,14 @@ var HARNESS_INVOCATIONS = {
|
|
|
17
19
|
modelArgs: (model) => ["-m", model]
|
|
18
20
|
}
|
|
19
21
|
};
|
|
20
|
-
function
|
|
22
|
+
function buildHarnessArgs(harness, taskPrompt, options = {}) {
|
|
23
|
+
const args = HARNESS_INVOCATIONS[harness].buildArgs(taskPrompt);
|
|
24
|
+
if (harness === "claude" && options.dangerouslySkipPermissions) {
|
|
25
|
+
args.push("--dangerously-skip-permissions");
|
|
26
|
+
}
|
|
27
|
+
return args;
|
|
28
|
+
}
|
|
29
|
+
function harnessInvocation(harness, profile, taskPrompt, options = {}) {
|
|
21
30
|
const invocation = HARNESS_INVOCATIONS[harness];
|
|
22
31
|
if (!invocation) {
|
|
23
32
|
throw new Error(`harnessInvocation: unknown harness ${String(harness)}`);
|
|
@@ -26,7 +35,7 @@ function harnessInvocation(harness, profile, taskPrompt) {
|
|
|
26
35
|
const composedPrompt = typeof systemPrompt === "string" && systemPrompt.trim().length > 0 ? `${systemPrompt}
|
|
27
36
|
|
|
28
37
|
${taskPrompt}` : taskPrompt;
|
|
29
|
-
const args =
|
|
38
|
+
const args = buildHarnessArgs(harness, composedPrompt, options);
|
|
30
39
|
const model = profile.model?.default;
|
|
31
40
|
if (typeof model === "string" && model.length > 0) {
|
|
32
41
|
args.push(...invocation.modelArgs(model));
|
|
@@ -45,7 +54,7 @@ function runLocalHarness(options) {
|
|
|
45
54
|
}
|
|
46
55
|
const startedAt = Date.now();
|
|
47
56
|
const command = options.invocation?.command ?? invocation.command;
|
|
48
|
-
const args = options.invocation ? [...options.invocation.args] :
|
|
57
|
+
const args = options.invocation ? [...options.invocation.args] : buildHarnessArgs(harness, taskPrompt, options);
|
|
49
58
|
return new Promise((resolve, reject) => {
|
|
50
59
|
let child;
|
|
51
60
|
try {
|
|
@@ -111,4 +120,4 @@ export {
|
|
|
111
120
|
harnessInvocation,
|
|
112
121
|
runLocalHarness
|
|
113
122
|
};
|
|
114
|
-
//# sourceMappingURL=chunk-
|
|
123
|
+
//# sourceMappingURL=chunk-CMYMTRGA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mcp/local-harness.ts"],"sourcesContent":["/**\n *\n * Subprocess wrappers for the local coding-harness CLIs installed in the\n * sandbox image (claude-code, codex, opencode). Used by the in-process\n * delegation executor (`createInProcessExecutor`) so a delegated coding task\n * spawns a real harness on a real git worktree instead of provisioning a\n * sibling sandbox.\n *\n * All harness invocations:\n * - run with `cwd` set to the worktree\n * - inherit env from the parent (the MCP server inside the sandbox has\n * the harness's auth already)\n * - capture stdout/stderr\n * - support cancellation via AbortSignal\n * - enforce a wall-clock timeout\n *\n * @experimental\n */\n\nimport { type ChildProcess, spawn } from 'node:child_process'\nimport type { AgentProfile } from '@tangle-network/agent-interface'\n\n/** Local coding harness available inside the sandbox. */\nexport type LocalHarness = 'claude' | 'codex' | 'opencode'\n\n/**\n * Default per-harness command + arg shape. `buildArgs` takes ONLY the task prompt and\n * emits the prompt-only invocation (no model, no system prompt) — the safe default shape\n * the in-process executor's `streamPrompt` drives. `modelArgs` maps a resolved model to\n * the harness's selector flag (every supported harness takes `-m <model>`). The §1.5\n * profile-aware mapper `harnessInvocation` composes these to thread the full\n * supervisor-authored profile (systemPrompt + model) into argv.\n */\nconst HARNESS_INVOCATIONS: Record<\n LocalHarness,\n {\n command: string\n buildArgs: (taskPrompt: string) => string[]\n /** Map a resolved model to the harness's model-selector flag. */\n modelArgs: (model: string) => string[]\n }\n> = {\n claude: {\n command: 'claude',\n // `-p` IS headless/print mode; the old `--headless` flag was removed from the CLI.\n // Permission bypass is an explicit per-run opt-in below, never the public default.\n buildArgs: (taskPrompt) => ['-p', taskPrompt],\n modelArgs: (model) => ['-m', model],\n },\n codex: {\n command: 'codex',\n buildArgs: (taskPrompt) => ['run', taskPrompt],\n modelArgs: (model) => ['-m', model],\n },\n opencode: {\n command: 'opencode',\n buildArgs: (taskPrompt) => ['run', taskPrompt],\n modelArgs: (model) => ['-m', model],\n },\n}\n\n/** Result of mapping an `AgentProfile` + task prompt onto a harness invocation. */\nexport interface HarnessInvocation {\n command: string\n args: string[]\n}\n\nexport interface HarnessInvocationOptions {\n /** Allow an unattended Claude process to edit its isolated candidate worktree.\n * Ignored by harnesses that do not use Claude's permission prompt. */\n dangerouslySkipPermissions?: boolean\n}\n\nfunction buildHarnessArgs(\n harness: LocalHarness,\n taskPrompt: string,\n options: HarnessInvocationOptions = {},\n): string[] {\n const args = HARNESS_INVOCATIONS[harness].buildArgs(taskPrompt)\n if (harness === 'claude' && options.dangerouslySkipPermissions) {\n args.push('--dangerously-skip-permissions')\n }\n return args\n}\n\n/**\n * Map a supervisor-authored `AgentProfile` + the per-task prompt onto a concrete harness\n * `command` + `args` (the §1.5 fix). UNLIKE the prompt-only `HARNESS_INVOCATIONS.buildArgs`\n * — which drops both the authored model and the system prompt — this threads the FULL\n * profile payload into argv:\n *\n * - `profile.prompt.systemPrompt` → the PROMPT channel: a portable, harness-agnostic\n * default that prepends the system prompt above the task prompt (`<system>\\n\\n<task>`),\n * so the authored standing instructions reach EVERY harness (none of the three CLIs\n * expose a portable replace-system-prompt flag for a one-shot non-interactive run).\n * - `profile.model.default` → the harness's `-m <model>` selector.\n *\n * The task prompt alone is the floor; an empty/absent profile yields exactly the legacy\n * `buildArgs(taskPrompt)` shape so existing callers are byte-identical.\n */\nexport function harnessInvocation(\n harness: LocalHarness,\n profile: AgentProfile,\n taskPrompt: string,\n options: HarnessInvocationOptions = {},\n): HarnessInvocation {\n const invocation = HARNESS_INVOCATIONS[harness]\n if (!invocation) {\n throw new Error(`harnessInvocation: unknown harness ${String(harness)}`)\n }\n\n const systemPrompt = profile.prompt?.systemPrompt\n const composedPrompt =\n typeof systemPrompt === 'string' && systemPrompt.trim().length > 0\n ? `${systemPrompt}\\n\\n${taskPrompt}`\n : taskPrompt\n\n const args = buildHarnessArgs(harness, composedPrompt, options)\n\n const model = profile.model?.default\n if (typeof model === 'string' && model.length > 0) {\n args.push(...invocation.modelArgs(model))\n }\n\n return { command: invocation.command, args }\n}\n\n/** @experimental */\nexport interface RunLocalHarnessOptions {\n harness: LocalHarness\n /** Working directory for the subprocess (typically a worktree path). */\n cwd: string\n /** Prompt forwarded as the harness CLI's task argument. */\n taskPrompt: string\n /**\n * Pre-built command + args (e.g. from `harnessInvocation` so the full authored\n * `AgentProfile` — systemPrompt + model — reaches the harness). When set it OVERRIDES the\n * default prompt-only `buildArgs(taskPrompt)` path; `command` defaults to the harness's\n * default binary when only `args` is supplied. When absent the legacy prompt-only shape\n * is used unchanged.\n */\n invocation?: { command?: string; args: ReadonlyArray<string> }\n /** Allow autonomous Claude edits without an interactive permission prompt.\n * Use only when `cwd` is an isolated candidate worktree. */\n dangerouslySkipPermissions?: boolean\n /** Wall-clock kill deadline (ms). Default 5 min. Subprocess SIGTERMed on expiry. */\n timeoutMs?: number\n /** Caller cancellation. SIGTERM is sent on abort. */\n signal?: AbortSignal\n /** Override env (defaults to inheriting from the parent). */\n env?: NodeJS.ProcessEnv\n /**\n * Test seam — inject a custom spawner so unit tests can mock the\n * subprocess without touching the OS. Defaults to node's `child_process.spawn`.\n */\n spawn?: (\n command: string,\n args: ReadonlyArray<string>,\n opts: {\n cwd: string\n env: NodeJS.ProcessEnv\n stdio: 'pipe'\n },\n ) => ChildProcess\n}\n\n/** @experimental */\nexport interface LocalHarnessResult {\n /** OS exit code. `null` when killed before exit. */\n exitCode: number | null\n /** Concatenated stdout. */\n stdout: string\n /** Concatenated stderr. */\n stderr: string\n /** Set when the process exited via signal (timeout / abort). */\n killedBySignal: NodeJS.Signals | null\n /** Wall-clock duration ms (spawn → exit). */\n durationMs: number\n /** Set when timeoutMs elapsed before exit. */\n timedOut: boolean\n}\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60 * 1000\n\n/**\n * Spawn a local coding harness CLI as a subprocess + collect its output.\n *\n * NOT responsible for parsing the harness's output or extracting a diff —\n * the in-process executor's `streamPrompt` orchestrates `git diff` against\n * the worktree after this resolves. This function is intentionally narrow:\n * spawn, wait, capture, return.\n *\n * Fails loud — throws when:\n * - `cwd` doesn't exist (subprocess emits ENOENT; surfaced as Error)\n * - the harness binary is not on PATH (ENOENT)\n *\n * Does NOT throw when:\n * - the subprocess exits non-zero (`result.exitCode` carries the code)\n * - the subprocess is aborted / timed out (`result.killedBySignal` /\n * `result.timedOut` carries the reason)\n *\n * @experimental\n */\nexport function runLocalHarness(options: RunLocalHarnessOptions): Promise<LocalHarnessResult> {\n const { harness, cwd, taskPrompt } = options\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const env = options.env ?? process.env\n const spawnImpl = options.spawn ?? spawn\n\n const invocation = HARNESS_INVOCATIONS[harness]\n if (!invocation) {\n return Promise.reject(new Error(`runLocalHarness: unknown harness ${String(harness)}`))\n }\n\n const startedAt = Date.now()\n const command = options.invocation?.command ?? invocation.command\n const args = options.invocation\n ? [...options.invocation.args]\n : buildHarnessArgs(harness, taskPrompt, options)\n\n return new Promise<LocalHarnessResult>((resolve, reject) => {\n let child: ChildProcess\n try {\n child = spawnImpl(command, args, { cwd, env, stdio: 'pipe' })\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n return\n }\n\n // The harness takes its task as an argv arg, not on stdin. Leaving stdin\n // OPEN makes a non-TTY `opencode run` (and likely the other harnesses)\n // BLOCK forever waiting on input — zero output, SIGTERM at the wall cap,\n // empty patch -> \"no candidate passed validation\". Close stdin so the\n // subprocess sees EOF and proceeds (the `cliExecutor` leaf does the same).\n child.stdin?.end()\n\n let stdout = ''\n let stderr = ''\n let timedOut = false\n let settled = false\n\n const timer =\n timeoutMs > 0\n ? setTimeout(() => {\n timedOut = true\n if (!child.killed) child.kill('SIGTERM')\n }, timeoutMs)\n : null\n if (timer && typeof (timer as { unref?: () => void }).unref === 'function') {\n ;(timer as { unref: () => void }).unref()\n }\n\n const onAbort = () => {\n if (!child.killed) child.kill('SIGTERM')\n }\n if (options.signal) {\n if (options.signal.aborted) onAbort()\n else options.signal.addEventListener('abort', onAbort, { once: true })\n }\n\n child.stdout?.on('data', (chunk) => {\n stdout += String(chunk)\n })\n child.stderr?.on('data', (chunk) => {\n stderr += String(chunk)\n })\n\n const finalize = (result: LocalHarnessResult) => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n options.signal?.removeEventListener('abort', onAbort)\n resolve(result)\n }\n\n child.on('error', (err) => {\n if (settled) return\n settled = true\n if (timer) clearTimeout(timer)\n options.signal?.removeEventListener('abort', onAbort)\n reject(err)\n })\n\n child.on('close', (code, signal) => {\n finalize({\n exitCode: code,\n stdout,\n stderr,\n killedBySignal: signal,\n durationMs: Date.now() - startedAt,\n timedOut,\n })\n })\n })\n}\n"],"mappings":"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","names":[]}
|
|
@@ -344,7 +344,7 @@ async function exportEvalRuns(events, config) {
|
|
|
344
344
|
const apiKey = config?.apiKey ?? (typeof process !== "undefined" ? process.env.TANGLE_API_KEY : void 0);
|
|
345
345
|
if (!apiKey)
|
|
346
346
|
throw new Error("exportEvalRuns: apiKey required (pass config.apiKey or set TANGLE_API_KEY)");
|
|
347
|
-
const base = config?.base ?? (typeof process !== "undefined" ? process.env.
|
|
347
|
+
const base = config?.base ?? (typeof process !== "undefined" ? process.env.TANGLE_INTELLIGENCE_URL : void 0) ?? DEFAULT_INTELLIGENCE_BASE;
|
|
348
348
|
const url = `${base.replace(/\/+$/, "")}/v1/ingest/eval-runs`;
|
|
349
349
|
const res = await fetch(url, {
|
|
350
350
|
method: "POST",
|
|
@@ -378,4 +378,4 @@ export {
|
|
|
378
378
|
INTELLIGENCE_WIRE_VERSION,
|
|
379
379
|
exportEvalRuns
|
|
380
380
|
};
|
|
381
|
-
//# sourceMappingURL=chunk-
|
|
381
|
+
//# sourceMappingURL=chunk-IVGYLCFH.js.map
|