@tangle-network/agent-runtime 0.219.0 → 0.220.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{activation-C-8VqFSN.js → activation-D3yrSA_b.js} +2 -2
- package/dist/{activation-C-8VqFSN.js.map → activation-D3yrSA_b.js.map} +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +2 -2
- package/dist/{coordination-driver-CvinSUI-.js → coordination-driver-BxGh-Tnj.js} +599 -355
- package/dist/coordination-driver-BxGh-Tnj.js.map +1 -0
- package/dist/{delegate-BH8r8hkQ.js → delegate-CiGfF17V.js} +2 -2
- package/dist/{delegate-BH8r8hkQ.js.map → delegate-CiGfF17V.js.map} +1 -1
- package/dist/durable.d.ts +1 -1
- package/dist/durable.js +2 -2
- package/dist/{graph-5N4gLnOt.js → graph-D9tSg6Vd.js} +3 -3
- package/dist/{graph-5N4gLnOt.js.map → graph-D9tSg6Vd.js.map} +1 -1
- package/dist/{improvement-cycle-BIAKVZ3B.js → improvement-cycle-BZ_KfBap.js} +3 -3
- package/dist/{improvement-cycle-BIAKVZ3B.js.map → improvement-cycle-BZ_KfBap.js.map} +1 -1
- package/dist/{index-CLDpu-bs.d.ts → index-B9_U4zYI.d.ts} +16 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -7
- package/dist/intelligence.js +3 -3
- package/dist/kernel.d.ts +1 -1
- package/dist/kernel.js +8 -8
- package/dist/{loop-runner-bin-Cn9DBrLv.d.ts → loop-runner-bin-BaeOlnpi.d.ts} +2 -2
- package/dist/{loop-runner-bin-CTEJAcac.js → loop-runner-bin-DkU6vtuc.js} +3 -3
- package/dist/{loop-runner-bin-CTEJAcac.js.map → loop-runner-bin-DkU6vtuc.js.map} +1 -1
- package/dist/loop-runner-bin.d.ts +1 -1
- package/dist/loop-runner-bin.js +1 -1
- package/dist/mcp/bin.js +3 -3
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +4 -4
- package/dist/{provision-supervisor-40EOGn14.js → provision-supervisor-Cg3fETK-.js} +3 -3
- package/dist/{provision-supervisor-40EOGn14.js.map → provision-supervisor-Cg3fETK-.js.map} +1 -1
- package/dist/{runtime-BDK_GOAd.js → runtime-DZcPHLwa.js} +8 -8
- package/dist/{runtime-BDK_GOAd.js.map → runtime-DZcPHLwa.js.map} +1 -1
- package/dist/{server-DuvbSAVA.js → server-BgPZIHRd.js} +3 -3
- package/dist/{server-DuvbSAVA.js.map → server-BgPZIHRd.js.map} +1 -1
- package/dist/{structural-rollout-BAr61WjX.js → structural-rollout-iCePfrcK.js} +2 -2
- package/dist/{structural-rollout-BAr61WjX.js.map → structural-rollout-iCePfrcK.js.map} +1 -1
- package/dist/{supervise-W_mtzswa.js → supervise-CSYu0BeH.js} +440 -726
- package/dist/supervise-CSYu0BeH.js.map +1 -0
- package/dist/{supervisor-WRrw0nlk.js → supervisor-n8DDNGli.js} +368 -31
- package/dist/supervisor-n8DDNGli.js.map +1 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +12 -12
- package/dist/tui/index.d.ts +1 -1
- package/dist/tui/index.js +1 -1
- package/package.json +1 -1
- package/dist/coordination-driver-CvinSUI-.js.map +0 -1
- package/dist/supervise-W_mtzswa.js.map +0 -1
- package/dist/supervisor-WRrw0nlk.js.map +0 -1
|
@@ -1,385 +1,355 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $r as parseCommittedJsonLines, Cn as chargedTokens, Ct as workerTraceAnalysisStore, Ht as createPeerMailbox, J as freeSlots, Kt as isLiveNodeStatus, Ln as addResourceSpend, Lt as createInbox, On as promptCacheTokenClasses, Pn as unmeteredSpend, Qr as isNoEntError, Si as detachedFrozen, St as parseWorkerToolTraceArtifact, Tt as assertValidBudget, a as pickBestDelivered, b as meterRuntimeOwnedProviderAttempt, dt as FileSpawnJournal, ei as prepareJsonlAppend, ft as InMemoryResultBlobStore, h as withDriverExecutor, it as createExecutorRegistry, nr as resolveRedactor, o as runFinalizer, pn as runBrainLoop, pt as InMemorySpawnJournal, qt as isTerminalNodeStatus, r as bestDelivered, s as runTree, ti as writeAllBytes, ui as providerAttemptEvidence, ut as FileResultBlobStore, zn as withBudgetResources } from "./supervisor-n8DDNGli.js";
|
|
2
2
|
import { f as RuntimeRunStateError, m as ValidationError } from "./errors-DodWX-cb.js";
|
|
3
3
|
import { A as writeWorkerCancellation, M as writeWorkerSteerAcknowledgement, b as workerCancellationsDir, c as readWorkerCancelRequests, d as readWorkerSteerRequests, k as writeRunCancellation, l as readWorkerCancellation, o as readRunCancelRequest, r as claimWorkerSteerDelivery, s as readRunCancellation, u as readWorkerSteerAcknowledgement } from "./run-layout-hZREkcfu.js";
|
|
4
4
|
import { agentProfileSchema, canonicalCandidateDigest } from "@tangle-network/agent-interface";
|
|
5
5
|
import { argHash, errorStreakDetector, observeAll, repeatedActionDetector } from "@tangle-network/agent-eval";
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { mkdirSync, readFileSync, watch } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return record;
|
|
8
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
9
|
+
import { open, realpath, stat } from "node:fs/promises";
|
|
10
|
+
//#region src/runtime/supervise/detector-monitor.ts
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
* The ONLINE analyst: watch a `TraceSource` and fold each tool span through agent-eval's published
|
|
14
|
+
* streaming detector kernel (`repeatedActionDetector`/`errorStreakDetector` — the SAME kernel the
|
|
15
|
+
* control loop folds), firing `onSignal` the moment a worker loops or error-storms. Substrate-
|
|
16
|
+
* agnostic: it consumes spans from any source (owned router/bridge loop OR a sandbox box session),
|
|
17
|
+
* never the raw tool seam. Detection logic + the failure taxonomy live in agent-eval; not reimplemented.
|
|
18
|
+
*
|
|
19
|
+
* @experimental
|
|
20
|
+
*/
|
|
21
|
+
/** The default online panel for a tool-call pipe: a worker repeating the same call, or hammering
|
|
22
|
+
* consecutive errors. (No-progress needs a domain progress-probe, so it is opt-in, not default.)
|
|
23
|
+
*
|
|
24
|
+
* Coverage note: `repeated-action` works for EVERY harness (it needs only tool name + args, which
|
|
25
|
+
* every adapter provides). `error-streak` needs per-call status — opencode carries it inline
|
|
26
|
+
* (`state.status`, VALIDATED live), but claude-code/codex tool-call parts do NOT (their errors live
|
|
27
|
+
* in separate result blocks not yet decoded), so error-streak is silent for those until result-block
|
|
28
|
+
* decoding is added + live-validated. It is in the panel because it is correct where status exists. */
|
|
29
|
+
function defaultToolDetectors() {
|
|
30
|
+
return [repeatedActionDetector({ maxRepeated: 3 }), errorStreakDetector({ maxErrors: 3 })];
|
|
32
31
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (failure !== void 0) throw failure;
|
|
40
|
-
if (active) applyRunCancellation(dir, abortRun, () => (/* @__PURE__ */ new Date()).toISOString(), path);
|
|
41
|
-
};
|
|
42
|
-
const fail = (error) => {
|
|
43
|
-
if (!active) return;
|
|
44
|
-
failure = error;
|
|
45
|
-
abortRun(`durable run cancellation observer failed: ${String(error)}`);
|
|
46
|
-
};
|
|
47
|
-
const watcher = watch(workerCancellationsDir(dir), (_event, filename) => {
|
|
48
|
-
if (!active || filename !== null && filename !== "run.request.json") return;
|
|
32
|
+
/** Subscribe to a `TraceSource` and run the streaming detectors over its live spans. Returns an
|
|
33
|
+
* unsubscribe. A defensive `argHash` failure (circular args) never throws out of the side-channel. */
|
|
34
|
+
function watchTrace(source, opts = {}) {
|
|
35
|
+
const detectors = opts.detectors ?? defaultToolDetectors();
|
|
36
|
+
return source.onSpan((span) => {
|
|
37
|
+
let fingerprint;
|
|
49
38
|
try {
|
|
50
|
-
|
|
51
|
-
} catch
|
|
52
|
-
|
|
39
|
+
fingerprint = `${span.toolName}|${argHash(span.args)}`;
|
|
40
|
+
} catch {
|
|
41
|
+
fingerprint = `${span.toolName}|<unhashable>`;
|
|
53
42
|
}
|
|
43
|
+
const signals = observeAll(detectors, {
|
|
44
|
+
actionFingerprint: fingerprint,
|
|
45
|
+
...span.status ? { status: span.status } : {},
|
|
46
|
+
label: span.toolName
|
|
47
|
+
});
|
|
48
|
+
for (const s of signals) opts.onSignal?.(s, span);
|
|
54
49
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/runtime/supervise/event-bus.ts
|
|
53
|
+
/** Create the child→parent coordination bus: one typed pipe for settled outputs, questions, and analyst findings, with a priority-ordered pull queue and a pass-through subscribe lane.
|
|
54
|
+
* @experimental In-process queue; durability is a transport swap that does not exist yet. */
|
|
55
|
+
function createEventBus(now = Date.now) {
|
|
56
|
+
const queue = [];
|
|
57
|
+
const log = [];
|
|
58
|
+
const subscribers = [];
|
|
59
|
+
const byKind = {};
|
|
60
|
+
const staged = /* @__PURE__ */ new WeakMap();
|
|
61
|
+
let seq = 0;
|
|
62
|
+
let published = 0;
|
|
63
|
+
let pulled = 0;
|
|
64
|
+
const matches = (r, kinds) => !kinds || kinds.includes(r.event.type);
|
|
65
|
+
const bestIndex = (kinds) => {
|
|
66
|
+
let best = -1;
|
|
67
|
+
let bestPriority = Number.NEGATIVE_INFINITY;
|
|
68
|
+
for (let i = 0; i < queue.length; i++) {
|
|
69
|
+
const r = queue[i];
|
|
70
|
+
if (!r || !matches(r, kinds)) continue;
|
|
71
|
+
if (r.priority > bestPriority) {
|
|
72
|
+
best = i;
|
|
73
|
+
bestPriority = r.priority;
|
|
74
|
+
}
|
|
61
75
|
}
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
return best;
|
|
77
|
+
};
|
|
64
78
|
return {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
async publish(event, opts) {
|
|
80
|
+
const record = staged.get(event) ?? {
|
|
81
|
+
seq: seq++,
|
|
82
|
+
at: now(),
|
|
83
|
+
priority: opts?.priority ?? 0,
|
|
84
|
+
event
|
|
85
|
+
};
|
|
86
|
+
staged.set(event, record);
|
|
87
|
+
for (const handler of subscribers) await handler(record);
|
|
88
|
+
staged.delete(event);
|
|
89
|
+
if (opts?.queue !== false) queue.push(record);
|
|
90
|
+
log.push(record);
|
|
91
|
+
published += 1;
|
|
92
|
+
byKind[event.type] = (byKind[event.type] ?? 0) + 1;
|
|
93
|
+
return record;
|
|
94
|
+
},
|
|
95
|
+
pull(kinds) {
|
|
96
|
+
const i = bestIndex(kinds);
|
|
97
|
+
if (i < 0) return void 0;
|
|
98
|
+
pulled++;
|
|
99
|
+
return queue.splice(i, 1)[0]?.event;
|
|
100
|
+
},
|
|
101
|
+
subscribe(handler) {
|
|
102
|
+
subscribers.push(handler);
|
|
103
|
+
return () => {
|
|
104
|
+
const i = subscribers.indexOf(handler);
|
|
105
|
+
if (i >= 0) subscribers.splice(i, 1);
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
pending(kinds) {
|
|
109
|
+
return kinds ? queue.filter((r) => matches(r, kinds)).length : queue.length;
|
|
110
|
+
},
|
|
111
|
+
history() {
|
|
112
|
+
return log;
|
|
113
|
+
},
|
|
114
|
+
stats() {
|
|
115
|
+
return {
|
|
116
|
+
published,
|
|
117
|
+
pulled,
|
|
118
|
+
byKind: { ...byKind }
|
|
119
|
+
};
|
|
70
120
|
}
|
|
71
121
|
};
|
|
72
122
|
}
|
|
73
123
|
//#endregion
|
|
74
|
-
//#region src/
|
|
124
|
+
//#region src/mcp/tools/spawn-resource-paths.ts
|
|
75
125
|
/**
|
|
76
|
-
*
|
|
77
|
-
* findings, answer decisions, authorized continuation receipts, delivery-attempt markers, and
|
|
78
|
-
* delivery outcomes. A durable run
|
|
79
|
-
* (`supervise({ runDir })`) appends them as they publish and loads them on resume, so a restarted
|
|
80
|
-
* coordinator retains the exact evidence produced by prior processes.
|
|
126
|
+
* Inline resources a manager hands a child by PATH instead of by content.
|
|
81
127
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
128
|
+
* The canonical profile schema knows two resource kinds, `inline` (a content string) and `github`.
|
|
129
|
+
* A manager that wants a child to receive a data file therefore had one way to do it: emit the
|
|
130
|
+
* bytes inside its own `spawn` tool call. That is a transcription by a language model, and it does
|
|
131
|
+
* not survive size. Measured 2026-09-12 (discovery-lab `mech-interp-foundations-glm2-b-20260912b`):
|
|
132
|
+
* a 29,144-character base64 payload on the manager's disk reached three children as 15,928
|
|
133
|
+
* characters with 11 substitutions, and a fourth child received the literal placeholder the
|
|
134
|
+
* manager meant to replace. Seven blind-check attempts across two runs delivered no data.
|
|
86
135
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
136
|
+
* `{ kind: 'inline', name, path }` lets the coordination server be the transport: it reads the
|
|
137
|
+
* file under the manager's workspace root, fills `content`, and the profile that reaches the
|
|
138
|
+
* schema, the journal, and the provider is an ordinary inline resource. Nothing downstream
|
|
139
|
+
* changes; the model's output stops carrying the bytes.
|
|
89
140
|
*
|
|
90
|
-
*
|
|
141
|
+
* Fail-closed rules, each with the reason a manager needs to fix its call:
|
|
142
|
+
* - no root configured → refused (a nested manager in a remote sandbox has no host directory
|
|
143
|
+
* the server may read for it)
|
|
144
|
+
* - absolute path, `..` escape, or a symlink that resolves outside the root → refused; the
|
|
145
|
+
* workspace root is the only directory a manager may hand out
|
|
146
|
+
* - not a regular file → refused
|
|
147
|
+
* - over {@link SPAWN_RESOURCE_PATH_MAX_BYTES} → refused naming the bound
|
|
148
|
+
* - bytes that are not valid UTF-8 → refused; `content` is a string, so binary data is encoded
|
|
149
|
+
* (base64) by the manager first, exactly as it would be for a content resource
|
|
91
150
|
*/
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return event.type !== "settled";
|
|
151
|
+
const SPAWN_RESOURCE_PATH_MAX_BYTES = 4 * 1024 * 1024;
|
|
152
|
+
function isRecord(value) {
|
|
153
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
96
154
|
}
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
155
|
+
/** An inline ref that names a path and carries no content. Anything else is left untouched so the
|
|
156
|
+
* canonical schema keeps its verdict on it. */
|
|
157
|
+
function pathInlineRef(value) {
|
|
158
|
+
if (!isRecord(value)) return void 0;
|
|
159
|
+
if (value.kind !== "inline" || value.content !== void 0) return void 0;
|
|
160
|
+
if (typeof value.path !== "string" || typeof value.name !== "string") return void 0;
|
|
161
|
+
return {
|
|
162
|
+
kind: "inline",
|
|
163
|
+
name: value.name,
|
|
164
|
+
path: value.path
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async function readUnderRoot(root, requested) {
|
|
168
|
+
if (requested.length === 0) return {
|
|
169
|
+
ok: false,
|
|
170
|
+
reason: "path is empty"
|
|
171
|
+
};
|
|
172
|
+
if (isAbsolute(requested)) return {
|
|
173
|
+
ok: false,
|
|
174
|
+
reason: `path must be relative to the workspace root, not absolute (${requested})`
|
|
175
|
+
};
|
|
176
|
+
const rootReal = await realpath(root).catch(() => void 0);
|
|
177
|
+
if (rootReal === void 0) return {
|
|
178
|
+
ok: false,
|
|
179
|
+
reason: `workspace root ${root} does not exist`
|
|
180
|
+
};
|
|
181
|
+
const candidate = resolve(rootReal, requested);
|
|
182
|
+
if (!inside(rootReal, candidate)) return {
|
|
183
|
+
ok: false,
|
|
184
|
+
reason: `path ${requested} leaves the workspace root`
|
|
185
|
+
};
|
|
186
|
+
let handle;
|
|
187
|
+
try {
|
|
188
|
+
handle = await open(candidate, "r");
|
|
189
|
+
} catch (error) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
reason: `path ${requested}: ${describe(error)}`
|
|
193
|
+
};
|
|
103
194
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
195
|
+
try {
|
|
196
|
+
const info = await handle.stat();
|
|
197
|
+
if (!info.isFile()) return {
|
|
198
|
+
ok: false,
|
|
199
|
+
reason: `path ${requested} is not a regular file`
|
|
200
|
+
};
|
|
201
|
+
const real = await realpath(candidate).catch(() => void 0);
|
|
202
|
+
if (real === void 0 || !inside(rootReal, real)) return {
|
|
203
|
+
ok: false,
|
|
204
|
+
reason: `path ${requested} resolves outside the workspace root`
|
|
205
|
+
};
|
|
206
|
+
const named = await stat(real).catch(() => void 0);
|
|
207
|
+
if (named === void 0 || named.dev !== info.dev || named.ino !== info.ino) return {
|
|
208
|
+
ok: false,
|
|
209
|
+
reason: `path ${requested} changed while it was being read; retry the spawn`
|
|
210
|
+
};
|
|
211
|
+
if (info.size > 4194304) return {
|
|
212
|
+
ok: false,
|
|
213
|
+
reason: `path ${requested} is ${info.size} bytes; an inline resource is at most ${SPAWN_RESOURCE_PATH_MAX_BYTES} bytes`
|
|
214
|
+
};
|
|
215
|
+
const bytes = await handle.readFile();
|
|
216
|
+
if (bytes.length > 4194304) return {
|
|
217
|
+
ok: false,
|
|
218
|
+
reason: `path ${requested} is ${bytes.length} bytes; an inline resource is at most ${SPAWN_RESOURCE_PATH_MAX_BYTES} bytes`
|
|
219
|
+
};
|
|
220
|
+
const content = bytes.toString("utf8");
|
|
221
|
+
if (!Buffer.from(content, "utf8").equals(bytes)) return {
|
|
222
|
+
ok: false,
|
|
223
|
+
reason: `path ${requested} is not valid UTF-8; encode binary data (base64) before handing it to a child`
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
ok: true,
|
|
227
|
+
content,
|
|
228
|
+
byteLength: bytes.length,
|
|
229
|
+
sha256: createHash("sha256").update(bytes).digest("hex")
|
|
230
|
+
};
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
reason: `path ${requested}: ${describe(error)}`
|
|
235
|
+
};
|
|
236
|
+
} finally {
|
|
237
|
+
await handle.close().catch(() => void 0);
|
|
109
238
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
239
|
+
}
|
|
240
|
+
/** `target` is `rootReal` itself or strictly below it; both must already be absolute. */
|
|
241
|
+
function inside(rootReal, target) {
|
|
242
|
+
const rel = relative(rootReal, target);
|
|
243
|
+
return rel !== "" && !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
|
|
244
|
+
}
|
|
245
|
+
function describe(error) {
|
|
246
|
+
const code = error?.code;
|
|
247
|
+
if (code === "ENOENT") return "does not exist under the workspace root";
|
|
248
|
+
if (code === "EACCES" || code === "EPERM") return "is not readable by the runtime process";
|
|
249
|
+
if (code === "EISDIR") return "is not a regular file";
|
|
250
|
+
return error instanceof Error ? error.message : String(error);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Replace every `{ kind: 'inline', name, path }` under `profile.resources` with the inline
|
|
254
|
+
* resource its bytes make. Returns the profile unchanged (same reference) when nothing names a
|
|
255
|
+
* path, so callers pay nothing on the common case.
|
|
256
|
+
*/
|
|
257
|
+
async function resolveSpawnResourcePaths(profile, root) {
|
|
258
|
+
if (!isRecord(profile) || !isRecord(profile.resources)) return {
|
|
259
|
+
ok: true,
|
|
260
|
+
profile,
|
|
261
|
+
resolved: []
|
|
262
|
+
};
|
|
263
|
+
const resources = profile.resources;
|
|
264
|
+
const resolved = [];
|
|
265
|
+
let touched = false;
|
|
266
|
+
const next = { ...resources };
|
|
267
|
+
const substitute = async (at, ref) => {
|
|
268
|
+
if (root === void 0) return { error: "this manager has no workspace root the coordination server may read, so a resource by path cannot be resolved; pass content, or a github resource" };
|
|
269
|
+
const read = await readUnderRoot(root, ref.path);
|
|
270
|
+
if (!read.ok) return { error: read.reason };
|
|
271
|
+
resolved.push({
|
|
272
|
+
at,
|
|
273
|
+
path: ref.path,
|
|
274
|
+
byteLength: read.byteLength,
|
|
275
|
+
sha256: read.sha256
|
|
276
|
+
});
|
|
277
|
+
touched = true;
|
|
278
|
+
return {
|
|
279
|
+
kind: "inline",
|
|
280
|
+
name: ref.name,
|
|
281
|
+
content: read.content
|
|
118
282
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
283
|
+
};
|
|
284
|
+
for (const list of [
|
|
285
|
+
"tools",
|
|
286
|
+
"skills",
|
|
287
|
+
"agents"
|
|
288
|
+
]) {
|
|
289
|
+
const entries = resources[list];
|
|
290
|
+
if (!Array.isArray(entries)) continue;
|
|
291
|
+
const out = [];
|
|
292
|
+
for (const [index, entry] of entries.entries()) {
|
|
293
|
+
const ref = pathInlineRef(entry);
|
|
294
|
+
if (ref === void 0) {
|
|
295
|
+
out.push(entry);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const at = `${list}[${index}]`;
|
|
299
|
+
const replaced = await substitute(at, ref);
|
|
300
|
+
if ("error" in replaced) return {
|
|
301
|
+
ok: false,
|
|
302
|
+
at,
|
|
303
|
+
reason: replaced.error
|
|
304
|
+
};
|
|
305
|
+
out.push(replaced);
|
|
126
306
|
}
|
|
307
|
+
next[list] = out;
|
|
127
308
|
}
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
309
|
+
if (Array.isArray(resources.files)) {
|
|
310
|
+
const out = [];
|
|
311
|
+
for (const [index, mount] of resources.files.entries()) {
|
|
312
|
+
const ref = isRecord(mount) ? pathInlineRef(mount.resource) : void 0;
|
|
313
|
+
if (ref === void 0 || !isRecord(mount)) {
|
|
314
|
+
out.push(mount);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const at = `files[${index}].resource`;
|
|
318
|
+
const replaced = await substitute(at, ref);
|
|
319
|
+
if ("error" in replaced) return {
|
|
320
|
+
ok: false,
|
|
321
|
+
at,
|
|
322
|
+
reason: replaced.error
|
|
323
|
+
};
|
|
324
|
+
out.push({
|
|
325
|
+
...mount,
|
|
326
|
+
resource: replaced
|
|
327
|
+
});
|
|
136
328
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
for (const stored of parseCommittedJsonLines(text, this.path)) {
|
|
147
|
-
if (stored.runId !== runId) continue;
|
|
148
|
-
if (ownerId !== void 0 && stored.ownerId !== ownerId) continue;
|
|
149
|
-
const record = "seq" in stored ? {
|
|
150
|
-
seq: stored.seq,
|
|
151
|
-
at: stored.at,
|
|
152
|
-
priority: stored.priority,
|
|
153
|
-
event: stored.event
|
|
154
|
-
} : {
|
|
155
|
-
seq: legacySeq++,
|
|
156
|
-
at: Date.parse(stored.at),
|
|
157
|
-
priority: 0,
|
|
158
|
-
event: stored.event
|
|
159
|
-
};
|
|
160
|
-
records.push(record);
|
|
161
|
-
const ev = record.event;
|
|
162
|
-
if (ev.type === "delivery-attempt" || ev.type === "steer" || ev.type === "answer") deliveryEvidence.push(ev);
|
|
163
|
-
if (ev.type === "question") byId.set(ev.question.id, ev.question);
|
|
164
|
-
else if (ev.type === "finding") findings.push(ev.finding);
|
|
165
|
-
else if (ev.type === "answer") {
|
|
166
|
-
const prior = byId.get(ev.questionId);
|
|
167
|
-
if (prior && ev.down.delivered) byId.set(ev.questionId, {
|
|
168
|
-
...prior,
|
|
169
|
-
status: "answered",
|
|
170
|
-
decision: {
|
|
171
|
-
kind: "answer",
|
|
172
|
-
answer: ev.down.instruction,
|
|
173
|
-
by: "prior-run"
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
} else if (ev.type === "instruction") continuations.push(ev.instruction);
|
|
177
|
-
else if (ev.type === "mail") mail.push(ev.mail);
|
|
178
|
-
else if (ev.type === "escalation") escalations.push(ev.escalation);
|
|
179
|
-
else if (ev.type === "analyst-defined") analystDefinitions.push(ev.analyst);
|
|
180
|
-
}
|
|
181
|
-
return {
|
|
182
|
-
...ownerId !== void 0 ? { ownerId } : {},
|
|
183
|
-
questions: [...byId.values()],
|
|
184
|
-
findings,
|
|
185
|
-
escalations,
|
|
186
|
-
analystDefinitions,
|
|
187
|
-
continuations,
|
|
188
|
-
deliveryEvidence,
|
|
189
|
-
mail,
|
|
190
|
-
records
|
|
329
|
+
next.files = out;
|
|
330
|
+
}
|
|
331
|
+
const instructions = pathInlineRef(resources.instructions);
|
|
332
|
+
if (instructions !== void 0) {
|
|
333
|
+
const replaced = await substitute("instructions", instructions);
|
|
334
|
+
if ("error" in replaced) return {
|
|
335
|
+
ok: false,
|
|
336
|
+
at: "instructions",
|
|
337
|
+
reason: replaced.error
|
|
191
338
|
};
|
|
339
|
+
next.instructions = replaced;
|
|
192
340
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
questions: [],
|
|
198
|
-
findings: [],
|
|
199
|
-
escalations: [],
|
|
200
|
-
analystDefinitions: [],
|
|
201
|
-
continuations: [],
|
|
202
|
-
deliveryEvidence: [],
|
|
203
|
-
mail: [],
|
|
204
|
-
records: []
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
//#endregion
|
|
208
|
-
//#region src/runtime/supervise/run-context.ts
|
|
209
|
-
/**
|
|
210
|
-
*
|
|
211
|
-
* `createInMemoryRunContext` — the one-call bundle of the in-memory stores a
|
|
212
|
-
* `createSupervisor().run(root, task, opts)` needs: a fresh `InMemorySpawnJournal`
|
|
213
|
-
* (the event-sourced spawn log), a fresh `InMemoryResultBlobStore` (the
|
|
214
|
-
* content-addressed `outRef` payload store the driver's `observe`/`finalize` reads
|
|
215
|
-
* settled outputs through), and a fresh `createExecutorRegistry()` (the open
|
|
216
|
-
* `AgentSpec → Executor` resolver).
|
|
217
|
-
*
|
|
218
|
-
* It exists to kill the boilerplate every offline/local supervised run repeats by
|
|
219
|
-
* hand — three constructors threaded into `SupervisorOpts` — and to single-source the
|
|
220
|
-
* ONE wiring invariant that is easy to get wrong: when the root is the recursive
|
|
221
|
-
* `driverAgent` LLM-driver brain AND it may spawn nested managed children, the registry MUST be
|
|
222
|
-
* wrapped with `withDriverExecutor` so a child constructed by `driverChild` resolves to the
|
|
223
|
-
* nested-scope executor — and that SAME blob
|
|
224
|
-
* store MUST be the one passed to `driverAgent({ blobs })`, or the driver
|
|
225
|
-
* reads from a different store than the scope writes to. Pass `{ withDriver: true }`
|
|
226
|
-
* and reuse the returned `blobs` for both.
|
|
227
|
-
*
|
|
228
|
-
* The spread shape matches `SupervisorOpts` exactly, so the call site reads:
|
|
229
|
-
* const run = createInMemoryRunContext()
|
|
230
|
-
* await createSupervisor().run(root, task, { budget, runId, ...run })
|
|
231
|
-
*
|
|
232
|
-
* @experimental
|
|
233
|
-
*/
|
|
234
|
-
/**
|
|
235
|
-
* Build a fresh in-memory run context. Every call returns NEW stores (no shared global
|
|
236
|
-
* state between runs), so two runs never cross-contaminate their journals/blobs.
|
|
237
|
-
*/
|
|
238
|
-
function createInMemoryRunContext(opts = {}) {
|
|
239
|
-
const base = createExecutorRegistry();
|
|
240
|
-
return {
|
|
241
|
-
journal: new InMemorySpawnJournal(),
|
|
242
|
-
blobs: new InMemoryResultBlobStore(),
|
|
243
|
-
executors: opts.withDriver ? withDriverExecutor(base) : base
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Build a DURABLE run context: the spawn journal and the result blobs are file-backed (fsynced
|
|
248
|
-
* per append/write) under `dir`, and the context carries `resume: true` so spreading it into
|
|
249
|
-
* `SupervisorOpts` makes the supervisor `loadTree`-first. A run that dies mid-flight therefore
|
|
250
|
-
* resumes when it is re-run with the SAME `runId` and the SAME `dir`: the committed children come
|
|
251
|
-
* back on `Scope.resume` (rehydrated by `replaySpawnTree`) instead of being re-executed.
|
|
252
|
-
*
|
|
253
|
-
* Layout: `${dir}/spawn-journal.jsonl` (one JSONL record per event), `${dir}/blobs/` (one
|
|
254
|
-
* content-addressed JSON file per settled result), and `${dir}/coordination-log.jsonl`
|
|
255
|
-
* (questions, findings, answer decisions, and authorized continuation receipts retained as
|
|
256
|
-
* evidence). The directory is created on first write.
|
|
257
|
-
*
|
|
258
|
-
* Opt-in by construction — `createInMemoryRunContext()` is unchanged and stays the default, so no
|
|
259
|
-
* existing consumer writes to disk or resumes unless it asks for this.
|
|
260
|
-
*/
|
|
261
|
-
function createFileRunContext(dir, opts = {}) {
|
|
262
|
-
const base = createExecutorRegistry();
|
|
263
|
-
return {
|
|
264
|
-
journal: new FileSpawnJournal(`${dir}/spawn-journal.jsonl`),
|
|
265
|
-
blobs: new FileResultBlobStore(`${dir}/blobs`),
|
|
266
|
-
executors: opts.withDriver ? withDriverExecutor(base) : base,
|
|
267
|
-
resume: true,
|
|
268
|
-
coordinationLog: new FileCoordinationLog(`${dir}/coordination-log.jsonl`)
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
//#endregion
|
|
272
|
-
//#region src/runtime/supervise/detector-monitor.ts
|
|
273
|
-
/**
|
|
274
|
-
*
|
|
275
|
-
* The ONLINE analyst: watch a `TraceSource` and fold each tool span through agent-eval's published
|
|
276
|
-
* streaming detector kernel (`repeatedActionDetector`/`errorStreakDetector` — the SAME kernel the
|
|
277
|
-
* control loop folds), firing `onSignal` the moment a worker loops or error-storms. Substrate-
|
|
278
|
-
* agnostic: it consumes spans from any source (owned router/bridge loop OR a sandbox box session),
|
|
279
|
-
* never the raw tool seam. Detection logic + the failure taxonomy live in agent-eval; not reimplemented.
|
|
280
|
-
*
|
|
281
|
-
* @experimental
|
|
282
|
-
*/
|
|
283
|
-
/** The default online panel for a tool-call pipe: a worker repeating the same call, or hammering
|
|
284
|
-
* consecutive errors. (No-progress needs a domain progress-probe, so it is opt-in, not default.)
|
|
285
|
-
*
|
|
286
|
-
* Coverage note: `repeated-action` works for EVERY harness (it needs only tool name + args, which
|
|
287
|
-
* every adapter provides). `error-streak` needs per-call status — opencode carries it inline
|
|
288
|
-
* (`state.status`, VALIDATED live), but claude-code/codex tool-call parts do NOT (their errors live
|
|
289
|
-
* in separate result blocks not yet decoded), so error-streak is silent for those until result-block
|
|
290
|
-
* decoding is added + live-validated. It is in the panel because it is correct where status exists. */
|
|
291
|
-
function defaultToolDetectors() {
|
|
292
|
-
return [repeatedActionDetector({ maxRepeated: 3 }), errorStreakDetector({ maxErrors: 3 })];
|
|
293
|
-
}
|
|
294
|
-
/** Subscribe to a `TraceSource` and run the streaming detectors over its live spans. Returns an
|
|
295
|
-
* unsubscribe. A defensive `argHash` failure (circular args) never throws out of the side-channel. */
|
|
296
|
-
function watchTrace(source, opts = {}) {
|
|
297
|
-
const detectors = opts.detectors ?? defaultToolDetectors();
|
|
298
|
-
return source.onSpan((span) => {
|
|
299
|
-
let fingerprint;
|
|
300
|
-
try {
|
|
301
|
-
fingerprint = `${span.toolName}|${argHash(span.args)}`;
|
|
302
|
-
} catch {
|
|
303
|
-
fingerprint = `${span.toolName}|<unhashable>`;
|
|
304
|
-
}
|
|
305
|
-
const signals = observeAll(detectors, {
|
|
306
|
-
actionFingerprint: fingerprint,
|
|
307
|
-
...span.status ? { status: span.status } : {},
|
|
308
|
-
label: span.toolName
|
|
309
|
-
});
|
|
310
|
-
for (const s of signals) opts.onSignal?.(s, span);
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
//#endregion
|
|
314
|
-
//#region src/runtime/supervise/event-bus.ts
|
|
315
|
-
/** Create the child→parent coordination bus: one typed pipe for settled outputs, questions, and analyst findings, with a priority-ordered pull queue and a pass-through subscribe lane.
|
|
316
|
-
* @experimental In-process queue; durability is a transport swap that does not exist yet. */
|
|
317
|
-
function createEventBus(now = Date.now) {
|
|
318
|
-
const queue = [];
|
|
319
|
-
const log = [];
|
|
320
|
-
const subscribers = [];
|
|
321
|
-
const byKind = {};
|
|
322
|
-
const staged = /* @__PURE__ */ new WeakMap();
|
|
323
|
-
let seq = 0;
|
|
324
|
-
let published = 0;
|
|
325
|
-
let pulled = 0;
|
|
326
|
-
const matches = (r, kinds) => !kinds || kinds.includes(r.event.type);
|
|
327
|
-
const bestIndex = (kinds) => {
|
|
328
|
-
let best = -1;
|
|
329
|
-
let bestPriority = Number.NEGATIVE_INFINITY;
|
|
330
|
-
for (let i = 0; i < queue.length; i++) {
|
|
331
|
-
const r = queue[i];
|
|
332
|
-
if (!r || !matches(r, kinds)) continue;
|
|
333
|
-
if (r.priority > bestPriority) {
|
|
334
|
-
best = i;
|
|
335
|
-
bestPriority = r.priority;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return best;
|
|
341
|
+
if (!touched) return {
|
|
342
|
+
ok: true,
|
|
343
|
+
profile,
|
|
344
|
+
resolved: []
|
|
339
345
|
};
|
|
340
346
|
return {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
priority: opts?.priority ?? 0,
|
|
346
|
-
event
|
|
347
|
-
};
|
|
348
|
-
staged.set(event, record);
|
|
349
|
-
for (const handler of subscribers) await handler(record);
|
|
350
|
-
staged.delete(event);
|
|
351
|
-
if (opts?.queue !== false) queue.push(record);
|
|
352
|
-
log.push(record);
|
|
353
|
-
published += 1;
|
|
354
|
-
byKind[event.type] = (byKind[event.type] ?? 0) + 1;
|
|
355
|
-
return record;
|
|
356
|
-
},
|
|
357
|
-
pull(kinds) {
|
|
358
|
-
const i = bestIndex(kinds);
|
|
359
|
-
if (i < 0) return void 0;
|
|
360
|
-
pulled++;
|
|
361
|
-
return queue.splice(i, 1)[0]?.event;
|
|
362
|
-
},
|
|
363
|
-
subscribe(handler) {
|
|
364
|
-
subscribers.push(handler);
|
|
365
|
-
return () => {
|
|
366
|
-
const i = subscribers.indexOf(handler);
|
|
367
|
-
if (i >= 0) subscribers.splice(i, 1);
|
|
368
|
-
};
|
|
347
|
+
ok: true,
|
|
348
|
+
profile: {
|
|
349
|
+
...profile,
|
|
350
|
+
resources: next
|
|
369
351
|
},
|
|
370
|
-
|
|
371
|
-
return kinds ? queue.filter((r) => matches(r, kinds)).length : queue.length;
|
|
372
|
-
},
|
|
373
|
-
history() {
|
|
374
|
-
return log;
|
|
375
|
-
},
|
|
376
|
-
stats() {
|
|
377
|
-
return {
|
|
378
|
-
published,
|
|
379
|
-
pulled,
|
|
380
|
-
byKind: { ...byKind }
|
|
381
|
-
};
|
|
382
|
-
}
|
|
352
|
+
resolved
|
|
383
353
|
};
|
|
384
354
|
}
|
|
385
355
|
//#endregion
|
|
@@ -786,7 +756,7 @@ const spawnProfileFields = [
|
|
|
786
756
|
},
|
|
787
757
|
{
|
|
788
758
|
name: "resources",
|
|
789
|
-
description: "Files, tools, skills, and agents materialized into the child workspace before it starts. Brief form: `{ files?, tools?, skills?, agents? }`, each an array whose entries are `{ kind: \"inline\", name, content }` or `{ kind: \"github\", repository?, path, ref? }` (`files` entries wrap that as `{ path, resource, executable? }`). A child typically gets `files` for seed inputs and `skills` for a procedure it must follow; the canonical AgentProfile schema carries the full form and governs validation.",
|
|
759
|
+
description: "Files, tools, skills, and agents materialized into the child workspace before it starts. Brief form: `{ files?, tools?, skills?, agents? }`, each an array whose entries are `{ kind: \"inline\", name, content }`, `{ kind: \"inline\", name, path }`, or `{ kind: \"github\", repository?, path, ref? }` (`files` entries wrap that as `{ path, resource, executable? }`). An inline `path` is relative to YOUR workspace and is read by the runtime, so use it for any file over a few hundred bytes: content you retype in this call arrives truncated and altered, a path arrives byte-exact and the result reports its sha256. A child typically gets `files` for seed inputs and `skills` for a procedure it must follow; the canonical AgentProfile schema carries the full form and governs validation.",
|
|
790
760
|
brief: {
|
|
791
761
|
type: "object",
|
|
792
762
|
properties: {
|
|
@@ -1929,7 +1899,16 @@ function createCoordinationTools(opts) {
|
|
|
1929
1899
|
live,
|
|
1930
1900
|
freeSlots: freeWorkerSlots()
|
|
1931
1901
|
}))(liveWorkerCount()));
|
|
1932
|
-
const
|
|
1902
|
+
const resourcePaths = await resolveSpawnResourcePaths(a.profile, opts.spawnResourceRoot);
|
|
1903
|
+
if (!resourcePaths.ok) return {
|
|
1904
|
+
error: "invalid-profile",
|
|
1905
|
+
reason: `resources.${resourcePaths.at}: ${resourcePaths.reason}`,
|
|
1906
|
+
issues: [{
|
|
1907
|
+
path: `resources.${resourcePaths.at}`,
|
|
1908
|
+
message: resourcePaths.reason
|
|
1909
|
+
}]
|
|
1910
|
+
};
|
|
1911
|
+
const parsedProfile = agentProfileSchema.safeParse(resourcePaths.profile);
|
|
1933
1912
|
if (!parsedProfile.success) return Promise.resolve({
|
|
1934
1913
|
error: "invalid-profile",
|
|
1935
1914
|
reason: `the profile you wrote is not a valid AgentProfile: ${parsedProfile.error.issues.map((issue) => `${issue.path.join(".") || "profile"} ${issue.message}`).join("; ")}`,
|
|
@@ -2023,7 +2002,8 @@ function createCoordinationTools(opts) {
|
|
|
2023
2002
|
...continuity.continuity === "resume" ? { resume: continuity.resume } : {},
|
|
2024
2003
|
live: liveWorkerCount(),
|
|
2025
2004
|
freeSlots: freeWorkerSlots(),
|
|
2026
|
-
...priorHistory
|
|
2005
|
+
...priorHistory,
|
|
2006
|
+
...resourcePaths.resolved.length === 0 ? {} : { resourcesFromPath: resourcePaths.resolved }
|
|
2027
2007
|
} : {
|
|
2028
2008
|
error: res.reason,
|
|
2029
2009
|
reason: res.reason === "usd-unbudgeted" ? "this run's root budget declares no maxUsd, so a child budget naming maxUsd can never be admitted at any amount — spawn with a budget that omits maxUsd" : res.reason === "in-doubt" ? "this key has a prior worker recorded as started without a terminal receipt; no replacement was started because that remote worker may still be running — inspect or recover the exact prior execution before retrying" : `the conserved pool refused this spawn (${String(res.reason)}); the run has no allocation left to give this worker`,
|
|
@@ -2642,6 +2622,269 @@ function safeJsonText(value) {
|
|
|
2642
2622
|
}
|
|
2643
2623
|
}
|
|
2644
2624
|
//#endregion
|
|
2625
|
+
//#region src/runtime/supervise/run-cancellation.ts
|
|
2626
|
+
/** Apply a durable request through the run's existing abort controller. */
|
|
2627
|
+
function applyRunCancellation(dir, abortRun, now, path = "turn-boundary") {
|
|
2628
|
+
const request = readRunCancelRequest(dir);
|
|
2629
|
+
if (request === void 0) return void 0;
|
|
2630
|
+
const prior = readRunCancellation(dir, request.operationId);
|
|
2631
|
+
if (prior !== void 0) return prior;
|
|
2632
|
+
abortRun(request.reason ?? "run cancel requested", request);
|
|
2633
|
+
const observedAt = now();
|
|
2634
|
+
const appliedAfterMs = Math.max(0, Date.parse(observedAt) - Date.parse(request.at));
|
|
2635
|
+
const record = {
|
|
2636
|
+
operationId: request.operationId,
|
|
2637
|
+
effect: "cancel_requested",
|
|
2638
|
+
path,
|
|
2639
|
+
appliedAfterMs,
|
|
2640
|
+
...request.deadlineMs === void 0 ? {} : { deadlineExceeded: appliedAfterMs > request.deadlineMs },
|
|
2641
|
+
requestedAt: request.at,
|
|
2642
|
+
observedAt,
|
|
2643
|
+
...request.reason === void 0 ? {} : { reason: request.reason },
|
|
2644
|
+
detail: "root abort issued to the whole run; termination not yet proven"
|
|
2645
|
+
};
|
|
2646
|
+
writeRunCancellation(dir, record);
|
|
2647
|
+
return record;
|
|
2648
|
+
}
|
|
2649
|
+
/** External drivers may remain silent indefinitely, so turn boundaries cannot observe control. */
|
|
2650
|
+
function watchRunCancellation(dir, abortRun) {
|
|
2651
|
+
mkdirSync(workerCancellationsDir(dir), { recursive: true });
|
|
2652
|
+
let active = true;
|
|
2653
|
+
let failure;
|
|
2654
|
+
const check = (path = "observer") => {
|
|
2655
|
+
if (failure !== void 0) throw failure;
|
|
2656
|
+
if (active) applyRunCancellation(dir, abortRun, () => (/* @__PURE__ */ new Date()).toISOString(), path);
|
|
2657
|
+
};
|
|
2658
|
+
const fail = (error) => {
|
|
2659
|
+
if (!active) return;
|
|
2660
|
+
failure = error;
|
|
2661
|
+
abortRun(`durable run cancellation observer failed: ${String(error)}`);
|
|
2662
|
+
};
|
|
2663
|
+
const watcher = watch(workerCancellationsDir(dir), (_event, filename) => {
|
|
2664
|
+
if (!active || filename !== null && filename !== "run.request.json") return;
|
|
2665
|
+
try {
|
|
2666
|
+
check();
|
|
2667
|
+
} catch (error) {
|
|
2668
|
+
fail(error);
|
|
2669
|
+
}
|
|
2670
|
+
});
|
|
2671
|
+
watcher.on("error", fail);
|
|
2672
|
+
const fallback = setInterval(() => {
|
|
2673
|
+
try {
|
|
2674
|
+
check("fallback");
|
|
2675
|
+
} catch (error) {
|
|
2676
|
+
fail(error);
|
|
2677
|
+
}
|
|
2678
|
+
}, 100);
|
|
2679
|
+
fallback.unref();
|
|
2680
|
+
return {
|
|
2681
|
+
check,
|
|
2682
|
+
close() {
|
|
2683
|
+
active = false;
|
|
2684
|
+
watcher.close();
|
|
2685
|
+
clearInterval(fallback);
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
//#endregion
|
|
2690
|
+
//#region src/runtime/supervise/coordination-log.ts
|
|
2691
|
+
/**
|
|
2692
|
+
* Durable side-log for coordination evidence the spawn journal does not own: questions, analyst
|
|
2693
|
+
* findings, answer decisions, authorized continuation receipts, delivery-attempt markers, and
|
|
2694
|
+
* delivery outcomes. A durable run
|
|
2695
|
+
* (`supervise({ runDir })`) appends them as they publish and loads them on resume, so a restarted
|
|
2696
|
+
* coordinator retains the exact evidence produced by prior processes.
|
|
2697
|
+
*
|
|
2698
|
+
* Answer down-events also fold status on load: a question answered before the crash reloads as
|
|
2699
|
+
* `answered`, not as a re-blocking `open`. Settled events are skipped (the spawn journal is their
|
|
2700
|
+
* ledger). A receipt followed by an attempt but no outcome proves the process died in the delivery
|
|
2701
|
+
* window; that outcome remains unknown and no prior instruction is auto-delivered.
|
|
2702
|
+
*
|
|
2703
|
+
* JSONL, one fsynced record per event, keyed by `runId` — several runs may share one log file
|
|
2704
|
+
* exactly as they share one spawn-journal file.
|
|
2705
|
+
*
|
|
2706
|
+
* @experimental
|
|
2707
|
+
*/
|
|
2708
|
+
/** Persist prior context plus exact continuation authorization, attempt, and result evidence.
|
|
2709
|
+
* Settlements have their own journal. */
|
|
2710
|
+
function persisted(event) {
|
|
2711
|
+
return event.type !== "settled";
|
|
2712
|
+
}
|
|
2713
|
+
/** FS-backed `CoordinationLog`: append-only JSONL, fsynced per record. */
|
|
2714
|
+
var FileCoordinationLog = class {
|
|
2715
|
+
path;
|
|
2716
|
+
appendTail = Promise.resolve();
|
|
2717
|
+
constructor(path) {
|
|
2718
|
+
this.path = path;
|
|
2719
|
+
}
|
|
2720
|
+
async append(runId, record, ownerId) {
|
|
2721
|
+
if (!persisted(record.event)) return;
|
|
2722
|
+
const append = this.appendTail.then(() => this.appendRecord(runId, record, ownerId));
|
|
2723
|
+
this.appendTail = append.catch(() => void 0);
|
|
2724
|
+
return append;
|
|
2725
|
+
}
|
|
2726
|
+
async appendRecord(runId, busRecord, ownerId) {
|
|
2727
|
+
const fs = await import("node:fs/promises");
|
|
2728
|
+
const path = await import("node:path");
|
|
2729
|
+
await fs.mkdir(path.dirname(this.path), { recursive: true });
|
|
2730
|
+
const record = {
|
|
2731
|
+
runId,
|
|
2732
|
+
...ownerId !== void 0 ? { ownerId } : {},
|
|
2733
|
+
...busRecord
|
|
2734
|
+
};
|
|
2735
|
+
const needsSeparator = await prepareJsonlAppend(this.path);
|
|
2736
|
+
const fh = await fs.open(this.path, "a");
|
|
2737
|
+
try {
|
|
2738
|
+
await writeAllBytes(fh, `${needsSeparator ? "\n" : ""}${JSON.stringify(record)}\n`);
|
|
2739
|
+
await fh.sync();
|
|
2740
|
+
} finally {
|
|
2741
|
+
await fh.close();
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
async load(runId, ownerId) {
|
|
2745
|
+
const fs = await import("node:fs/promises");
|
|
2746
|
+
let text;
|
|
2747
|
+
try {
|
|
2748
|
+
text = await fs.readFile(this.path, "utf8");
|
|
2749
|
+
} catch (err) {
|
|
2750
|
+
if (isNoEntError(err)) return emptyPriorCoordination(ownerId);
|
|
2751
|
+
throw err;
|
|
2752
|
+
}
|
|
2753
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2754
|
+
const findings = [];
|
|
2755
|
+
const escalations = [];
|
|
2756
|
+
const analystDefinitions = [];
|
|
2757
|
+
const continuations = [];
|
|
2758
|
+
const deliveryEvidence = [];
|
|
2759
|
+
const mail = [];
|
|
2760
|
+
const records = [];
|
|
2761
|
+
let legacySeq = 0;
|
|
2762
|
+
for (const stored of parseCommittedJsonLines(text, this.path)) {
|
|
2763
|
+
if (stored.runId !== runId) continue;
|
|
2764
|
+
if (ownerId !== void 0 && stored.ownerId !== ownerId) continue;
|
|
2765
|
+
const record = "seq" in stored ? {
|
|
2766
|
+
seq: stored.seq,
|
|
2767
|
+
at: stored.at,
|
|
2768
|
+
priority: stored.priority,
|
|
2769
|
+
event: stored.event
|
|
2770
|
+
} : {
|
|
2771
|
+
seq: legacySeq++,
|
|
2772
|
+
at: Date.parse(stored.at),
|
|
2773
|
+
priority: 0,
|
|
2774
|
+
event: stored.event
|
|
2775
|
+
};
|
|
2776
|
+
records.push(record);
|
|
2777
|
+
const ev = record.event;
|
|
2778
|
+
if (ev.type === "delivery-attempt" || ev.type === "steer" || ev.type === "answer") deliveryEvidence.push(ev);
|
|
2779
|
+
if (ev.type === "question") byId.set(ev.question.id, ev.question);
|
|
2780
|
+
else if (ev.type === "finding") findings.push(ev.finding);
|
|
2781
|
+
else if (ev.type === "answer") {
|
|
2782
|
+
const prior = byId.get(ev.questionId);
|
|
2783
|
+
if (prior && ev.down.delivered) byId.set(ev.questionId, {
|
|
2784
|
+
...prior,
|
|
2785
|
+
status: "answered",
|
|
2786
|
+
decision: {
|
|
2787
|
+
kind: "answer",
|
|
2788
|
+
answer: ev.down.instruction,
|
|
2789
|
+
by: "prior-run"
|
|
2790
|
+
}
|
|
2791
|
+
});
|
|
2792
|
+
} else if (ev.type === "instruction") continuations.push(ev.instruction);
|
|
2793
|
+
else if (ev.type === "mail") mail.push(ev.mail);
|
|
2794
|
+
else if (ev.type === "escalation") escalations.push(ev.escalation);
|
|
2795
|
+
else if (ev.type === "analyst-defined") analystDefinitions.push(ev.analyst);
|
|
2796
|
+
}
|
|
2797
|
+
return {
|
|
2798
|
+
...ownerId !== void 0 ? { ownerId } : {},
|
|
2799
|
+
questions: [...byId.values()],
|
|
2800
|
+
findings,
|
|
2801
|
+
escalations,
|
|
2802
|
+
analystDefinitions,
|
|
2803
|
+
continuations,
|
|
2804
|
+
deliveryEvidence,
|
|
2805
|
+
mail,
|
|
2806
|
+
records
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
};
|
|
2810
|
+
function emptyPriorCoordination(ownerId) {
|
|
2811
|
+
return {
|
|
2812
|
+
...ownerId !== void 0 ? { ownerId } : {},
|
|
2813
|
+
questions: [],
|
|
2814
|
+
findings: [],
|
|
2815
|
+
escalations: [],
|
|
2816
|
+
analystDefinitions: [],
|
|
2817
|
+
continuations: [],
|
|
2818
|
+
deliveryEvidence: [],
|
|
2819
|
+
mail: [],
|
|
2820
|
+
records: []
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
//#endregion
|
|
2824
|
+
//#region src/runtime/supervise/run-context.ts
|
|
2825
|
+
/**
|
|
2826
|
+
*
|
|
2827
|
+
* `createInMemoryRunContext` — the one-call bundle of the in-memory stores a
|
|
2828
|
+
* `createSupervisor().run(root, task, opts)` needs: a fresh `InMemorySpawnJournal`
|
|
2829
|
+
* (the event-sourced spawn log), a fresh `InMemoryResultBlobStore` (the
|
|
2830
|
+
* content-addressed `outRef` payload store the driver's `observe`/`finalize` reads
|
|
2831
|
+
* settled outputs through), and a fresh `createExecutorRegistry()` (the open
|
|
2832
|
+
* `AgentSpec → Executor` resolver).
|
|
2833
|
+
*
|
|
2834
|
+
* It exists to kill the boilerplate every offline/local supervised run repeats by
|
|
2835
|
+
* hand — three constructors threaded into `SupervisorOpts` — and to single-source the
|
|
2836
|
+
* ONE wiring invariant that is easy to get wrong: when the root is the recursive
|
|
2837
|
+
* `driverAgent` LLM-driver brain AND it may spawn nested managed children, the registry MUST be
|
|
2838
|
+
* wrapped with `withDriverExecutor` so a child constructed by `driverChild` resolves to the
|
|
2839
|
+
* nested-scope executor — and that SAME blob
|
|
2840
|
+
* store MUST be the one passed to `driverAgent({ blobs })`, or the driver
|
|
2841
|
+
* reads from a different store than the scope writes to. Pass `{ withDriver: true }`
|
|
2842
|
+
* and reuse the returned `blobs` for both.
|
|
2843
|
+
*
|
|
2844
|
+
* The spread shape matches `SupervisorOpts` exactly, so the call site reads:
|
|
2845
|
+
* const run = createInMemoryRunContext()
|
|
2846
|
+
* await createSupervisor().run(root, task, { budget, runId, ...run })
|
|
2847
|
+
*
|
|
2848
|
+
* @experimental
|
|
2849
|
+
*/
|
|
2850
|
+
/**
|
|
2851
|
+
* Build a fresh in-memory run context. Every call returns NEW stores (no shared global
|
|
2852
|
+
* state between runs), so two runs never cross-contaminate their journals/blobs.
|
|
2853
|
+
*/
|
|
2854
|
+
function createInMemoryRunContext(opts = {}) {
|
|
2855
|
+
const base = createExecutorRegistry();
|
|
2856
|
+
return {
|
|
2857
|
+
journal: new InMemorySpawnJournal(),
|
|
2858
|
+
blobs: new InMemoryResultBlobStore(),
|
|
2859
|
+
executors: opts.withDriver ? withDriverExecutor(base) : base
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* Build a DURABLE run context: the spawn journal and the result blobs are file-backed (fsynced
|
|
2864
|
+
* per append/write) under `dir`, and the context carries `resume: true` so spreading it into
|
|
2865
|
+
* `SupervisorOpts` makes the supervisor `loadTree`-first. A run that dies mid-flight therefore
|
|
2866
|
+
* resumes when it is re-run with the SAME `runId` and the SAME `dir`: the committed children come
|
|
2867
|
+
* back on `Scope.resume` (rehydrated by `replaySpawnTree`) instead of being re-executed.
|
|
2868
|
+
*
|
|
2869
|
+
* Layout: `${dir}/spawn-journal.jsonl` (one JSONL record per event), `${dir}/blobs/` (one
|
|
2870
|
+
* content-addressed JSON file per settled result), and `${dir}/coordination-log.jsonl`
|
|
2871
|
+
* (questions, findings, answer decisions, and authorized continuation receipts retained as
|
|
2872
|
+
* evidence). The directory is created on first write.
|
|
2873
|
+
*
|
|
2874
|
+
* Opt-in by construction — `createInMemoryRunContext()` is unchanged and stays the default, so no
|
|
2875
|
+
* existing consumer writes to disk or resumes unless it asks for this.
|
|
2876
|
+
*/
|
|
2877
|
+
function createFileRunContext(dir, opts = {}) {
|
|
2878
|
+
const base = createExecutorRegistry();
|
|
2879
|
+
return {
|
|
2880
|
+
journal: new FileSpawnJournal(`${dir}/spawn-journal.jsonl`),
|
|
2881
|
+
blobs: new FileResultBlobStore(`${dir}/blobs`),
|
|
2882
|
+
executors: opts.withDriver ? withDriverExecutor(base) : base,
|
|
2883
|
+
resume: true,
|
|
2884
|
+
coordinationLog: new FileCoordinationLog(`${dir}/coordination-log.jsonl`)
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
//#endregion
|
|
2645
2888
|
//#region src/runtime/anytime.ts
|
|
2646
2889
|
/**
|
|
2647
2890
|
* The best-so-far fold — the ONE definition of "how good was the run after k results", shared by
|
|
@@ -3480,6 +3723,7 @@ function driverAgent(opts) {
|
|
|
3480
3723
|
...opts.continuityByProfile ? { continuityByProfile: opts.continuityByProfile } : {},
|
|
3481
3724
|
...opts.preflightSpawn ? { preflightSpawn: opts.preflightSpawn } : {},
|
|
3482
3725
|
...opts.resolveSpawnProfile ? { resolveSpawnProfile: opts.resolveSpawnProfile } : {},
|
|
3726
|
+
...opts.spawnResourceRoot ? { spawnResourceRoot: opts.spawnResourceRoot } : {},
|
|
3483
3727
|
...opts.escalateQuestion ? { escalateQuestion: opts.escalateQuestion } : {},
|
|
3484
3728
|
...opts.onEvent ? { onEvent: opts.onEvent } : {},
|
|
3485
3729
|
...opts.replaySettlements ? { replaySettlements: true } : {},
|
|
@@ -3811,6 +4055,6 @@ function errMessage(e) {
|
|
|
3811
4055
|
return e instanceof Error ? e.message : String(e);
|
|
3812
4056
|
}
|
|
3813
4057
|
//#endregion
|
|
3814
|
-
export {
|
|
4058
|
+
export { normalizeAnalyzeOnSettle as A, ANALYST_DEFINITION_BOUNDS as C, coordinationVerbNames as D, canonicalFindingEvent as E, watchTrace as F, questionEscalationTargets as M, createEventBus as N, createCoordinationTools as O, defaultToolDetectors as P, watchRunCancellation as S, analystToolGroupNames as T, renderAnytimeTable as _, allOf as a, FileCoordinationLog as b, createProgressTracker as c, progressStop as d, sampleFromSettled as f, plateauLength as g, bestSoFar as h, finalizeBestDelivered as i, parseAuthoredAnalystDefinition as j, downMessageRefusalReasons as k, noProgressFor as l, areaUnderCurve as m, createSteerAcknowledger as n, allWorkersStalled as o, anytimeReport as p, driverAgent as r, anyOf as s, createCancelAcknowledger as t, plateau as u, createFileRunContext as v, DEFAULT_AWAIT_EVENT_TIMEOUT_MS as w, applyRunCancellation as x, createInMemoryRunContext as y };
|
|
3815
4059
|
|
|
3816
|
-
//# sourceMappingURL=coordination-driver-
|
|
4060
|
+
//# sourceMappingURL=coordination-driver-BxGh-Tnj.js.map
|