@tangle-network/agent-runtime 0.113.1 → 0.115.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.
Files changed (68) hide show
  1. package/dist/{activation-UZ1JenNv.js → activation-BNQQO-bc.js} +3 -3
  2. package/dist/{activation-UZ1JenNv.js.map → activation-BNQQO-bc.js.map} +1 -1
  3. package/dist/agent.d.ts +18 -7
  4. package/dist/agent.js +123 -64
  5. package/dist/agent.js.map +1 -1
  6. package/dist/candidate-execution/index.js +4 -4
  7. package/dist/{candidate-execution-Co3sRC_7.js → candidate-execution-CfpJrd3o.js} +4 -4
  8. package/dist/{candidate-execution-Co3sRC_7.js.map → candidate-execution-CfpJrd3o.js.map} +1 -1
  9. package/dist/{environment-provider-BsHpW4Dp.js → environment-provider-CCaEhA-l.js} +38 -4
  10. package/dist/environment-provider-CCaEhA-l.js.map +1 -0
  11. package/dist/{environment-provider-DG-cSx89.d.ts → environment-provider-Cygthiy3.d.ts} +40 -1
  12. package/dist/environment-provider.d.ts +1 -1
  13. package/dist/environment-provider.js +1 -1
  14. package/dist/{improvement-cycle-C3CrNfcy.js → improvement-cycle-CBLOgfsQ.js} +4 -4
  15. package/dist/{improvement-cycle-C3CrNfcy.js.map → improvement-cycle-CBLOgfsQ.js.map} +1 -1
  16. package/dist/{index-Cr7QlEUL.d.ts → index-B_vcAAle.d.ts} +6 -6
  17. package/dist/{index-zMAqz53v.d.ts → index-BuzkRsOn.d.ts} +244 -9
  18. package/dist/{index-B6WclhZI.d.ts → index-I97xACXk.d.ts} +3 -3
  19. package/dist/index.d.ts +8 -8
  20. package/dist/index.js +13 -13
  21. package/dist/intelligence.d.ts +3 -3
  22. package/dist/intelligence.js +6 -6
  23. package/dist/kernel.d.ts +4 -4
  24. package/dist/kernel.js +7 -7
  25. package/dist/{knowledge-GAJFl_gz.js → knowledge-DcIKJaPd.js} +5 -5
  26. package/dist/{knowledge-GAJFl_gz.js.map → knowledge-DcIKJaPd.js.map} +1 -1
  27. package/dist/knowledge.d.ts +1 -1
  28. package/dist/knowledge.js +1 -1
  29. package/dist/{local-harness-Ce1emWJE.d.ts → local-harness-BnB1E9nM.d.ts} +82 -2
  30. package/dist/{loop-runner-bin-DvLdFo38.js → loop-runner-bin-C2GIqSUn.js} +3 -3
  31. package/dist/{loop-runner-bin-DvLdFo38.js.map → loop-runner-bin-C2GIqSUn.js.map} +1 -1
  32. package/dist/{loop-runner-bin-Cba5tsdb.d.ts → loop-runner-bin-DgAXgQCI.d.ts} +3 -3
  33. package/dist/loop-runner-bin.d.ts +1 -1
  34. package/dist/loop-runner-bin.js +1 -1
  35. package/dist/mcp/bin.js +2 -2
  36. package/dist/mcp/index.d.ts +2 -2
  37. package/dist/mcp/index.js +5 -5
  38. package/dist/{openai-tools-DftmzKLc.js → openai-tools-BWAEIHkZ.js} +2 -2
  39. package/dist/{openai-tools-DftmzKLc.js.map → openai-tools-BWAEIHkZ.js.map} +1 -1
  40. package/dist/{otel-export-D34IQW1C.js → otel-export-CPZTSADj.js} +16 -7
  41. package/dist/otel-export-CPZTSADj.js.map +1 -0
  42. package/dist/{prepare-BKVdOn0w.js → prepare-BHQBb02e.js} +2 -2
  43. package/dist/{prepare-BKVdOn0w.js.map → prepare-BHQBb02e.js.map} +1 -1
  44. package/dist/primeintellect/index.d.ts +1 -1
  45. package/dist/{protected-model-port-BtldAE9j.js → protected-model-port-DqAH1Z2M.js} +2 -2
  46. package/dist/{protected-model-port-BtldAE9j.js.map → protected-model-port-DqAH1Z2M.js.map} +1 -1
  47. package/dist/{redact-xZvPu9qe.d.ts → redact-BRaHUFMf.d.ts} +2 -2
  48. package/dist/{runtime-DlSPFm9o.js → runtime-C_g_SsXg.js} +8 -5
  49. package/dist/runtime-C_g_SsXg.js.map +1 -0
  50. package/dist/{spawn-journal-DLhnom3e.js → spawn-journal-DCPbicXB.js} +2 -1
  51. package/dist/{spawn-journal-DLhnom3e.js.map → spawn-journal-DCPbicXB.js.map} +1 -1
  52. package/dist/{structural-rollout-R_ZqFaB5.js → structural-rollout-Cqm1dKa7.js} +3 -3
  53. package/dist/{structural-rollout-R_ZqFaB5.js.map → structural-rollout-Cqm1dKa7.js.map} +1 -1
  54. package/dist/{supervise-BDvAFOTM.js → supervise-BcCHhLrz.js} +807 -35
  55. package/dist/supervise-BcCHhLrz.js.map +1 -0
  56. package/dist/{supervisor-DS278ICs.js → supervisor-CspDspod.js} +250 -22
  57. package/dist/supervisor-CspDspod.js.map +1 -0
  58. package/dist/testing.js +9 -9
  59. package/dist/{trace-propagation-CJJC7SVB.js → trace-propagation-B-pL7xn_.js} +2 -2
  60. package/dist/{trace-propagation-CJJC7SVB.js.map → trace-propagation-B-pL7xn_.js.map} +1 -1
  61. package/dist/{workspace-archive-CYYxZyA7.js → workspace-archive-DXzJq7WP.js} +2 -2
  62. package/dist/{workspace-archive-CYYxZyA7.js.map → workspace-archive-DXzJq7WP.js.map} +1 -1
  63. package/package.json +1 -1
  64. package/dist/environment-provider-BsHpW4Dp.js.map +0 -1
  65. package/dist/otel-export-D34IQW1C.js.map +0 -1
  66. package/dist/runtime-DlSPFm9o.js.map +0 -1
  67. package/dist/supervise-BDvAFOTM.js.map +0 -1
  68. package/dist/supervisor-DS278ICs.js.map +0 -1
@@ -1,19 +1,21 @@
1
1
  import { c as RuntimeRunStateError, i as ConfigError, o as NotFoundError, t as AgentEvalError$1, u as ValidationError } from "./errors-DEAvWQPy.js";
2
2
  import { a as mapWithConcurrency, c as sleep, d as throwIfAborted, f as withTimeout, l as stringifySafe, n as addTokenUsage, o as randomSuffix, p as zeroTokenUsage, r as deleteBoxSafe, s as randomUuid, t as abortError, u as throwAbort } from "./util-Cc9g9Y-o.js";
3
- import { a as contentAddress, i as InMemorySpawnJournal, n as FileSpawnJournal, r as InMemoryResultBlobStore, t as FileResultBlobStore } from "./spawn-journal-DLhnom3e.js";
4
- import { A as createWorktree, C as routerChatWithUsage, D as runWorktreeChecks, O as runWorktreeHarness, T as runBrainLoop, a as runFinalizer, i as pickBestDelivered, j as removeWorktree, k as captureWorktreeDiff, l as withDriverExecutor, n as bestDelivered, o as runTree, t as createSupervisor, x as routerBrain, y as createActivityLog } from "./supervisor-DS278ICs.js";
5
- import { i as notifyRuntimeHookEvent } from "./runtime-hooks-C7iJOWm3.js";
3
+ import { a as contentAddress, i as InMemorySpawnJournal, n as FileSpawnJournal, r as InMemoryResultBlobStore, t as FileResultBlobStore } from "./spawn-journal-DCPbicXB.js";
4
+ import { A as captureWorktreeDiff, C as routerChatWithUsage, E as runBrainLoop, M as removeWorktree, O as runWorktreeChecks, a as runFinalizer, i as pickBestDelivered, j as createWorktree, k as runWorktreeHarness, l as withDriverExecutor, n as bestDelivered, o as runTree, t as createSupervisor, x as routerBrain, y as createActivityLog } from "./supervisor-CspDspod.js";
5
+ import { i as notifyRuntimeHookEvent, t as composeRuntimeHooks } from "./runtime-hooks-C7iJOWm3.js";
6
6
  import { a as notifySandboxEventObserver, n as extractLlmCallEvent } from "./sandbox-events-DeI5xX8P.js";
7
- import { i as resolveAgentEnvironmentProvider, n as providerAsExecutor, s as buildBackendOptions } from "./environment-provider-BsHpW4Dp.js";
8
- import { r as buildLoopSpanNodes } from "./otel-export-D34IQW1C.js";
7
+ import { a as createOtelExporter, c as generateSpanId, r as buildLoopSpanNodes, u as toOtelAttributes } from "./otel-export-CPZTSADj.js";
8
+ import { i as resolveAgentEnvironmentProvider, n as providerAsExecutor, s as buildBackendOptions } from "./environment-provider-CCaEhA-l.js";
9
9
  import { t as createStdioToolServer } from "./tool-server-RcWgLIsL.js";
