@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.
- package/dist/{activation-UZ1JenNv.js → activation-BNQQO-bc.js} +3 -3
- package/dist/{activation-UZ1JenNv.js.map → activation-BNQQO-bc.js.map} +1 -1
- package/dist/agent.d.ts +18 -7
- package/dist/agent.js +123 -64
- package/dist/agent.js.map +1 -1
- package/dist/candidate-execution/index.js +4 -4
- package/dist/{candidate-execution-Co3sRC_7.js → candidate-execution-CfpJrd3o.js} +4 -4
- package/dist/{candidate-execution-Co3sRC_7.js.map → candidate-execution-CfpJrd3o.js.map} +1 -1
- package/dist/{environment-provider-BsHpW4Dp.js → environment-provider-CCaEhA-l.js} +38 -4
- package/dist/environment-provider-CCaEhA-l.js.map +1 -0
- package/dist/{environment-provider-DG-cSx89.d.ts → environment-provider-Cygthiy3.d.ts} +40 -1
- package/dist/environment-provider.d.ts +1 -1
- package/dist/environment-provider.js +1 -1
- package/dist/{improvement-cycle-C3CrNfcy.js → improvement-cycle-CBLOgfsQ.js} +4 -4
- package/dist/{improvement-cycle-C3CrNfcy.js.map → improvement-cycle-CBLOgfsQ.js.map} +1 -1
- package/dist/{index-Cr7QlEUL.d.ts → index-B_vcAAle.d.ts} +6 -6
- package/dist/{index-zMAqz53v.d.ts → index-BuzkRsOn.d.ts} +244 -9
- package/dist/{index-B6WclhZI.d.ts → index-I97xACXk.d.ts} +3 -3
- package/dist/index.d.ts +8 -8
- package/dist/index.js +13 -13
- package/dist/intelligence.d.ts +3 -3
- package/dist/intelligence.js +6 -6
- package/dist/kernel.d.ts +4 -4
- package/dist/kernel.js +7 -7
- package/dist/{knowledge-GAJFl_gz.js → knowledge-DcIKJaPd.js} +5 -5
- package/dist/{knowledge-GAJFl_gz.js.map → knowledge-DcIKJaPd.js.map} +1 -1
- package/dist/knowledge.d.ts +1 -1
- package/dist/knowledge.js +1 -1
- package/dist/{local-harness-Ce1emWJE.d.ts → local-harness-BnB1E9nM.d.ts} +82 -2
- package/dist/{loop-runner-bin-DvLdFo38.js → loop-runner-bin-C2GIqSUn.js} +3 -3
- package/dist/{loop-runner-bin-DvLdFo38.js.map → loop-runner-bin-C2GIqSUn.js.map} +1 -1
- package/dist/{loop-runner-bin-Cba5tsdb.d.ts → loop-runner-bin-DgAXgQCI.d.ts} +3 -3
- package/dist/loop-runner-bin.d.ts +1 -1
- package/dist/loop-runner-bin.js +1 -1
- package/dist/mcp/bin.js +2 -2
- package/dist/mcp/index.d.ts +2 -2
- package/dist/mcp/index.js +5 -5
- package/dist/{openai-tools-DftmzKLc.js → openai-tools-BWAEIHkZ.js} +2 -2
- package/dist/{openai-tools-DftmzKLc.js.map → openai-tools-BWAEIHkZ.js.map} +1 -1
- package/dist/{otel-export-D34IQW1C.js → otel-export-CPZTSADj.js} +16 -7
- package/dist/otel-export-CPZTSADj.js.map +1 -0
- package/dist/{prepare-BKVdOn0w.js → prepare-BHQBb02e.js} +2 -2
- package/dist/{prepare-BKVdOn0w.js.map → prepare-BHQBb02e.js.map} +1 -1
- package/dist/primeintellect/index.d.ts +1 -1
- package/dist/{protected-model-port-BtldAE9j.js → protected-model-port-DqAH1Z2M.js} +2 -2
- package/dist/{protected-model-port-BtldAE9j.js.map → protected-model-port-DqAH1Z2M.js.map} +1 -1
- package/dist/{redact-xZvPu9qe.d.ts → redact-BRaHUFMf.d.ts} +2 -2
- package/dist/{runtime-DlSPFm9o.js → runtime-C_g_SsXg.js} +8 -5
- package/dist/runtime-C_g_SsXg.js.map +1 -0
- package/dist/{spawn-journal-DLhnom3e.js → spawn-journal-DCPbicXB.js} +2 -1
- package/dist/{spawn-journal-DLhnom3e.js.map → spawn-journal-DCPbicXB.js.map} +1 -1
- package/dist/{structural-rollout-R_ZqFaB5.js → structural-rollout-Cqm1dKa7.js} +3 -3
- package/dist/{structural-rollout-R_ZqFaB5.js.map → structural-rollout-Cqm1dKa7.js.map} +1 -1
- package/dist/{supervise-BDvAFOTM.js → supervise-BcCHhLrz.js} +807 -35
- package/dist/supervise-BcCHhLrz.js.map +1 -0
- package/dist/{supervisor-DS278ICs.js → supervisor-CspDspod.js} +250 -22
- package/dist/supervisor-CspDspod.js.map +1 -0
- package/dist/testing.js +9 -9
- package/dist/{trace-propagation-CJJC7SVB.js → trace-propagation-B-pL7xn_.js} +2 -2
- package/dist/{trace-propagation-CJJC7SVB.js.map → trace-propagation-B-pL7xn_.js.map} +1 -1
- package/dist/{workspace-archive-CYYxZyA7.js → workspace-archive-DXzJq7WP.js} +2 -2
- package/dist/{workspace-archive-CYYxZyA7.js.map → workspace-archive-DXzJq7WP.js.map} +1 -1
- package/package.json +1 -1
- package/dist/environment-provider-BsHpW4Dp.js.map +0 -1
- package/dist/otel-export-D34IQW1C.js.map +0 -1
- package/dist/runtime-DlSPFm9o.js.map +0 -1
- package/dist/supervise-BDvAFOTM.js.map +0 -1
- 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-
|
|
4
|
-
import { A as
|
|
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 {
|
|
8
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
1900
|
-
|
|
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
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
}
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
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
|
-
|
|
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,
|
|
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-
|
|
8704
|
+
//# sourceMappingURL=supervise-BcCHhLrz.js.map
|