10
10
  import { n as UI_LENSES } from "./substrate-B0TYNrXn.js";
11
11
  import { argHash, computeFindingId, errorStreakDetector, estimateCost, isModelPriced, makeFinding, observeAll, repeatedActionDetector } from "@tangle-network/agent-eval";
12
12
  import { agentProfileSchema } from "@tangle-network/agent-interface";
13
13
  import { randomUUID } from "node:crypto";
14
14
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
15
- import path, { dirname } from "node:path";
15
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
16
+ import path, { dirname, isAbsolute, join, sep } from "node:path";
16
17
  import { spawn } from "node:child_process";
18
+ import { homedir, tmpdir } from "node:os";
17
19
  import { createServer, request } from "node:http";
18
20
  import { request as request$1 } from "node:https";
19
21
  import { Readable, Writable } from "node:stream";
@@ -92,6 +94,308 @@ function assertModelAllowed(model, allowed) {
92
94
  if (!allowed.includes(model)) throw new ConfigError(`model ${JSON.stringify(model)} is not in the allowed set ${JSON.stringify([...allowed])}`);
93
95
  }
94
96
  //#endregion
97
+ //#region src/runtime/supervise/otel-spans.ts
98
+ /**
99
+ * Supervisor tree → OTLP spans. OPT-IN, off by default.
100
+ *
101
+ * WHY. A supervised tree is legible today only by parsing this package's own spawn journal, so
102
+ * every other multi-agent shape on the machine (a coding-CLI's subagents, a pi fanout, ad-hoc tool
103
+ * parallelism) needs its own bespoke reader. A span carrying `parent_span_id` IS a tree, and any
104
+ * system can emit one — so emitting spans makes the supervisor readable by the same viewer as
105
+ * everything else, with no per-system reader.
106
+ *
107
+ * WHAT IT IS NOT. This is telemetry, never the record of truth. The spawn journal remains the sole
108
+ * durable ledger for replay/resume and for cost; nothing here is read back, and a run whose export
109
+ * fails is unaffected in every observable way. The two data models are deliberately separate.
110
+ *
111
+ * HOW IT ATTACHES. This is a pure `RuntimeHooks` observer over the lifecycle events `Scope` ALREADY
112
+ * emits — `agent.spawn` (a node opened), `agent.child` (a node settled), `agent.turn` (a driver
113
+ * inference turn was metered). It adds no event, mutates no journal, and changes no result. Because
114
+ * `Scope` re-seeds the same hooks into every nested scope (`makeNestedScopeSeam`), one observer sees
115
+ * the WHOLE recursion at arbitrary depth.
116
+ *
117
+ * SPAN SHAPE. One span per supervised node, opened at spawn and closed at settle, parented to its
118
+ * parent node's span; the run root is the trace. Driver inference rides as an `LLM` child span under
119
+ * the node that metered it. Attributes reuse the vocabulary the rest of the stack already reads
120
+ * (`openinference.span.kind`, `agent.name`, `llm.token_count.*`, `llm.cost_usd`, `tangle.cost.usd`)
121
+ * — see `@tangle-network/agent-eval`'s `src/trace/attribute-vocabulary.ts`, which is the consumer.
122
+ *
123
+ * UNKNOWN IS NEVER ZERO. `Spend.tokensKnown === false` / `usdKnown === false` mark work that
124
+ * HAPPENED with an unreported count. Those spans OMIT the token/cost attribute entirely and set
125
+ * `tangle.supervise.tokens_known` / `tangle.supervise.cost_known` to `false`, so a reader can never
126
+ * mistake an unmeasured turn for a free one.
127
+ */
128
+ /** OTEL status codes (`UNSET` / `OK` / `ERROR`) — the numeric wire values `OtelSpan.status` carries. */
129
+ const STATUS_UNSET = 0;
130
+ const STATUS_OK = 1;
131
+ const STATUS_ERROR = 2;
132
+ /** Longest string attribute value written from free-form detail, so an oversized turn payload
133
+ * cannot inflate a span. Identity/label attributes we control are never truncated. */
134
+ const MAX_DETAIL_CHARS = 256;
135
+ /**
136
+ * Build the span recorder for one supervised run, or `undefined` when no exporter resolves — the
137
+ * off-by-default path. A run that passes no `exporter` and no `exportConfig` never reaches this
138
+ * function at all; one that passes an `exportConfig` with no endpoint (and no env endpoint) gets
139
+ * `undefined` here, so "configured but unreachable" also costs nothing.
140
+ */
141
+ function createSupervisorSpanRecorder(opts) {
142
+ const exporter = opts.exporter ?? createOtelExporter(opts.exportConfig);
143
+ if (!exporter) return void 0;
144
+ const ownsExporter = opts.exporter === void 0;
145
+ const now = opts.now ?? Date.now;
146
+ const traceId = normalizeTraceId(opts.traceId, opts.runId);
147
+ const rootSpanId = generateSpanId();
148
+ const rootStartMs = now();
149
+ const base = {
150
+ "tangle.run.id": opts.runId,
151
+ "tangle.sessionId": opts.runId,
152
+ ...opts.attributes ?? {}
153
+ };
154
+ /** Node id → its open span. Seeded with the run id ⇒ the root span, because a depth-0 spawn's
155
+ * `parentId` is the run id itself and every deeper spawn's is a real node id. */
156
+ const open = /* @__PURE__ */ new Map();
157
+ const spanIdOf = /* @__PURE__ */ new Map([[opts.runId, rootSpanId]]);
158
+ let finished = false;
159
+ /** Every export is best-effort: a throwing exporter must never reach the run. */
160
+ const emit = (span) => {
161
+ try {
162
+ exporter.exportSpan(span);
163
+ } catch {}
164
+ };
165
+ const span = (spanId, parentSpanId, name, startMs, endMs, attrs, status, message) => ({
166
+ traceId,
167
+ spanId,
168
+ ...parentSpanId ? { parentSpanId } : {},
169
+ name,
170
+ kind: 1,
171
+ startTimeUnixNano: msToNano(startMs),
172
+ endTimeUnixNano: msToNano(Math.max(startMs, endMs)),
173
+ attributes: toOtelAttributes(attrs),
174
+ status: {
175
+ code: status,
176
+ ...message ? { message } : {}
177
+ }
178
+ });
179
+ function onSpawn(event) {
180
+ const p = record(event.payload);
181
+ const childId = str$1(p.childId);
182
+ if (!childId) return;
183
+ const label = str$1(p.label) ?? "node";
184
+ const runtime = str$1(p.runtime);
185
+ const isWait = runtime === "wait";
186
+ const attrs = {
187
+ ...base,
188
+ "openinference.span.kind": isWait ? "CHAIN" : "AGENT",
189
+ "agent.name": label,
190
+ "tangle.supervise.node.id": childId,
191
+ "tangle.supervise.node.label": label,
192
+ "tangle.supervise.node.kind": isWait ? "wait" : "agent",
193
+ "tangle.supervise.tree.root": event.runId
194
+ };
195
+ if (event.parentId) attrs["tangle.supervise.node.parent_id"] = event.parentId;
196
+ if (runtime) attrs["tangle.supervise.node.runtime"] = runtime;
197
+ if (typeof p.depth === "number") attrs["tangle.supervise.node.depth"] = p.depth;
198
+ if (typeof event.stepIndex === "number") attrs["tangle.supervise.node.ordinal"] = event.stepIndex;
199
+ if (p.resumed === true) attrs["tangle.supervise.node.resumed"] = true;
200
+ assignBudget(attrs, p.budget);
201
+ const spanId = generateSpanId();
202
+ spanIdOf.set(childId, spanId);
203
+ open.set(childId, {
204
+ spanId,
205
+ parentSpanId: event.parentId && spanIdOf.get(event.parentId) || rootSpanId,
206
+ name: label,
207
+ startMs: event.timestamp,
208
+ attrs
209
+ });
210
+ }
211
+ function onSettled(event) {
212
+ const p = record(event.payload);
213
+ const childId = str$1(p.childId);
214
+ if (!childId) return;
215
+ const node = open.get(childId);
216
+ if (!node) return;
217
+ open.delete(childId);
218
+ const status = str$1(p.status);
219
+ const down = status === "down";
220
+ const attrs = {
221
+ ...node.attrs,
222
+ "tangle.supervise.node.status": status ?? "done"
223
+ };
224
+ if (typeof event.stepIndex === "number") attrs["tangle.supervise.node.seq"] = event.stepIndex;
225
+ if (typeof p.outRef === "string") attrs["tangle.supervise.node.out_ref"] = p.outRef;
226
+ if (typeof p.valid === "boolean") attrs["tangle.supervise.verdict.valid"] = p.valid;
227
+ if (typeof p.score === "number") attrs["tangle.supervise.verdict.score"] = p.score;
228
+ if (down) {
229
+ attrs["error.type"] = p.infra === true ? "infra" : "child-down";
230
+ const reason = str$1(p.reason);
231
+ if (reason) attrs["error.message"] = truncate(reason);
232
+ if (typeof p.infra === "boolean") attrs["tangle.supervise.node.infra"] = p.infra;
233
+ }
234
+ const wokeBy = str$1(record(p.wait).settled);
235
+ if (wokeBy) attrs["tangle.supervise.wait.settled"] = wokeBy;
236
+ assignSpend(attrs, p.spent);
237
+ emit(span(node.spanId, node.parentSpanId, node.name, node.startMs, event.timestamp, attrs, down ? STATUS_ERROR : STATUS_OK, down ? str$1(p.reason) ?? "child down" : void 0));
238
+ }
239
+ function onTurn(event) {
240
+ const p = record(event.payload);
241
+ const parentId = event.parentId;
242
+ const parentSpanId = parentId && spanIdOf.get(parentId) || rootSpanId;
243
+ const attrs = {
244
+ ...base,
245
+ "openinference.span.kind": "LLM",
246
+ "inference.observation_kind": "LLM",
247
+ "tangle.supervise.node.kind": "inference"
248
+ };
249
+ if (parentId) attrs["tangle.supervise.node.id"] = parentId;
250
+ for (const [key, value] of Object.entries(p)) {
251
+ if (key === "spend") continue;
252
+ if (key === "driver" && typeof value === "string") {
253
+ attrs["agent.name"] = value;
254
+ attrs["inference.agent_name"] = value;
255
+ continue;
256
+ }
257
+ if (key === "model" && typeof value === "string") {
258
+ attrs["llm.model_name"] = value;
259
+ continue;
260
+ }
261
+ if (Array.isArray(value)) {
262
+ const scalars = value.filter((v) => typeof v === "string" || typeof v === "number");
263
+ if (scalars.length > 0) attrs[`tangle.supervise.turn.${key}`] = truncate(scalars.join(","));
264
+ continue;
265
+ }
266
+ if (typeof value === "string") attrs[`tangle.supervise.turn.${key}`] = truncate(value);
267
+ else if (typeof value === "number" || typeof value === "boolean") attrs[`tangle.supervise.turn.${key}`] = value;
268
+ }
269
+ const spend = assignSpend(attrs, p.spend);
270
+ const endMs = event.timestamp + (spend?.ms ?? 0);
271
+ emit(span(generateSpanId(), parentSpanId, "gen_ai.client.inference", event.timestamp, endMs, attrs, STATUS_OK));
272
+ }
273
+ return {
274
+ hooks: { onEvent(event) {
275
+ if (finished) return;
276
+ try {
277
+ if (event.phase !== "after") return;
278
+ if (event.target === "agent.spawn") onSpawn(event);
279
+ else if (event.target === "agent.child") onSettled(event);
280
+ else if (event.target === "agent.turn") onTurn(event);
281
+ } catch {}
282
+ } },
283
+ traceId,
284
+ rootSpanId,
285
+ async finish(outcome) {
286
+ if (finished) return;
287
+ finished = true;
288
+ const endMs = now();
289
+ try {
290
+ for (const [nodeId, node] of open) emit(span(node.spanId, node.parentSpanId, node.name, node.startMs, endMs, {
291
+ ...node.attrs,
292
+ "tangle.supervise.node.status": "unsettled",
293
+ "tangle.supervise.node.settled": false,
294
+ "tangle.supervise.node.id": nodeId
295
+ }, STATUS_UNSET));
296
+ open.clear();
297
+ emit(span(rootSpanId, opts.parentSpanId, "supervisor.run", rootStartMs, endMs, rootAttrs(base, opts.agentName ?? "supervisor", outcome), rootStatus(outcome), rootMessage(outcome)));
298
+ await exporter.flush();
299
+ if (ownsExporter) await exporter.shutdown();
300
+ } catch {}
301
+ }
302
+ };
303
+ }
304
+ function rootAttrs(base, agentName, outcome) {
305
+ const attrs = {
306
+ ...base,
307
+ "openinference.span.kind": "AGENT",
308
+ "inference.observation_kind": "AGENT",
309
+ "agent.name": agentName,
310
+ "inference.agent_name": agentName,
311
+ "tangle.supervise.node.kind": "root"
312
+ };
313
+ const result = outcome?.result;
314
+ if (result) {
315
+ attrs["tangle.supervise.result"] = result.kind;
316
+ if (result.kind === "no-winner") {
317
+ attrs["tangle.supervise.reason"] = result.reason;
318
+ attrs["tangle.supervise.down_count"] = result.downCount;
319
+ if (result.reason === "driver-failed") {
320
+ attrs["error.type"] = result.error.name;
321
+ attrs["error.message"] = truncate(result.error.message);
322
+ }
323
+ }
324
+ assignSpend(attrs, result.spentTotal);
325
+ }
326
+ if (outcome?.error !== void 0) {
327
+ attrs["tangle.supervise.result"] = "error";
328
+ const err = outcome.error;
329
+ attrs["error.type"] = err instanceof Error ? err.name : typeof err;
330
+ attrs["error.message"] = truncate(err instanceof Error ? err.message : String(err));
331
+ }
332
+ return attrs;
333
+ }
334
+ function rootStatus(outcome) {
335
+ if (outcome?.error !== void 0) return STATUS_ERROR;
336
+ if (!outcome?.result) return STATUS_UNSET;
337
+ return outcome.result.kind === "winner" ? STATUS_OK : STATUS_ERROR;
338
+ }
339
+ function rootMessage(outcome) {
340
+ if (outcome?.error !== void 0) return truncate(outcome.error instanceof Error ? outcome.error.message : String(outcome.error));
341
+ const result = outcome?.result;
342
+ return result && result.kind === "no-winner" ? result.reason : void 0;
343
+ }
344
+ /**
345
+ * Write a `Spend` onto a span, honouring the "a missing measurement is never zero" invariant: a
346
+ * channel marked NOT known contributes no number at all and instead flags itself, so nothing
347
+ * downstream can sum an unmeasured turn as free. Returns the spend it read (for the duration).
348
+ */
349
+ function assignSpend(attrs, value) {
350
+ if (!isRecord(value)) return void 0;
351
+ const spend = value;
352
+ const tokens = isRecord(spend.tokens) ? spend.tokens : void 0;
353
+ if (spend.tokensKnown === false) attrs["tangle.supervise.tokens_known"] = false;
354
+ else if (tokens) {
355
+ if (typeof tokens.input === "number") attrs["llm.token_count.prompt"] = tokens.input;
356
+ if (typeof tokens.output === "number") attrs["llm.token_count.completion"] = tokens.output;
357
+ }
358
+ if (spend.usdKnown === false) attrs["tangle.supervise.cost_known"] = false;
359
+ else if (typeof spend.usd === "number" && Number.isFinite(spend.usd)) {
360
+ attrs["llm.cost_usd"] = spend.usd;
361
+ attrs["tangle.cost.usd"] = spend.usd;
362
+ }
363
+ if (typeof spend.iterations === "number") attrs["tangle.supervise.iterations"] = spend.iterations;
364
+ if (typeof spend.ms === "number" && spend.ms > 0) attrs["tangle.supervise.duration_ms"] = spend.ms;
365
+ return spend;
366
+ }
367
+ function assignBudget(attrs, value) {
368
+ if (!isRecord(value)) return;
369
+ const budget = value;
370
+ if (typeof budget.maxTokens === "number") attrs["tangle.supervise.budget.max_tokens"] = budget.maxTokens;
371
+ if (typeof budget.maxIterations === "number") attrs["tangle.supervise.budget.max_iterations"] = budget.maxIterations;
372
+ if (typeof budget.maxUsd === "number") attrs["tangle.supervise.budget.max_usd"] = budget.maxUsd;
373
+ }
374
+ /**
375
+ * A trace id must be 32 hex characters. A caller-supplied one is used verbatim when it already is;
376
+ * anything else (and the default) is DERIVED from the run id by content address — deterministic, so
377
+ * a resumed run rejoins the trace its first process opened rather than forking a new one.
378
+ */
379
+ function normalizeTraceId(traceId, runId) {
380
+ if (traceId && /^[0-9a-f]{32}$/i.test(traceId)) return traceId.toLowerCase();
381
+ return contentAddress(traceId ?? runId).slice(7, 39);
382
+ }
383
+ function msToNano(ms) {
384
+ return (BigInt(Number.isFinite(ms) ? Math.floor(ms) : 0) * 1000000n).toString();
385
+ }
386
+ function isRecord(value) {
387
+ return typeof value === "object" && value !== null && !Array.isArray(value);
388
+ }
389
+ function record(value) {
390
+ return isRecord(value) ? value : {};
391
+ }
392
+ function str$1(value) {
393
+ return typeof value === "string" && value.length > 0 ? value : void 0;
394
+ }
395
+ function truncate(value) {
396
+ return value.length > MAX_DETAIL_CHARS ? `${value.slice(0, MAX_DETAIL_CHARS)}…` : value;
397
+ }
398
+ //#endregion
95
399
  //#region src/runtime/supervise/coordination-log.ts
96
400
  /** Should this bus event be persisted? Questions and findings ARE the prior context a resumed
97
401
  * driver needs; answers fold their status; everything else has a better ledger or none. */
@@ -1273,6 +1577,395 @@ function createInbox() {
1273
1577
  };
1274
1578
  }
1275
1579
  //#endregion
1580
+ //#region src/runtime/supervise/pi-mcp.ts
1581
+ /**
1582
+ *
1583
+ * MCP for the `pi` harness — the mount `piExecutor` performs before it spawns pi.
1584
+ *
1585
+ * pi ships NO MCP support of its own. MCP arrives through the `pi-mcp-adapter` extension, and the
1586
+ * adapter takes its config from a path the CALLER names: it registers a `--mcp-config <path>` flag
1587
+ * (`pi-mcp-adapter` `index.ts:252`, read back at `init.ts:101` and off raw argv at `utils.ts:55`).
1588
+ * So "give pi an MCP server" means: write the canonical `{ mcpServers: … }` object to a path this
1589
+ * run owns, pass that path with `--mcp-config`, make sure the adapter is loaded, and delete the
1590
+ * file afterwards. This module does exactly that and nothing else.
1591
+ *
1592
+ * Three rules carry the whole design:
1593
+ *
1594
+ * 1. **Fail loud, never fail safe.** If the profile declares MCP servers and `pi-mcp-adapter` is not
1595
+ * installed, `preparePiMcp` throws BEFORE any file is written and before pi is spawned. A run
1596
+ * whose tools never existed must fail, not score zero for the wrong reason.
1597
+ * 2. **`--no-extensions` is a trap.** When `extensions.pi.load` is an array, pi runs
1598
+ * `--no-extensions`, which suppresses ALL auto-discovery — cwd `.pi/extensions`, global
1599
+ * `~/.pi/agent/extensions`, AND the `settings.json` `packages` list that would otherwise have
1600
+ * auto-loaded the adapter. So `load: ['pi-memory']` alongside `mcp` kills MCP dead even though
1601
+ * the adapter IS installed. When MCP is declared, this module adds `pi-mcp-adapter` to the
1602
+ * resolved load list and REPORTS that it did (`PiMcpReceipt.adapterInjected`). The caller's
1603
+ * profile object is never mutated.
1604
+ * 3. **One config file per worker EXECUTION, never one per workspace.** `PiSeam.cwd` comes from the
1605
+ * `ExecutorConfig` and is therefore shared by every worker the factory builds, so a config
1606
+ * discovered by cwd is a file two concurrent siblings fight over. `mountPiMcpConfig` instead
1607
+ * creates a fresh `mkdtemp` directory — OS-guaranteed unique, so no counter, lock, merge or
1608
+ * restore is needed — and hands its path to `--mcp-config`. Concurrent workers built from ONE
1609
+ * seam get ONE config each and never collide.
1610
+ *
1611
+ * Why the flag is safe to emit here even though pi only lists it under `Extension CLI Flags`: it is
1612
+ * emitted ONLY when the profile declares at least one usable MCP server, and that case has already
1613
+ * passed the `piMcpAdapterAvailable()` gate above, so the extension that registers the flag is
1614
+ * guaranteed loadable (auto-discovered, or force-loaded by the `--extension` this module appends).
1615
+ * A machine without the adapter never reaches the flag — it fails at the gate with an install
1616
+ * command instead.
1617
+ *
1618
+ * Where the run's servers sit in pi's config layering, stated plainly because it is a real
1619
+ * behavioral edge: `--mcp-config` REPLACES pi's user-global override file (`config.ts:310`,
1620
+ * `getPiGlobalConfigPath(overridePath)`), so the operator's own `~/.pi/agent/mcp.json` servers no
1621
+ * longer leak into a worker — good. It does NOT suppress project discovery: the adapter still
1622
+ * layers `<cwd>/.mcp.json` and `<cwd>/.pi/mcp.json` on top (`config.ts:352-373`), and those are
1623
+ * later sources, so a workspace that ships a server under the SAME NAME overrides this run's entry.
1624
+ * The consequence is worth naming: an A/B whose arms differ only in `profile.mcp` is confounded
1625
+ * when the shared workspace carries its own MCP config. Deleting a file the run did not create
1626
+ * would be worse — a run cannot quietly edit the repo it was pointed at.
1627
+ *
1628
+ * Upstream note: `@tangle-network/agent-profile-materialize` is the shared owner of profile → harness
1629
+ * mapping and already declares `pi` a first-class `HarnessId`, but as of 0.9.4 its pi arm answers
1630
+ * `unsupported('mcp', 'pi MCP requires a loaded MCP extension; the base Pi runtime has no MCP
1631
+ * control')` and its pi extensions arm answers `unsupported('extensions', …)`. That refusal is now
1632
+ * factually stale — the shape is exactly its `kimi-code` arm's (write a config file, push a config
1633
+ * flag, because the harness has no cwd discovery worth trusting). Teaching the materializer these
1634
+ * two rows is the durable home for this mapping; until it lands, this module is the only thing in
1635
+ * the `backend: 'pi'` path that can honor `profile.mcp` at all.
1636
+ *
1637
+ * Verified against pi 0.83.0 and pi-mcp-adapter 2.15.0.
1638
+ *
1639
+ * @experimental
1640
+ */
1641
+ /** The pi extension that gives pi MCP at all. Everything here is gated on it being loadable. */
1642
+ const PI_MCP_ADAPTER = "pi-mcp-adapter";
1643
+ /** Overrides adapter detection for installs whose path does not carry the adapter's name. */
1644
+ const PI_MCP_ADAPTER_ENV = "AGENT_RUNTIME_PI_MCP_ADAPTER";
1645
+ /** The adapter-registered flag that names this run's config file (`pi-mcp-adapter` `index.ts:252`). */
1646
+ const PI_MCP_CONFIG_FLAG = "--mcp-config";
1647
+ /** pi's agent home. Holds `settings.json` and `npm/node_modules/<extension>`. */
1648
+ function piAgentDir() {
1649
+ return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
1650
+ }
1651
+ /** `<agentDir>/npm`, whose `node_modules/<pkg>` holds installed pi extensions. */
1652
+ function piNpmRoot() {
1653
+ return join(piAgentDir(), "npm");
1654
+ }
1655
+ /**
1656
+ * True when pi can actually consume MCP config — i.e. `pi-mcp-adapter` is installed. Ported from
1657
+ * cli-bridge `pi.ts:189`: an env override first (for vendored installs whose path does not carry the
1658
+ * adapter's name), then the npm install directory, then a scan of `settings.json` `packages` in
1659
+ * which a local-path entry is resolved by reading the target's `package.json` `name`. An
1660
+ * unreadable or absent `settings.json` reports "not detected"; it never throws and never assumes.
1661
+ */
1662
+ function piMcpAdapterAvailable() {
1663
+ const override = process.env[PI_MCP_ADAPTER_ENV];
1664
+ if (override === "1" || override === "true") return true;
1665
+ if (override === "0" || override === "false") return false;
1666
+ const agentDir = piAgentDir();
1667
+ if (existsSync(join(agentDir, "npm", "node_modules", "pi-mcp-adapter"))) return true;
1668
+ try {
1669
+ const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
1670
+ if (!Array.isArray(settings.packages)) return false;
1671
+ return settings.packages.some((entry) => packageEntryIsAdapter(entry, agentDir));
1672
+ } catch {
1673
+ return false;
1674
+ }
1675
+ }
1676
+ /** One `settings.json` `packages[]` entry: a name containing the adapter, or a local path to it. */
1677
+ function packageEntryIsAdapter(entry, agentDir) {
1678
+ if (typeof entry !== "string") return false;
1679
+ if (entry.includes("pi-mcp-adapter")) return true;
1680
+ const spec = entry.replace(/^(file|path):(\/\/)?/, "");
1681
+ const windowsAbsolute = /^[A-Za-z]:[\\/]/.test(spec);
1682
+ if (!isAbsolute(spec) && !windowsAbsolute && !spec.startsWith(".")) return false;
1683
+ return readPackageName(isAbsolute(spec) || windowsAbsolute ? spec : join(agentDir, spec)) === PI_MCP_ADAPTER;
1684
+ }
1685
+ function readPackageName(packageDir) {
1686
+ try {
1687
+ const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf-8"));
1688
+ return typeof manifest.name === "string" ? manifest.name : void 0;
1689
+ } catch {
1690
+ return;
1691
+ }
1692
+ }
1693
+ /**
1694
+ * Read one `AgentProfileConfigValue` as public bytes. A `secret-ref` is REFUSED rather than
1695
+ * stringified: this executor has no secret provider, so the only alternatives are writing a
1696
+ * placeholder into the config (an auth failure disguised as a tool bug) or writing the literal
1697
+ * reference object (nonsense to the adapter). Plain strings, numbers and booleans are accepted
1698
+ * because profiles authored as JSON commonly carry them where the type says `{kind:'public'}`.
1699
+ */
1700
+ function publicConfigString(value, where) {
1701
+ if (typeof value === "string") return value;
1702
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1703
+ if (value && typeof value === "object" && value.kind === "public") return value.value;
1704
+ if (value && typeof value === "object" && value.kind === "secret-ref") throw new ConfigError(`piExecutor: ${where} is a secret-ref ("${value.key}") and piExecutor has no secret provider; resolve it before handing the profile to the executor, or declare a public value`);
1705
+ throw new ValidationError(`piExecutor: ${where} is not a public configuration value`);
1706
+ }
1707
+ /**
1708
+ * Build the canonical `{ mcpServers }` body the adapter reads, from `AgentProfile.mcp`.
1709
+ *
1710
+ * Every server is written with `directTools: true` (`pi-mcp-adapter` types.ts:351), which
1711
+ * registers its tools as NATIVE pi tools instead of hiding them behind the generic `mcp` tool.
1712
+ * Without it an agent must discover its own verbs before it can use them — connect to the server,
1713
+ * then describe each tool — and a measured run spent 58 turns and 639,632 input tokens doing that
1714
+ * before it could delegate once. A profile that declares an MCP server is asking for its tools,
1715
+ * not for a directory it has to browse.
1716
+ *
1717
+ * This is cli-bridge's `buildCanonicalMcpServers`
1718
+ * (`/home/drew/code/cli-bridge/src/backends/profile-support.ts:253`) reproduced field for field and
1719
+ * in the same key order — same drop of `enabled:false`, same stdio-versus-remote decision
1720
+ * (`isStdioMcpSpec`, `profile-support.ts:177`), same `{command, args, env}` and `{type, url,
1721
+ * headers}` bodies, same silent drop of an entry with nothing to connect to. For any server both
1722
+ * input types can express, the emitted JSON is byte-identical.
1723
+ *
1724
+ * The two input types are not the same type, so three differences remain and each is a field one
1725
+ * side cannot express:
1726
+ *
1727
+ * 1. **`cwd` (emitted here, absent there).** `AgentProfileLocalMcpServer` has `cwd`; cli-bridge's
1728
+ * `McpServerSpec` (`src/backends/types.ts:53`) has no such field, so it can never emit one. The
1729
+ * adapter honors it (`pi-mcp-adapter` `types.ts:325`, `ServerEntry.cwd`), so dropping it would
1730
+ * silently discard a declared setting. It is emitted where cli-bridge emits `timeout`.
1731
+ * 2. **`timeout` (emitted there, absent here).** `AgentProfileMcpServer` has no timeout field, so
1732
+ * there is nothing to emit. (The adapter's own knob is `requestTimeoutMs`, not `timeout`.)
1733
+ * 3. **A `url` with no declared transport.** cli-bridge DROPS it — its remote branch requires an
1734
+ * explicit `spec.type` — even though its own `McpServerSpec` docstring says "http is implied if
1735
+ * `url` is set" (`types.ts:53-56`). `transport` is optional on `AgentProfileRemoteMcpServer`, so
1736
+ * dropping such a server here would be exactly the silent-drop bug this module exists to fix.
1737
+ * We honor the documented convention and emit `type: 'http'`.
1738
+ *
1739
+ * `sse` is ACCEPTED and forwarded verbatim, as cli-bridge forwards it. The adapter connects a URL
1740
+ * server by probing StreamableHTTP and falling back to `SSEClientTransport` when the probe fails
1741
+ * (`pi-mcp-adapter` `server-manager.ts:765`), so an SSE-only endpoint does connect. Its
1742
+ * `ServerEntry` (`types.ts:319`) carries no transport discriminator at all, which means the `type`
1743
+ * key is inert to the adapter either way — but it is what cli-bridge writes, and a config file that
1744
+ * round-trips the caller's declared transport is the one that stays readable by both.
1745
+ */
1746
+ function buildPiMcpServers(mcp) {
1747
+ const out = {};
1748
+ if (!mcp || typeof mcp !== "object") return out;
1749
+ for (const [name, raw] of Object.entries(mcp)) {
1750
+ if (!name || !raw || typeof raw !== "object") continue;
1751
+ if (raw.enabled === false) continue;
1752
+ const transport = raw.transport;
1753
+ const command = typeof raw.command === "string" && raw.command.length > 0 ? raw.command : null;
1754
+ const url = typeof raw.url === "string" && raw.url.length > 0 ? raw.url : null;
1755
+ if ((transport === "http" || transport === "sse" ? false : command !== null) && command !== null) {
1756
+ const args = Array.isArray(raw.args) ? raw.args.map((a, i) => publicConfigString(a, `mcp["${name}"].args[${i}]`)) : [];
1757
+ const env = readConfigRecord(raw.env, `mcp["${name}"].env`);
1758
+ out[name] = {
1759
+ command,
1760
+ ...args.length > 0 ? { args } : {},
1761
+ ...Object.keys(env).length > 0 ? { env } : {},
1762
+ ...typeof raw.cwd === "string" && raw.cwd.length > 0 ? { cwd: raw.cwd } : {},
1763
+ directTools: true
1764
+ };
1765
+ continue;
1766
+ }
1767
+ if (url !== null && transport !== "stdio") {
1768
+ const headers = readConfigRecord(raw.headers, `mcp["${name}"].headers`);
1769
+ out[name] = {
1770
+ type: transport === "sse" ? "sse" : "http",
1771
+ url,
1772
+ ...Object.keys(headers).length > 0 ? { headers } : {},
1773
+ directTools: true
1774
+ };
1775
+ }
1776
+ }
1777
+ return out;
1778
+ }
1779
+ function readConfigRecord(record, where) {
1780
+ if (!record || typeof record !== "object") return {};
1781
+ const out = {};
1782
+ for (const [key, value] of Object.entries(record)) {
1783
+ if (value === void 0) continue;
1784
+ out[key] = publicConfigString(value, `${where}["${key}"]`);
1785
+ }
1786
+ return out;
1787
+ }
1788
+ /**
1789
+ * Resolve one `extensions.pi.load` entry to an absolute extension entry file. Ported from cli-bridge
1790
+ * `resolvePiExtensionEntry` (`pi.ts:123`): an absolute path is used verbatim; a bare package name
1791
+ * resolves to `<npmRoot>/node_modules/<pkg>/<package.json main>`, `main` defaulting to `index.ts`.
1792
+ * The resolution is required because pi's per-file loader does NOT resolve bare specifiers against
1793
+ * `~/.pi/agent/npm/node_modules`. Fail-closed: an unreadable manifest or a missing entry file
1794
+ * rejects the run rather than letting an ablation arm run without the extension it declared.
1795
+ */
1796
+ function resolvePiExtensionEntry(spec, npmRoot) {
1797
+ let entry;
1798
+ if (isAbsolute(spec)) entry = spec;
1799
+ else {
1800
+ const packageDir = join(npmRoot, "node_modules", spec);
1801
+ let main = "index.ts";
1802
+ try {
1803
+ const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf-8"));
1804
+ if (typeof manifest.main === "string" && manifest.main) main = manifest.main;
1805
+ } catch (err) {
1806
+ throw new ConfigError(`piExecutor: cannot load pi extension "${spec}": ${join(packageDir, "package.json")} is not readable (install it with \`pi install npm:${spec}\`): ${err instanceof Error ? err.message : String(err)}`);
1807
+ }
1808
+ entry = join(packageDir, main);
1809
+ }
1810
+ if (!existsSync(entry)) throw new ConfigError(`piExecutor: cannot load pi extension "${spec}": resolved entry ${entry} does not exist`);
1811
+ return entry;
1812
+ }
1813
+ /** Does this `load` entry name the MCP adapter, before resolution? */
1814
+ function entryNamesAdapter(spec) {
1815
+ if (!isAbsolute(spec)) return spec === PI_MCP_ADAPTER;
1816
+ if (spec.split(/[\\/]/).includes("pi-mcp-adapter")) return true;
1817
+ const parent = spec.slice(0, spec.lastIndexOf(sep) + 1);
1818
+ return parent.length > 0 && readPackageName(parent) === "pi-mcp-adapter";
1819
+ }
1820
+ /**
1821
+ * Lower `AgentProfile.extensions.pi.load` into pi argv, honoring the `--no-extensions` trap.
1822
+ *
1823
+ * `load` absent (not an array) → no flags at all, so pi keeps its default discovery and the adapter
1824
+ * auto-loads from `settings.json` `packages`. `load` present → `--no-extensions` plus one
1825
+ * `--extension <absolute entry>` per listed extension, which is what makes an ablation arm
1826
+ * reproducible and leak-free.
1827
+ *
1828
+ * When `mcpRequested` is true and an explicit `load` array does not already name the adapter, the
1829
+ * adapter is APPENDED — otherwise `--no-extensions` would suppress the very extension that turns
1830
+ * the `--mcp-config` file into tools. The caller's profile is not touched; the addition is reported
1831
+ * through `adapterInjected` so it reaches the executor receipt and the live progress channel.
1832
+ *
1833
+ * A non-string or blank entry is a schema error here, not a silent skip: an ablation arm that
1834
+ * quietly drops a typo'd extension is the same class of structural false-null this module exists to
1835
+ * eliminate.
1836
+ */
1837
+ function derivePiExtensionArgs(profile, mcpRequested) {
1838
+ const load = profile?.extensions?.pi?.load;
1839
+ if (!Array.isArray(load)) return {
1840
+ args: [],
1841
+ entries: [],
1842
+ adapterInjected: false
1843
+ };
1844
+ const declared = [];
1845
+ for (const [index, entry] of load.entries()) {
1846
+ if (typeof entry !== "string" || !entry.trim()) throw new ValidationError(`piExecutor: extensions.pi.load[${index}] must be a non-empty string (got ${entry === null ? "null" : typeof entry})`);
1847
+ declared.push(entry.trim());
1848
+ }
1849
+ const adapterInjected = mcpRequested && !declared.some(entryNamesAdapter);
1850
+ const resolvedSpecs = adapterInjected ? [...declared, PI_MCP_ADAPTER] : declared;
1851
+ const npmRoot = piNpmRoot();
1852
+ const entries = resolvedSpecs.map((spec) => resolvePiExtensionEntry(spec, npmRoot));
1853
+ const args = ["--no-extensions"];
1854
+ for (const entry of entries) args.push("--extension", entry);
1855
+ return {
1856
+ args,
1857
+ entries,
1858
+ adapterInjected
1859
+ };
1860
+ }
1861
+ /** Directory-name-safe form of a run id; the `mkdtemp` suffix supplies the uniqueness. */
1862
+ function runIdSlug(runId) {
1863
+ const slug = runId.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
1864
+ return slug.length > 0 ? slug.slice(0, 60) : "worker";
1865
+ }
1866
+ /**
1867
+ * Write this worker execution's MCP config to a directory only it owns, and hand back the removal.
1868
+ *
1869
+ * Every property of the old cwd-discovered mount — a cross-process lock, a merge under whatever the
1870
+ * workspace already had, a verbatim byte restore, a symlink guard on `.pi` — existed because two
1871
+ * workers sharing a `PiSeam` shared one file path. `mkdtemp` deletes the shared path, and with it
1872
+ * the entire class of problem: the directory did not exist a moment ago, nothing else can name it,
1873
+ * `cleanup()` removes it whole, and two siblings started in the same millisecond get two configs.
1874
+ *
1875
+ * Returns `null` when there are no usable servers, so the no-MCP path writes nothing at all.
1876
+ */
1877
+ function mountPiMcpConfig(mcpServers, options) {
1878
+ const serverNames = Object.keys(mcpServers);
1879
+ if (serverNames.length === 0) return null;
1880
+ const slug = runIdSlug(options.runId);
1881
+ const base = tmpdir();
1882
+ const prefix = `agent-runtime-pi-mcp-${slug}-`;
1883
+ let dir;
1884
+ try {
1885
+ dir = mkdtempSync(join(base, prefix));
1886
+ } catch (err) {
1887
+ throw new ConfigError(`piExecutor: cannot create a private MCP config directory under ${base}: ${err instanceof Error ? err.message : String(err)}`);
1888
+ }
1889
+ const configPath = join(dir, "mcp.json");
1890
+ try {
1891
+ writeFileSync(configPath, JSON.stringify({ mcpServers }, null, 2), {
1892
+ flag: "wx",
1893
+ mode: 384
1894
+ });
1895
+ } catch (err) {
1896
+ rmSync(dir, {
1897
+ recursive: true,
1898
+ force: true
1899
+ });
1900
+ throw new ConfigError(`piExecutor: failed to write the MCP config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
1901
+ }
1902
+ let cleaned = false;
1903
+ return {
1904
+ configPath,
1905
+ serverNames,
1906
+ cleanup: () => {
1907
+ if (cleaned) return;
1908
+ cleaned = true;
1909
+ rmSync(dir, {
1910
+ recursive: true,
1911
+ force: true
1912
+ });
1913
+ }
1914
+ };
1915
+ }
1916
+ /**
1917
+ * The one call `piExecutor` makes before spawning pi: decide what pi must be told, fail loud if it
1918
+ * cannot work, then mount.
1919
+ *
1920
+ * Order matters and is the point:
1921
+ *
1922
+ * 1. Canonicalize `profile.mcp` first, so the adapter gate keys off servers that would ACTUALLY be
1923
+ * written — a block of `enabled:false` entries must not trip it.
1924
+ * 2. If any server survives and `pi-mcp-adapter` is not installed, throw. No file has been written
1925
+ * and pi has not been spawned.
1926
+ * 3. Lower `extensions.pi.load` (which may itself throw on an unresolvable extension) — still before
1927
+ * any file exists.
1928
+ * 4. Only then create this execution's private config directory and emit `--mcp-config`.
1929
+ *
1930
+ * A profile with no usable MCP server is a pure no-op: no adapter check, no directory, no flags, no
1931
+ * throw. `extensions.pi.load` on its own still produces flags, because reproducible ablation arms
1932
+ * need them whether or not MCP is involved.
1933
+ *
1934
+ * The caller MUST call `mount.cleanup()` on every exit path — settle, throw, abort, timeout, and
1935
+ * spawn failure — so the directory does not outlive the worker that owns it.
1936
+ */
1937
+ function preparePiMcp(profile, options) {
1938
+ const mcpServers = buildPiMcpServers(profile?.mcp);
1939
+ const requested = Object.keys(mcpServers);
1940
+ if (requested.length > 0 && !piMcpAdapterAvailable()) throw new ConfigError(`piExecutor: cannot mount MCP servers for pi (${requested.join(", ")}): the ${PI_MCP_ADAPTER} extension is not installed. pi ships no MCP support of its own, so these servers would never connect and the worker would run tool-less. Looked in ${join(piNpmRoot(), "node_modules", PI_MCP_ADAPTER)} and in the \`packages\` list of ${join(piAgentDir(), "settings.json")}. Install it with \`pi install npm:${PI_MCP_ADAPTER}\`, or set ${PI_MCP_ADAPTER_ENV}=1 if it is vendored somewhere this check cannot see.`);
1941
+ const extensions = derivePiExtensionArgs(profile, requested.length > 0);
1942
+ if (requested.length === 0) return {
1943
+ args: extensions.args,
1944
+ mount: null,
1945
+ receipt: void 0
1946
+ };
1947
+ const mount = mountPiMcpConfig(mcpServers, options);
1948
+ if (!mount) return {
1949
+ args: extensions.args,
1950
+ mount: null,
1951
+ receipt: void 0
1952
+ };
1953
+ return {
1954
+ args: [
1955
+ ...extensions.args,
1956
+ PI_MCP_CONFIG_FLAG,
1957
+ mount.configPath
1958
+ ],
1959
+ mount,
1960
+ receipt: {
1961
+ servers: mount.serverNames,
1962
+ configPath: mount.configPath,
1963
+ extensions: extensions.entries,
1964
+ adapterInjected: extensions.adapterInjected
1965
+ }
1966
+ };
1967
+ }
1968
+ //#endregion
1276
1969
  //#region src/runtime/supervise/trace-source.ts
1277
1970
  /** Project a normalized tool step into the canonical agent-eval `ToolSpan`. When the step carries
1278
1971
  * real wall-clock (`startedAt`/`endedAt`) the span gets a true duration; otherwise it collapses to
@@ -1510,6 +2203,32 @@ function sandboxSessionTraceSource(box, sessionId, opts = {}) {
1510
2203
  * usage whose dollar price is absent or zero stays explicitly unknown through the live usage
1511
2204
  * stream and terminal artifact.
1512
2205
  *
2206
+ * ## What of the `AgentProfile` this executor honors, and what it does not
2207
+ *
2208
+ * This adapter is NOT a profile materializer. It reads a deliberately small part of
2209
+ * `spec.profile`, and a caller who needs the rest must lower it before handing the profile over.
2210
+ * Stated exhaustively so a silent drop is never a surprise:
2211
+ *
2212
+ * | Field | Status |
2213
+ * | --- | --- |
2214
+ * | `profile.name` | honored — trace `runId` label only, no behavioral effect |
2215
+ * | `profile.prompt.systemPrompt` | honored — prepended to the task text (pi RPC takes no separate system-prompt channel) |
2216
+ * | `profile.mcp` | honored — written to this execution's own file and passed as `--mcp-config` for `pi-mcp-adapter`; see `pi-mcp.ts` |
2217
+ * | `profile.extensions.pi.load` | honored — lowered to `--no-extensions` + `--extension <abs>` |
2218
+ * | `profile.prompt.instructions` | DROPPED — fold into `systemPrompt` before calling |
2219
+ * | `profile.model` | DROPPED — the seam's `model` is the only model channel; a profile that disagrees with the seam is silently overridden by the seam |
2220
+ * | `profile.model.reasoningEffort` | DROPPED — no `--thinking` flag is emitted, so pi's configured `defaultThinkingLevel` applies |
2221
+ * | `profile.tools` | DROPPED — no `--no-tools` / allow-deny mapping; pi runs its full builtin tool set |
2222
+ * | `profile.permissions` | DROPPED |
2223
+ * | `profile.resources` (context / skills / commands / subagents / instructions) | DROPPED — nothing is written into the run cwd |
2224
+ * | `profile.hooks` | DROPPED |
2225
+ * | `profile.subagents`, `profile.connections`, `profile.modes`, `profile.confidential` | DROPPED |
2226
+ * | `profile.resources.failOnError` | not consulted — the MCP path above is unconditionally fail-closed, which is the stricter reading |
2227
+ * | every other `profile.extensions.<ns>` | DROPPED — only the `pi` namespace is read |
2228
+ *
2229
+ * Closing those gaps belongs in `@tangle-network/agent-profile-materialize`, whose plan this
2230
+ * executor would then apply, rather than in a second mapping grown here.
2231
+ *
1513
2232
  * @experimental
1514
2233
  */
1515
2234
  /** The runtime name `piExecutor` registers under. */
@@ -1521,7 +2240,9 @@ const piExecutor = (spec, ctx) => {
1521
2240
  const seam = readPiSeam(ctx);
1522
2241
  const inbox = createInbox();
1523
2242
  const activity = createActivityLog(seam.activityWindow ?? 12);
1524
- const trace = createPushTraceSource({ runId: `pi-${spec.profile.name ?? "worker"}-${Date.now()}` });
2243
+ const runId = `pi-${spec.profile.name ?? "worker"}-${Date.now()}`;
2244
+ const trace = createPushTraceSource({ runId });
2245
+ const derived = [];
1525
2246
  const controller = new AbortController();
1526
2247
  const cascade = () => controller.abort();
1527
2248
  if (ctx.signal.aborted) controller.abort();
@@ -1540,6 +2261,7 @@ const piExecutor = (spec, ctx) => {
1540
2261
  turns: state.turns,
1541
2262
  pendingMessages: inbox.pending(),
1542
2263
  recentActivity: activity.read(),
2264
+ ...derived.length > 0 ? { derived: [...derived] } : {},
1543
2265
  note: state.note
1544
2266
  }),
1545
2267
  traceSource: () => trace.source,
@@ -1550,8 +2272,10 @@ const piExecutor = (spec, ctx) => {
1550
2272
  controller,
1551
2273
  seam,
1552
2274
  spec,
2275
+ runId,
1553
2276
  inbox,
1554
2277
  activity,
2278
+ derived,
1555
2279
  record: (step) => {
1556
2280
  trace.record(step);
1557
2281
  },
@@ -1588,7 +2312,28 @@ async function* streamPiSession(args) {
1588
2312
  let usdKnown = true;
1589
2313
  throwIfAborted(args.signal);
1590
2314
  throwIfAborted(args.controller.signal);
1591
- const proc = spawnPi(seam);
2315
+ const piMcp = preparePiMcp(args.spec.profile, {
2316
+ ...seam.cwd !== void 0 ? { cwd: seam.cwd } : {},
2317
+ runId: args.runId
2318
+ });
2319
+ if (piMcp.receipt) {
2320
+ const injected = piMcp.receipt.adapterInjected ? ` (+${PI_MCP_ADAPTER} added)` : "";
2321
+ activity.push({
2322
+ at: Date.now(),
2323
+ kind: "note",
2324
+ label: "mcp",
2325
+ detail: `${piMcp.receipt.servers.join(", ")}${injected}`
2326
+ });
2327
+ args.derived.push(`mcp: mounted ${piMcp.receipt.servers.join(", ")} via --mcp-config ${piMcp.receipt.configPath}`);
2328
+ if (piMcp.receipt.adapterInjected) args.derived.push(`extensions: added ${PI_MCP_ADAPTER} to extensions.pi.load — the profile's own list would have run under --no-extensions and suppressed it, mounting zero servers`);
2329
+ }
2330
+ let proc;
2331
+ try {
2332
+ proc = spawnPi(seam, piMcp.args);
2333
+ } catch (spawnFailure) {
2334
+ piMcp.mount?.cleanup();
2335
+ throw spawnFailure;
2336
+ }
1592
2337
  state.proc = proc;
1593
2338
  state.note = "connected";
1594
2339
  const events = [];
@@ -1704,6 +2449,7 @@ async function* streamPiSession(args) {
1704
2449
  args.signal.removeEventListener("abort", abortAll);
1705
2450
  args.controller.signal.removeEventListener("abort", abortAll);
1706
2451
  await killPi(proc, 2e3).catch(() => ({ destroyed: false }));
2452
+ piMcp.mount?.cleanup();
1707
2453
  }
1708
2454
  if (args.signal.aborted || args.controller.signal.aborted) throw abortError();
1709
2455
  if (!lastAssistant) throw new ValidationError("piExecutor: agent_settled without an assistant turn");
@@ -1717,12 +2463,14 @@ async function* streamPiSession(args) {
1717
2463
  ...usdKnown ? {} : { usdKnown: false },
1718
2464
  ms: Date.now() - started
1719
2465
  };
2466
+ const out = {
2467
+ content: state.lastText,
2468
+ turns: state.turns,
2469
+ ...piMcp.receipt ? { mcp: piMcp.receipt } : {}
2470
+ };
1720
2471
  state.artifact = {
1721
2472
  outRef: `pi:${hash(state.lastText)}`,
1722
- out: {
1723
- content: state.lastText,
1724
- turns: state.turns
1725
- },
2473
+ out,
1726
2474
  spent
1727
2475
  };
1728
2476
  }
@@ -1896,8 +2644,13 @@ function taskText(task) {
1896
2644
  return String(task);
1897
2645
  }
1898
2646
  }
1899
- /** Launch `pi --mode rpc`, plus provider/model flags derived from the seam's `provider/model`. */
1900
- function spawnPi(seam) {
2647
+ /**
2648
+ * Launch `pi --mode rpc`, plus provider/model flags derived from the seam's `provider/model` and
2649
+ * the profile-derived extension flags (`--no-extensions` / `--extension`). Seam args go LAST so an
2650
+ * operator's explicit flag wins over a derived one under pi's last-flag-wins parsing; RPC mode has
2651
+ * no positional prompt, so nothing here has to precede an argument.
2652
+ */
2653
+ function spawnPi(seam, profileArgs = []) {
1901
2654
  const bin = seam.bin ?? "pi";
1902
2655
  const argv = ["--mode", "rpc"];
1903
2656
  if (seam.model) {
@@ -1905,6 +2658,7 @@ function spawnPi(seam) {
1905
2658
  if (slash > 0) argv.push("--provider", seam.model.slice(0, slash), "--model", seam.model.slice(slash + 1));
1906
2659
  else argv.push("--model", seam.model);
1907
2660
  }
2661
+ argv.push(...profileArgs);
1908
2662
  if (seam.args) argv.push(...seam.args);
1909
2663
  return spawn(bin, argv, {
1910
2664
  ...seam.cwd ? { cwd: seam.cwd } : {},
@@ -4570,7 +5324,7 @@ function createCoordinationTools(opts) {
4570
5324
  },
4571
5325
  {
4572
5326
  name: "observe_agent",
4573
- description: "Inspect a worker WHILE IT RUNS, not only after it finishes: status, spend so far, and `progress` — how long since it last did anything (`idleMs`), whether that counts as stalled, how many turns it has taken, the last tools/files it touched (`recentActivity`), whether a steer can even reach it (`steerable`), and how many steers it has not yet read (`pendingMessages`). Returns the settled output artifact once it exists. Use this BEFORE steer_agent: a steer is only worth sending when the progress says the worker is on the wrong path or has stopped making any.",
5327
+ description: "Inspect a worker WHILE IT RUNS, not only after it finishes: status, spend so far, and `progress` — how long since it last did anything (`idleMs`), whether that counts as stalled, how many turns it has taken, the last tools/files it touched (`recentActivity`), what its executor CHANGED about the profile you gave it (`derived` — an MCP config it materialized, an extension it had to add), whether a steer can even reach it (`steerable`), and how many steers it has not yet read (`pendingMessages`). Returns the settled output artifact once it exists. Use this BEFORE steer_agent: a steer is only worth sending when the progress says the worker is on the wrong path or has stopped making any.",
4574
5328
  inputSchema: {
4575
5329
  type: "object",
4576
5330
  properties: { workerId: idArg },
@@ -5435,22 +6189,24 @@ function driverAgent(opts) {
5435
6189
  let driverTurn = 0;
5436
6190
  const meteredBrain = async (messages, tools, detail) => {
5437
6191
  const res = await opts.brain(messages, tools);
5438
- if (res.usage || res.costUsd !== void 0) {
5439
- const turnSpend = {
5440
- iterations: 0,
5441
- tokens: {
5442
- input: res.usage?.input ?? 0,
5443
- output: res.usage?.output ?? 0
5444
- },
5445
- usd: res.costUsd ?? 0,
5446
- ms: 0
5447
- };
5448
- await scope.meter(turnSpend, {
5449
- driver: opts.name,
5450
- toolCalls: (res.toolCalls ?? []).map((c) => c.name),
5451
- ...detail
5452
- });
5453
- }
6192
+ const nothingReported = res.usage === void 0 && res.costUsd === void 0;
6193
+ const turnSpend = {
6194
+ iterations: 0,
6195
+ tokens: {
6196
+ input: res.usage?.input ?? 0,
6197
+ output: res.usage?.output ?? 0
6198
+ },
6199
+ ...res.usage === void 0 ? { tokensKnown: false } : {},
6200
+ usd: res.costUsd ?? 0,
6201
+ ...nothingReported ? { usdKnown: false } : {},
6202
+ ms: 0
6203
+ };
6204
+ await scope.meter(turnSpend, {
6205
+ driver: opts.name,
6206
+ toolCalls: (res.toolCalls ?? []).map((c) => c.name),
6207
+ ...res.usageUnknown === true ? { streamUsageMissing: true } : {},
6208
+ ...detail
6209
+ });
5454
6210
  return res;
5455
6211
  };
5456
6212
  const chat = async (messages, tools) => {
@@ -7912,7 +8668,13 @@ function supervise(profile, task, opts) {
7912
8668
  ...opts.maxTurns !== void 0 ? { maxTurns: opts.maxTurns } : {},
7913
8669
  ...opts.compaction ? { compaction: opts.compaction } : {}
7914
8670
  });
7915
- return createSupervisor().run(agent, task, {
8671
+ const spans = opts.otel ? createSupervisorSpanRecorder({
8672
+ runId,
8673
+ ...opts.otel,
8674
+ now
8675
+ }) : void 0;
8676
+ const hooks = spans ? composeRuntimeHooks(opts.hooks, spans.hooks) : opts.hooks;
8677
+ const run = createSupervisor().run(agent, task, {
7916
8678
  budget: opts.budget,
7917
8679
  runId,
7918
8680
  journal: opts.journal ?? ctx.journal,
@@ -7921,12 +8683,22 @@ function supervise(profile, task, opts) {
7921
8683
  maxDepth: opts.maxDepth ?? 8,
7922
8684
  ...probes ? { probes } : {},
7923
8685
  ...ctx.resume === true ? { resume: true } : {},
7924
- ...opts.now ? { now: opts.now } : {}
8686
+ ...opts.now ? { now: opts.now } : {},
8687
+ ...hooks ? { hooks } : {}
7925
8688
  });
8689
+ if (!spans) return run;
8690
+ try {
8691
+ const result = await run;
8692
+ await spans.finish({ result });
8693
+ return result;
8694
+ } catch (error) {
8695
+ await spans.finish({ error });
8696
+ throw error;
8697
+ }
7926
8698
  };
7927
8699
  return start();
7928
8700
  }
7929
8701
  //#endregion
7930
- export { InMemoryDelegationStore as $, DELEGATE_INPUT_SCHEMA as A, createWorktreeCliExecutor as At, profileRichnessFinding as B, createSandboxForSpec as Bt, validateDelegateUiAuditArgs as C, defaultToolDetectors as Ct, createDelegateFeedbackHandler as D, cliWorktreeExecutor as Dt, DELEGATE_FEEDBACK_TOOL_NAME as E, createInMemoryRunContext as Et, delegate as F, piSeamKey as Ft, DELEGATION_TRACE_MAX_SPANS as G, probeSandboxCapabilities as Gt, DelegationTaskQueue as H, runAgentRounds as Ht, asAuthoredProfile as I, createPushTraceSource as It, composeLoopTraceEmitters as J, assertModelAllowed as Jt, buildDelegationTraceSpans as K, acquireSandbox as Kt, assessAuthoredProfile as L, decodeToolPart as Lt, createDelegateHandler as M, createSteerableSandboxSession as Mt, validateDelegateArgs as N, PI_RUNTIME as Nt, validateDelegateFeedbackArgs as O, createExecutor as Ot, defaultDelegateBudget as P, piExecutor as Pt, FileDelegationStore as Q, authoredWorker as R, sandboxSessionTraceSource as Rt, createDelegateUiAuditHandler as S, rollingDispatch as St, DELEGATE_FEEDBACK_INPUT_SCHEMA as T, createFileRunContext as Tt, hashIdempotencyInput as U, runLoop as Ut, supervisorInstructions as V, defaultSelectWinner as Vt, DELEGATION_TRACE_MAX_BYTES as W, createSandboxLineage as Wt, DelegationPersistenceError as X, createDelegationTraceCollector as Y, gateOnDeliverable as Yt, DelegationStateCorruptError as Z, createDelegationHistoryHandler as _, createCoordinationTools as _t, supervisorAgent as a, allWorkersStalled as at, DELEGATE_UI_AUDIT_INPUT_SCHEMA as b, freeSlots as bt, createMcpServer as c, noProgressFor as ct, DELEGATION_STATUS_TOOL_NAME as d, anytimeReport as dt, InMemoryFeedbackStore as et, createDelegationStatusHandler as f, areaUnderCurve as ft, DELEGATION_HISTORY_TOOL_NAME as g, DEFAULT_AWAIT_EVENT_TIMEOUT_MS as gt, DELEGATION_HISTORY_INPUT_SCHEMA as h, renderAnytimeTable as ht, resolveSupervisorProfile as i, allOf as it, DELEGATE_TOOL_NAME as j, DEFAULT_SANDBOX_STEERING_MAX_TURNS as jt, DELEGATE_DESCRIPTION as k, createExecutorRegistry as kt, DELEGATION_STATUS_DESCRIPTION as l, plateau as lt, DELEGATION_HISTORY_DESCRIPTION as m, plateauLength as mt, workerFromBackend as n, driverAgent as nt, serveCoordinationMcp as o, anyOf as ot, validateDelegationStatusArgs as p, bestSoFar as pt, capDelegationTrace as q, FileCoordinationLog as qt, assertCoordinationBinding as r, finalizeBestDelivered as rt, createInProcessTransport as s, createProgressTracker as st, supervise as t, eventToSnapshot as tt, DELEGATION_STATUS_INPUT_SCHEMA as u, sampleFromSettled as ut, validateDelegationHistoryArgs as v, createEventBus as vt, DELEGATE_FEEDBACK_DESCRIPTION as w, watchTrace as wt, DELEGATE_UI_AUDIT_TOOL_NAME as x, queueOf as xt, DELEGATE_UI_AUDIT_DESCRIPTION as y, effectiveConcurrency as yt, defaultProfileRichnessThresholds as z, createInbox as zt };
8702
+ export { InMemoryDelegationStore as $, FileCoordinationLog as $t, DELEGATE_INPUT_SCHEMA as A, createWorktreeCliExecutor as At, profileRichnessFinding as B, PI_MCP_ADAPTER_ENV as Bt, validateDelegateUiAuditArgs as C, defaultToolDetectors as Ct, createDelegateFeedbackHandler as D, cliWorktreeExecutor as Dt, DELEGATE_FEEDBACK_TOOL_NAME as E, createInMemoryRunContext as Et, delegate as F, piSeamKey as Ft, DELEGATION_TRACE_MAX_SPANS as G, createInbox as Gt, DelegationTaskQueue as H, buildPiMcpServers as Ht, asAuthoredProfile as I, createPushTraceSource as It, composeLoopTraceEmitters as J, runAgentRounds as Jt, buildDelegationTraceSpans as K, createSandboxForSpec as Kt, assessAuthoredProfile as L, decodeToolPart as Lt, createDelegateHandler as M, createSteerableSandboxSession as Mt, validateDelegateArgs as N, PI_RUNTIME as Nt, validateDelegateFeedbackArgs as O, createExecutor as Ot, defaultDelegateBudget as P, piExecutor as Pt, FileDelegationStore as Q, acquireSandbox as Qt, authoredWorker as R, sandboxSessionTraceSource as Rt, createDelegateUiAuditHandler as S, rollingDispatch as St, DELEGATE_FEEDBACK_INPUT_SCHEMA as T, createFileRunContext as Tt, hashIdempotencyInput as U, piMcpAdapterAvailable as Ut, supervisorInstructions as V, PI_MCP_CONFIG_FLAG as Vt, DELEGATION_TRACE_MAX_BYTES as W, preparePiMcp as Wt, DelegationPersistenceError as X, createSandboxLineage as Xt, createDelegationTraceCollector as Y, runLoop as Yt, DelegationStateCorruptError as Z, probeSandboxCapabilities as Zt, createDelegationHistoryHandler as _, createCoordinationTools as _t, supervisorAgent as a, allWorkersStalled as at, DELEGATE_UI_AUDIT_INPUT_SCHEMA as b, freeSlots as bt, createMcpServer as c, noProgressFor as ct, DELEGATION_STATUS_TOOL_NAME as d, anytimeReport as dt, createSupervisorSpanRecorder as en, InMemoryFeedbackStore as et, createDelegationStatusHandler as f, areaUnderCurve as ft, DELEGATION_HISTORY_TOOL_NAME as g, DEFAULT_AWAIT_EVENT_TIMEOUT_MS as gt, DELEGATION_HISTORY_INPUT_SCHEMA as h, renderAnytimeTable as ht, resolveSupervisorProfile as i, allOf as it, DELEGATE_TOOL_NAME as j, DEFAULT_SANDBOX_STEERING_MAX_TURNS as jt, DELEGATE_DESCRIPTION as k, createExecutorRegistry as kt, DELEGATION_STATUS_DESCRIPTION as l, plateau as lt, DELEGATION_HISTORY_DESCRIPTION as m, plateauLength as mt, workerFromBackend as n, gateOnDeliverable as nn, driverAgent as nt, serveCoordinationMcp as o, anyOf as ot, validateDelegationStatusArgs as p, bestSoFar as pt, capDelegationTrace as q, defaultSelectWinner as qt, assertCoordinationBinding as r, finalizeBestDelivered as rt, createInProcessTransport as s, createProgressTracker as st, supervise as t, assertModelAllowed as tn, eventToSnapshot as tt, DELEGATION_STATUS_INPUT_SCHEMA as u, sampleFromSettled as ut, validateDelegationHistoryArgs as v, createEventBus as vt, DELEGATE_FEEDBACK_DESCRIPTION as w, watchTrace as wt, DELEGATE_UI_AUDIT_TOOL_NAME as x, queueOf as xt, DELEGATE_UI_AUDIT_DESCRIPTION as y, effectiveConcurrency as yt, defaultProfileRichnessThresholds as z, PI_MCP_ADAPTER as zt };
7931
8703
 
7932
- //# sourceMappingURL=supervise-BDvAFOTM.js.map
8704
+ //# sourceMappingURL=supervise-BcCHhLrz.js.map