@holoscript/holoscript-agent 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -0
- package/bin/holoscript-agent.cjs +18 -0
- package/dist/ablation.js +4 -1
- package/dist/ablation.js.map +1 -1
- package/dist/brain.js +41 -5
- package/dist/brain.js.map +1 -1
- package/dist/commit-hook.js +6 -2
- package/dist/commit-hook.js.map +1 -1
- package/dist/cost-guard.d.ts +17 -2
- package/dist/cost-guard.js +31 -3
- package/dist/cost-guard.js.map +1 -1
- package/dist/holomesh-client.d.ts +57 -1
- package/dist/holomesh-client.js +52 -8
- package/dist/holomesh-client.js.map +1 -1
- package/dist/identity.js +5 -1
- package/dist/identity.js.map +1 -1
- package/dist/index.js +897 -127
- package/dist/index.js.map +1 -1
- package/dist/provision.js +39 -22
- package/dist/provision.js.map +1 -1
- package/dist/runner.d.ts +57 -0
- package/dist/runner.js +351 -31
- package/dist/runner.js.map +1 -1
- package/dist/supervisor-config.js +14 -5
- package/dist/supervisor-config.js.map +1 -1
- package/dist/supervisor.js +656 -57
- package/dist/supervisor.js.map +1 -1
- package/dist/types.d.ts +43 -1
- package/package.json +10 -5
package/dist/supervisor.js
CHANGED
|
@@ -9,9 +9,14 @@ var HolomeshClient = class {
|
|
|
9
9
|
this.bearer = opts.bearer;
|
|
10
10
|
this.teamId = opts.teamId;
|
|
11
11
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
12
|
+
this.signer = opts.signer;
|
|
13
|
+
}
|
|
14
|
+
/** Wrap body in a signed envelope when a signer is available (strict-mode endpoints). */
|
|
15
|
+
async signBody(body) {
|
|
16
|
+
return this.signer ? await this.signer(body) : body;
|
|
12
17
|
}
|
|
13
18
|
async heartbeat(payload) {
|
|
14
|
-
await this.req("POST", `/team/${this.teamId}/presence`, payload);
|
|
19
|
+
await this.req("POST", `/team/${this.teamId}/presence`, await this.signBody(payload));
|
|
15
20
|
}
|
|
16
21
|
async getOpenTasks() {
|
|
17
22
|
const data = await this.req(
|
|
@@ -21,28 +26,33 @@ var HolomeshClient = class {
|
|
|
21
26
|
return data.tasks ?? data.open ?? [];
|
|
22
27
|
}
|
|
23
28
|
async claim(taskId) {
|
|
24
|
-
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, { action: "claim" });
|
|
29
|
+
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({ action: "claim" }));
|
|
25
30
|
}
|
|
26
31
|
async joinTeam() {
|
|
27
32
|
return this.req(
|
|
28
33
|
"POST",
|
|
29
34
|
`/team/${this.teamId}/join`,
|
|
30
|
-
{}
|
|
35
|
+
await this.signBody({})
|
|
31
36
|
);
|
|
32
37
|
}
|
|
33
38
|
async sendMessageOnTask(taskId, body) {
|
|
34
|
-
await this.req("POST", `/team/${this.teamId}/message`, {
|
|
39
|
+
await this.req("POST", `/team/${this.teamId}/message`, await this.signBody({
|
|
35
40
|
to: "team",
|
|
36
41
|
subject: `task:${taskId}`,
|
|
37
42
|
content: body
|
|
38
|
-
});
|
|
43
|
+
}));
|
|
39
44
|
}
|
|
40
45
|
async markDone(taskId, summary, commitHash) {
|
|
41
|
-
await this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, {
|
|
46
|
+
await this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({
|
|
42
47
|
action: "done",
|
|
43
48
|
summary,
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
// verification_evidence required by server before task can be closed.
|
|
50
|
+
verification_evidence: summary,
|
|
51
|
+
// Exclude commitHash when undefined — JSON.stringify drops undefined but
|
|
52
|
+
// canonicalizeSigning preserves it as the literal string "undefined",
|
|
53
|
+
// causing a signature-mismatch vs what the server sees after JSON.parse.
|
|
54
|
+
...commitHash !== void 0 ? { commitHash } : {}
|
|
55
|
+
}));
|
|
46
56
|
}
|
|
47
57
|
// POST CAEL audit records for this agent. Server validator at
|
|
48
58
|
// packages/mcp-server/src/holomesh/routes/core-routes.ts:472-533 requires
|
|
@@ -65,6 +75,40 @@ var HolomeshClient = class {
|
|
|
65
75
|
wallet: raw.wallet
|
|
66
76
|
};
|
|
67
77
|
}
|
|
78
|
+
// ── Team Message Surface (E4 delegated-authority protocol) ───────────────────
|
|
79
|
+
/** Read recent team messages. */
|
|
80
|
+
async getTeamMessages(limit = 20) {
|
|
81
|
+
const data = await this.req(
|
|
82
|
+
"GET",
|
|
83
|
+
`/team/${this.teamId}/messages?limit=${limit}`
|
|
84
|
+
);
|
|
85
|
+
return data.messages ?? [];
|
|
86
|
+
}
|
|
87
|
+
/** Post a message to the team feed. */
|
|
88
|
+
async sendTeamMessage(content, messageType = "text") {
|
|
89
|
+
await this.req("POST", `/team/${this.teamId}/message`, await this.signBody({ content, type: messageType }));
|
|
90
|
+
}
|
|
91
|
+
// ── Owner-op API wrappers (E4) ─────────────────────────────────────────────
|
|
92
|
+
/** Switch team mode. Requires owner or founder role. */
|
|
93
|
+
async setTeamMode(mode, reason) {
|
|
94
|
+
return this.req("POST", `/team/${this.teamId}/mode`, await this.signBody({ mode, reason }));
|
|
95
|
+
}
|
|
96
|
+
/** Update room preferences. Requires config:write permission. */
|
|
97
|
+
async patchRoomPrefs(prefs) {
|
|
98
|
+
return this.req("PATCH", `/team/${this.teamId}/room`, await this.signBody(prefs));
|
|
99
|
+
}
|
|
100
|
+
/** Update a board task. */
|
|
101
|
+
async updateTask(taskId, updates) {
|
|
102
|
+
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({ action: "update", ...updates }));
|
|
103
|
+
}
|
|
104
|
+
/** Delete a board task. */
|
|
105
|
+
async deleteTask(taskId) {
|
|
106
|
+
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({ action: "delete" }));
|
|
107
|
+
}
|
|
108
|
+
/** Delegate a board task to another agent. */
|
|
109
|
+
async delegateTask(taskId, toAgentId) {
|
|
110
|
+
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({ action: "delegate", toAgentId }));
|
|
111
|
+
}
|
|
68
112
|
async req(method, path, body) {
|
|
69
113
|
const url = `${this.apiBase}${path}`;
|
|
70
114
|
const res = await this.fetchImpl(url, {
|
|
@@ -132,7 +176,18 @@ function brainClassOf(brain) {
|
|
|
132
176
|
return "unknown";
|
|
133
177
|
}
|
|
134
178
|
function buildCaelRecord(input) {
|
|
135
|
-
const {
|
|
179
|
+
const {
|
|
180
|
+
identity,
|
|
181
|
+
brain,
|
|
182
|
+
task,
|
|
183
|
+
messages,
|
|
184
|
+
finalText,
|
|
185
|
+
usage,
|
|
186
|
+
costUsd,
|
|
187
|
+
spentUsd,
|
|
188
|
+
prevChain,
|
|
189
|
+
runtimeVersion
|
|
190
|
+
} = input;
|
|
136
191
|
const l0 = sha(brain.systemPrompt);
|
|
137
192
|
const l1 = sha(`${task.id}|${task.title}|${task.description ?? ""}`);
|
|
138
193
|
const l2 = sha(JSON.stringify(messages));
|
|
@@ -148,15 +203,16 @@ function buildCaelRecord(input) {
|
|
|
148
203
|
prev_hash: prevChain,
|
|
149
204
|
fnv1a_chain,
|
|
150
205
|
version_vector_fingerprint: `agent@${runtimeVersion}|brain@${brainClassOf(brain)}|provider@${identity.llmProvider}|model@${identity.llmModel}`,
|
|
151
|
-
brain_class: brainClassOf(brain)
|
|
206
|
+
brain_class: brainClassOf(brain),
|
|
207
|
+
trust_epoch: "post-w107"
|
|
152
208
|
};
|
|
153
209
|
}
|
|
154
210
|
|
|
155
211
|
// src/tools.ts
|
|
156
212
|
import { readFile, writeFile, readdir, mkdir, stat } from "fs/promises";
|
|
157
|
-
import { resolve, dirname } from "path";
|
|
213
|
+
import { resolve, dirname, delimiter, isAbsolute, sep } from "path";
|
|
158
214
|
import { spawn } from "child_process";
|
|
159
|
-
var
|
|
215
|
+
var FLEET_READ_ROOTS = [
|
|
160
216
|
"/root/msc-paper-22",
|
|
161
217
|
// Paper 22 mechanization inputs (scp'd by deploy)
|
|
162
218
|
"/root/holoscript-mesh",
|
|
@@ -164,15 +220,24 @@ var ALLOWED_READ_ROOTS = [
|
|
|
164
220
|
"/root/agent-output"
|
|
165
221
|
// Read back what we wrote
|
|
166
222
|
];
|
|
167
|
-
var
|
|
223
|
+
var FLEET_WRITE_ROOTS = [
|
|
168
224
|
"/root/agent-output"
|
|
169
225
|
// Single write sink — keeps deliverables in one place
|
|
170
226
|
];
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
227
|
+
function parseRootsEnv(raw, fallback) {
|
|
228
|
+
if (!raw) return fallback;
|
|
229
|
+
const roots = raw.split(delimiter).map((r) => r.trim()).filter((r) => r.length > 0 && isAbsolute(r));
|
|
230
|
+
return roots.length > 0 ? roots : fallback;
|
|
231
|
+
}
|
|
232
|
+
var ALLOWED_READ_ROOTS = parseRootsEnv(
|
|
233
|
+
process.env.HOLOSCRIPT_AGENT_READ_ROOTS,
|
|
234
|
+
FLEET_READ_ROOTS
|
|
235
|
+
);
|
|
236
|
+
var ALLOWED_WRITE_ROOTS = parseRootsEnv(
|
|
237
|
+
process.env.HOLOSCRIPT_AGENT_WRITE_ROOTS,
|
|
238
|
+
FLEET_WRITE_ROOTS
|
|
239
|
+
);
|
|
240
|
+
var BASH_READ_ONLY_PREFIXES = [
|
|
176
241
|
"ls ",
|
|
177
242
|
"ls\n",
|
|
178
243
|
"ls$",
|
|
@@ -187,16 +252,36 @@ var BASH_WHITELIST = [
|
|
|
187
252
|
"git log",
|
|
188
253
|
"git diff",
|
|
189
254
|
"git show",
|
|
255
|
+
"pwd",
|
|
256
|
+
"echo ",
|
|
257
|
+
"lake env"
|
|
258
|
+
];
|
|
259
|
+
var BASH_PRODUCTIVE_PREFIXES = [
|
|
260
|
+
"lake build",
|
|
261
|
+
"lake clean",
|
|
262
|
+
"lean ",
|
|
190
263
|
"pnpm --filter",
|
|
191
264
|
"pnpm vitest",
|
|
192
265
|
"vitest run",
|
|
193
|
-
|
|
194
|
-
|
|
266
|
+
// Robotics / edge-node (Jetson) productive commands — without these, every
|
|
267
|
+
// ros2/colcon/tegrastats task fails the W.107 artifact gate and is abandoned
|
|
268
|
+
// as no-artifact. (jetson-orin-01 lane.)
|
|
269
|
+
"ros2 launch",
|
|
270
|
+
"ros2 topic pub",
|
|
271
|
+
"ros2 service call",
|
|
272
|
+
"colcon build",
|
|
273
|
+
"tegrastats"
|
|
195
274
|
];
|
|
275
|
+
var BASH_WHITELIST = [...BASH_READ_ONLY_PREFIXES, ...BASH_PRODUCTIVE_PREFIXES];
|
|
276
|
+
function isProductiveBashCommand(cmd) {
|
|
277
|
+
const trimmed = String(cmd ?? "").trim();
|
|
278
|
+
if (!trimmed) return false;
|
|
279
|
+
return BASH_PRODUCTIVE_PREFIXES.some((prefix) => trimmed.startsWith(prefix.trim()));
|
|
280
|
+
}
|
|
196
281
|
var MESH_TOOLS = [
|
|
197
282
|
{
|
|
198
283
|
name: "read_file",
|
|
199
|
-
description:
|
|
284
|
+
description: `Read a file from the agent sandbox. Allowed roots: ${ALLOWED_READ_ROOTS.join(", ")}. Returns the file content as text. Use this to inspect task inputs and the read-only repo view.`,
|
|
200
285
|
input_schema: {
|
|
201
286
|
type: "object",
|
|
202
287
|
properties: {
|
|
@@ -218,11 +303,11 @@ var MESH_TOOLS = [
|
|
|
218
303
|
},
|
|
219
304
|
{
|
|
220
305
|
name: "write_file",
|
|
221
|
-
description:
|
|
306
|
+
description: `Write a file to the deliverable sink (write roots: ${ALLOWED_WRITE_ROOTS.join(", ")}). Anything you want to emit as task output (a Lean proof, a markdown report, a JSON dataset, a .holo scene) goes here. Creates parent directories. Will refuse paths outside the write root(s).`,
|
|
222
307
|
input_schema: {
|
|
223
308
|
type: "object",
|
|
224
309
|
properties: {
|
|
225
|
-
path: { type: "string", description:
|
|
310
|
+
path: { type: "string", description: `Absolute path under a write root: ${ALLOWED_WRITE_ROOTS.join(", ")}` },
|
|
226
311
|
content: { type: "string", description: "File content to write (UTF-8)" }
|
|
227
312
|
},
|
|
228
313
|
required: ["path", "content"]
|
|
@@ -230,7 +315,7 @@ var MESH_TOOLS = [
|
|
|
230
315
|
},
|
|
231
316
|
{
|
|
232
317
|
name: "bash",
|
|
233
|
-
description: "Run a shell command. Whitelisted prefixes only: lake build, lean, ls, cat, grep, find, wc, head, tail, git status/log/diff/show, pnpm --filter, vitest run, pwd, echo. Hard 60s wall timeout, 1MB stdout cap. Use for
|
|
318
|
+
description: "Run a shell command. Whitelisted prefixes only: lake build, lean, ls, cat, grep, find, wc, head, tail, git status/log/diff/show, pnpm --filter, vitest run, pwd, echo, ros2 launch/topic/service, colcon build, tegrastats. Hard 60s wall timeout, 1MB stdout cap. Use for builds, tests, hardware probes. Refuses rm, curl, ssh, sudo, eval.",
|
|
234
319
|
input_schema: {
|
|
235
320
|
type: "object",
|
|
236
321
|
properties: {
|
|
@@ -239,22 +324,52 @@ var MESH_TOOLS = [
|
|
|
239
324
|
},
|
|
240
325
|
required: ["cmd"]
|
|
241
326
|
}
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "emit_hardware_receipt",
|
|
330
|
+
description: "Emit a portable hardware receipt (PortableHardwareReceiptMetadata v1) capturing device identity, runtime, and measured performance. Writes a JSON receipt to the agent output dir. Use after running tegrastats or colcon build to record hardware evidence for the CAEL audit chain. Accepts either pre-parsed measurements or raw tegrastats output (the tool parses it automatically).",
|
|
331
|
+
input_schema: {
|
|
332
|
+
type: "object",
|
|
333
|
+
properties: {
|
|
334
|
+
device_kind: {
|
|
335
|
+
type: "string",
|
|
336
|
+
description: 'Device identifier, e.g. "jetson-orin-nano-super", "raspberry-pi-5"'
|
|
337
|
+
},
|
|
338
|
+
accelerator: {
|
|
339
|
+
description: 'Accelerator string, e.g. "NVIDIA CUDA 8.7", or null for CPU-only'
|
|
340
|
+
},
|
|
341
|
+
runtime_name: { type: "string", description: 'Inference runtime, e.g. "Ollama", "llama.cpp"' },
|
|
342
|
+
runtime_version: { type: "string", description: 'Runtime version, e.g. "0.30.8"' },
|
|
343
|
+
host_os: { type: "string", description: 'OS + firmware, e.g. "JetPack 6.2.1 / Ubuntu 22.04"' },
|
|
344
|
+
composition_id: { type: "string", description: 'Brain composition reference, e.g. "jetson-orin-brain"' },
|
|
345
|
+
measurements: {
|
|
346
|
+
type: "array",
|
|
347
|
+
description: "Pre-parsed measurements. Each item: {metric: string, value: number, unit: string}",
|
|
348
|
+
items: { type: "object" }
|
|
349
|
+
},
|
|
350
|
+
tegrastats_output: {
|
|
351
|
+
type: "string",
|
|
352
|
+
description: "Raw tegrastats output line(s) \u2014 tool auto-parses GPU%, RAM, temp, power"
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
required: ["device_kind", "runtime_name", "runtime_version", "host_os"]
|
|
356
|
+
}
|
|
242
357
|
}
|
|
243
358
|
];
|
|
244
359
|
function isUnderRoot(absPath, root) {
|
|
245
360
|
const resolved = resolve(absPath);
|
|
246
361
|
const rootResolved = resolve(root);
|
|
247
|
-
return resolved === rootResolved || resolved.startsWith(rootResolved +
|
|
362
|
+
return resolved === rootResolved || resolved.startsWith(rootResolved + sep);
|
|
248
363
|
}
|
|
249
364
|
function checkReadAllowed(path) {
|
|
250
|
-
if (!path
|
|
365
|
+
if (!isAbsolute(path)) return `path must be absolute, got "${path}"`;
|
|
251
366
|
for (const root of ALLOWED_READ_ROOTS) {
|
|
252
367
|
if (isUnderRoot(path, root)) return null;
|
|
253
368
|
}
|
|
254
369
|
return `read denied \u2014 path "${path}" not under allowed roots: ${ALLOWED_READ_ROOTS.join(", ")}`;
|
|
255
370
|
}
|
|
256
371
|
function checkWriteAllowed(path) {
|
|
257
|
-
if (!path
|
|
372
|
+
if (!isAbsolute(path)) return `path must be absolute, got "${path}"`;
|
|
258
373
|
for (const root of ALLOWED_WRITE_ROOTS) {
|
|
259
374
|
if (isUnderRoot(path, root)) return null;
|
|
260
375
|
}
|
|
@@ -309,12 +424,113 @@ async function runTool(use) {
|
|
|
309
424
|
return result.code === 0 ? okResult(use.id, result.stdout) : errResult(use.id, `exit=${result.code}
|
|
310
425
|
${result.stderr || result.stdout}`);
|
|
311
426
|
}
|
|
427
|
+
if (use.name === "emit_hardware_receipt") {
|
|
428
|
+
const deviceKind = String(use.input.device_kind ?? "unknown-device");
|
|
429
|
+
const accelerator = use.input.accelerator === null || use.input.accelerator === "null" ? null : String(use.input.accelerator ?? "").trim() || null;
|
|
430
|
+
const runtimeName = String(use.input.runtime_name ?? "Ollama");
|
|
431
|
+
const runtimeVersion = String(use.input.runtime_version ?? "unknown");
|
|
432
|
+
const hostOs = String(use.input.host_os ?? "unknown");
|
|
433
|
+
const compositionId = String(use.input.composition_id ?? "unknown");
|
|
434
|
+
let measurements = [];
|
|
435
|
+
if (Array.isArray(use.input.measurements)) {
|
|
436
|
+
for (const m of use.input.measurements) {
|
|
437
|
+
const metric = String(m.metric ?? "");
|
|
438
|
+
const value = Number(m.value ?? 0);
|
|
439
|
+
const unit = String(m.unit ?? "");
|
|
440
|
+
if (metric && Number.isFinite(value)) {
|
|
441
|
+
measurements.push({ metric, value, unit, method: "measured" });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (typeof use.input.tegrastats_output === "string" && use.input.tegrastats_output.length > 0) {
|
|
446
|
+
measurements = [...measurements, ...parseTegrastats(use.input.tegrastats_output)];
|
|
447
|
+
}
|
|
448
|
+
if (measurements.length === 0) {
|
|
449
|
+
measurements.push({ metric: "agent-tick", value: 1, unit: "count", method: "presence" });
|
|
450
|
+
}
|
|
451
|
+
const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
452
|
+
const receipt = {
|
|
453
|
+
schemaVersion: "holoscript.hardware-receipt-metadata.v1",
|
|
454
|
+
target: {
|
|
455
|
+
id: `${deviceKind}-${Date.now()}`,
|
|
456
|
+
kind: deviceKind,
|
|
457
|
+
architecture: /jetson|orin|nano|agx|xavier/i.test(deviceKind) ? "arm64" : "unknown",
|
|
458
|
+
artifactKind: "measurement-trace"
|
|
459
|
+
},
|
|
460
|
+
device: {
|
|
461
|
+
vendor: /jetson|orin|nvidia/i.test(deviceKind) ? "nvidia" : "unknown",
|
|
462
|
+
model: deviceKind,
|
|
463
|
+
accelerator
|
|
464
|
+
},
|
|
465
|
+
runtime: { name: runtimeName, version: runtimeVersion, hostOS: hostOs },
|
|
466
|
+
compilerVersion: "holoscript-agent-1.0.0",
|
|
467
|
+
constraints: [],
|
|
468
|
+
measuredResults: measurements,
|
|
469
|
+
replayInputs: [
|
|
470
|
+
{ kind: "composition-ref", uri: `compositions/${compositionId}`, sha256: "unknown" }
|
|
471
|
+
],
|
|
472
|
+
provenance: {
|
|
473
|
+
capturedAt,
|
|
474
|
+
sourceCompositionHash: compositionId
|
|
475
|
+
},
|
|
476
|
+
owner: {
|
|
477
|
+
agent: process.env.HOLOSCRIPT_AGENT_HANDLE ?? "unknown",
|
|
478
|
+
...process.env.HOLOMESH_TEAM_ID ? { team: process.env.HOLOMESH_TEAM_ID } : {}
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
const ts = capturedAt.replace(/[:.]/g, "-");
|
|
482
|
+
const outPath = resolve(ALLOWED_WRITE_ROOTS[0], `hardware-receipt-${ts}.json`);
|
|
483
|
+
const denied = checkWriteAllowed(outPath);
|
|
484
|
+
if (denied) return errResult(use.id, `Cannot write receipt: ${denied}`);
|
|
485
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
486
|
+
await writeFile(outPath, JSON.stringify(receipt, null, 2), "utf8");
|
|
487
|
+
return okResult(
|
|
488
|
+
use.id,
|
|
489
|
+
`Hardware receipt written to ${outPath} \u2014 ${measurements.length} measurements, accelerator=${accelerator ?? "none"}`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
312
492
|
return errResult(use.id, `unknown tool: ${use.name}`);
|
|
313
493
|
} catch (err) {
|
|
314
494
|
return errResult(use.id, err instanceof Error ? err.message : String(err));
|
|
315
495
|
}
|
|
316
496
|
}
|
|
497
|
+
function parseTegrastats(raw) {
|
|
498
|
+
const results = [];
|
|
499
|
+
const m = (pattern, metric, unit, transform) => {
|
|
500
|
+
const match = raw.match(pattern);
|
|
501
|
+
if (match?.[1]) {
|
|
502
|
+
const value = transform ? transform(match[1]) : Number(match[1]);
|
|
503
|
+
if (Number.isFinite(value)) results.push({ metric, value, unit, method: "tegrastats" });
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
const ram = raw.match(/RAM\s+(\d+)\/(\d+)MB/);
|
|
507
|
+
if (ram) {
|
|
508
|
+
const used = Number(ram[1]);
|
|
509
|
+
const total = Number(ram[2]);
|
|
510
|
+
results.push({ metric: "ram-used", value: used, unit: "MB", method: "tegrastats" });
|
|
511
|
+
results.push({ metric: "ram-total", value: total, unit: "MB", method: "tegrastats" });
|
|
512
|
+
if (total > 0)
|
|
513
|
+
results.push({ metric: "ram-pct", value: Math.round(used / total * 100), unit: "%", method: "tegrastats" });
|
|
514
|
+
}
|
|
515
|
+
m(/GR3D_FREQ\s+(\d+)%/, "gpu-util", "%");
|
|
516
|
+
m(/EMC_FREQ\s+(\d+)%/, "emc-freq-pct", "%");
|
|
517
|
+
m(/tj@([\d.]+)C/, "temp-tj", "C", parseFloat);
|
|
518
|
+
m(/cpu@([\d.]+)C/, "temp-cpu", "C", parseFloat);
|
|
519
|
+
m(/gpu@([\d.]+)C/, "temp-gpu", "C", parseFloat);
|
|
520
|
+
m(/VDD_SOC\s+(\d+)mW/, "power-soc", "mW");
|
|
521
|
+
m(/VDD_CPU_CV\s+(\d+)mW/, "power-cpu-cv", "mW");
|
|
522
|
+
m(/VDD_IN\s+(\d+)mW/, "power-total", "mW");
|
|
523
|
+
m(/CPU\s+\[(\d+)%/, "cpu-util-core0", "%");
|
|
524
|
+
return results;
|
|
525
|
+
}
|
|
317
526
|
function runBash(cmd, cwd) {
|
|
527
|
+
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
|
|
528
|
+
return Promise.resolve({
|
|
529
|
+
code: 0,
|
|
530
|
+
stdout: `[mock-bash under vitest] cmd="${cmd}" cwd="${cwd}"`,
|
|
531
|
+
stderr: ""
|
|
532
|
+
});
|
|
533
|
+
}
|
|
318
534
|
return new Promise((resolveProm) => {
|
|
319
535
|
const child = spawn("bash", ["-c", cmd], { cwd, env: process.env });
|
|
320
536
|
let stdout = "";
|
|
@@ -383,6 +599,35 @@ var AgentRunner = class {
|
|
|
383
599
|
const { identity, brain, mesh, costGuard, provider, logger } = this.opts;
|
|
384
600
|
const log = logger ?? (() => void 0);
|
|
385
601
|
await this.heartbeatWithAutoRejoin();
|
|
602
|
+
if (this.opts.messageHandler) {
|
|
603
|
+
try {
|
|
604
|
+
const receipts = await this.opts.messageHandler.processMessages();
|
|
605
|
+
if (receipts.length > 0) {
|
|
606
|
+
log({
|
|
607
|
+
ev: "messages-processed",
|
|
608
|
+
count: receipts.length,
|
|
609
|
+
statuses: receipts.map((r) => r.status)
|
|
610
|
+
});
|
|
611
|
+
if (brain.capabilityTags.length === 0 || brain.capabilityTags.every((t) => t.startsWith("delegated"))) {
|
|
612
|
+
return {
|
|
613
|
+
action: "messages-processed",
|
|
614
|
+
spentUsd: costGuard.getState().spentUsd,
|
|
615
|
+
remainingUsd: costGuard.getRemainingUsd(),
|
|
616
|
+
receipts: receipts.map((r) => ({
|
|
617
|
+
status: r.status,
|
|
618
|
+
action: r.action,
|
|
619
|
+
reason: r.reason
|
|
620
|
+
}))
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
} catch (err) {
|
|
625
|
+
log({
|
|
626
|
+
ev: "message-handler-error",
|
|
627
|
+
message: err instanceof Error ? err.message : String(err)
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
386
631
|
if (costGuard.isOverBudget()) {
|
|
387
632
|
const state = costGuard.getState();
|
|
388
633
|
log({ ev: "over-budget", spentUsd: state.spentUsd, budget: identity.budgetUsdPerDay });
|
|
@@ -416,6 +661,8 @@ var AgentRunner = class {
|
|
|
416
661
|
const MAX_TOOL_ITERS = 30;
|
|
417
662
|
let lastResponse;
|
|
418
663
|
const toolsCalled = /* @__PURE__ */ new Set();
|
|
664
|
+
let productiveCallCount = 0;
|
|
665
|
+
let lastCommitHash;
|
|
419
666
|
while (true) {
|
|
420
667
|
iters++;
|
|
421
668
|
if (iters > MAX_TOOL_ITERS) {
|
|
@@ -423,12 +670,16 @@ var AgentRunner = class {
|
|
|
423
670
|
finalText = finalText || `[tool-loop hit ${MAX_TOOL_ITERS}-iter cap before final text]`;
|
|
424
671
|
break;
|
|
425
672
|
}
|
|
673
|
+
const activeTools = brain.requires.includes("local-llm") ? MESH_TOOLS.filter((t) => t.name === "write_file") : MESH_TOOLS;
|
|
426
674
|
const resp = await provider.complete(
|
|
427
675
|
{
|
|
428
676
|
messages,
|
|
429
|
-
|
|
677
|
+
// 8192 for local thinking models (qwen3:4b uses ~3800 tokens on thinking
|
|
678
|
+
// before the tool-call JSON; 4096 cuts off mid-generation). Frontier
|
|
679
|
+
// models ignore this ceiling and stop naturally earlier.
|
|
680
|
+
maxTokens: 8192,
|
|
430
681
|
temperature: 0.4,
|
|
431
|
-
tools:
|
|
682
|
+
tools: activeTools
|
|
432
683
|
},
|
|
433
684
|
identity.llmModel
|
|
434
685
|
);
|
|
@@ -439,13 +690,39 @@ var AgentRunner = class {
|
|
|
439
690
|
totalTokens: aggUsage.totalTokens + resp.usage.totalTokens
|
|
440
691
|
};
|
|
441
692
|
if (resp.finishReason === "tool_use" && resp.toolUses && resp.toolUses.length > 0) {
|
|
442
|
-
log({
|
|
443
|
-
|
|
693
|
+
log({
|
|
694
|
+
ev: "tool-call",
|
|
695
|
+
taskId: target.id,
|
|
696
|
+
iter: iters,
|
|
697
|
+
tools: resp.toolUses.map((t) => t.name)
|
|
698
|
+
});
|
|
699
|
+
for (const u of resp.toolUses) {
|
|
700
|
+
toolsCalled.add(u.name);
|
|
701
|
+
if (u.name === "write_file") {
|
|
702
|
+
const content = String(u.input?.content ?? "");
|
|
703
|
+
if (content.length > 0) productiveCallCount++;
|
|
704
|
+
} else if (u.name === "bash") {
|
|
705
|
+
const cmd = String(u.input?.cmd ?? "");
|
|
706
|
+
if (isProductiveBashCommand(cmd)) productiveCallCount++;
|
|
707
|
+
} else if (u.name === "emit_hardware_receipt") {
|
|
708
|
+
productiveCallCount++;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
444
711
|
messages.push({
|
|
445
712
|
role: "assistant",
|
|
446
713
|
content: resp.assistantBlocks ?? []
|
|
447
714
|
});
|
|
448
715
|
const toolResults = await Promise.all(resp.toolUses.map((u) => runTool(u)));
|
|
716
|
+
for (let ti = 0; ti < resp.toolUses.length; ti++) {
|
|
717
|
+
const tu = resp.toolUses[ti];
|
|
718
|
+
if (tu.name === "bash") {
|
|
719
|
+
const tr = toolResults[ti];
|
|
720
|
+
if (tr && !tr.is_error) {
|
|
721
|
+
const shaMatch = tr.content.match(/\b([0-9a-f]{7,40})\b/);
|
|
722
|
+
if (shaMatch) lastCommitHash = shaMatch[1];
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
449
726
|
messages.push({
|
|
450
727
|
role: "user",
|
|
451
728
|
content: toolResults
|
|
@@ -456,24 +733,75 @@ var AgentRunner = class {
|
|
|
456
733
|
break;
|
|
457
734
|
}
|
|
458
735
|
const durationMs = Date.now() - start;
|
|
459
|
-
|
|
460
|
-
const sideEffectingCalled = [...toolsCalled].some((t) => SIDE_EFFECTING_TOOLS.has(t));
|
|
461
|
-
if (!sideEffectingCalled) {
|
|
736
|
+
if (productiveCallCount === 0) {
|
|
462
737
|
log({
|
|
463
738
|
ev: "no-artifact",
|
|
464
739
|
taskId: target.id,
|
|
465
740
|
tool_iters: iters,
|
|
466
741
|
toolsCalled: [...toolsCalled],
|
|
467
|
-
|
|
742
|
+
productiveCallCount,
|
|
743
|
+
message: "task execution did not produce a real artifact \u2014 refusing to mark executed. Required: write_file with non-empty content OR bash with a productive prefix (lake build / pnpm --filter / vitest run / lean / pnpm vitest). Pure-text, read-only inspection, and trivial-bash-bypass (`echo`, `cat`, etc.) do not satisfy the gate."
|
|
468
744
|
});
|
|
469
745
|
return {
|
|
470
746
|
action: "no-artifact",
|
|
471
747
|
taskId: target.id,
|
|
472
748
|
spentUsd: costGuard.getState().spentUsd,
|
|
473
749
|
remainingUsd: costGuard.getRemainingUsd(),
|
|
474
|
-
message: `no
|
|
750
|
+
message: `no productive tool call observed (toolsCalled=[${[...toolsCalled].join(",")}], productiveCallCount=${productiveCallCount}, iters=${iters})`
|
|
475
751
|
};
|
|
476
752
|
}
|
|
753
|
+
let reflectVerdict;
|
|
754
|
+
if (brain.reflect) {
|
|
755
|
+
try {
|
|
756
|
+
const reflectResp = await provider.complete(
|
|
757
|
+
{
|
|
758
|
+
messages: [
|
|
759
|
+
{
|
|
760
|
+
role: "system",
|
|
761
|
+
content: "You are a strict reviewer. Evaluate the work against the criteria; do not rewrite it."
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
role: "user",
|
|
765
|
+
content: `Reflect on the artifact produced for this task. Evaluate it for: ${brain.reflect.criteria}.
|
|
766
|
+
|
|
767
|
+
--- artifact / final response ---
|
|
768
|
+
${finalText.slice(0, 4e3)}
|
|
769
|
+
--- end ---
|
|
770
|
+
|
|
771
|
+
Give a one-line reason, then end with exactly "VERDICT: PASS" or "VERDICT: FAIL".`
|
|
772
|
+
}
|
|
773
|
+
],
|
|
774
|
+
maxTokens: 512,
|
|
775
|
+
temperature: 0.1
|
|
776
|
+
},
|
|
777
|
+
identity.llmModel
|
|
778
|
+
);
|
|
779
|
+
aggUsage = {
|
|
780
|
+
promptTokens: aggUsage.promptTokens + reflectResp.usage.promptTokens,
|
|
781
|
+
completionTokens: aggUsage.completionTokens + reflectResp.usage.completionTokens,
|
|
782
|
+
totalTokens: aggUsage.totalTokens + reflectResp.usage.totalTokens
|
|
783
|
+
};
|
|
784
|
+
const verdictMatch = /VERDICT:\s*(PASS|FAIL)/i.exec(reflectResp.content);
|
|
785
|
+
const pass = verdictMatch ? verdictMatch[1].toUpperCase() === "PASS" : true;
|
|
786
|
+
reflectVerdict = {
|
|
787
|
+
pass,
|
|
788
|
+
reason: reflectResp.content.replace(/VERDICT:\s*(PASS|FAIL)/i, "").trim().slice(0, 300)
|
|
789
|
+
};
|
|
790
|
+
log({
|
|
791
|
+
ev: "reflect",
|
|
792
|
+
taskId: target.id,
|
|
793
|
+
pass,
|
|
794
|
+
escalateOnFail: brain.reflect.escalateOnFail,
|
|
795
|
+
reason: reflectVerdict.reason.slice(0, 120)
|
|
796
|
+
});
|
|
797
|
+
} catch (err) {
|
|
798
|
+
log({
|
|
799
|
+
ev: "reflect-error",
|
|
800
|
+
taskId: target.id,
|
|
801
|
+
message: err instanceof Error ? err.message : String(err)
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
477
805
|
const cost = costGuard.recordUsage(identity.llmModel, aggUsage);
|
|
478
806
|
log({
|
|
479
807
|
ev: "executed",
|
|
@@ -483,7 +811,11 @@ var AgentRunner = class {
|
|
|
483
811
|
tokens: aggUsage.totalTokens,
|
|
484
812
|
tool_iters: iters
|
|
485
813
|
});
|
|
486
|
-
const response = {
|
|
814
|
+
const response = {
|
|
815
|
+
...lastResponse ?? { content: finalText, usage: aggUsage },
|
|
816
|
+
content: finalText,
|
|
817
|
+
usage: aggUsage
|
|
818
|
+
};
|
|
487
819
|
const execResult = {
|
|
488
820
|
taskId: target.id,
|
|
489
821
|
responseText: response.content,
|
|
@@ -517,10 +849,32 @@ var AgentRunner = class {
|
|
|
517
849
|
});
|
|
518
850
|
const posted = await mesh.postAuditRecords(identity.handle, [caelRecord]);
|
|
519
851
|
this.prevCaelChain = caelRecord.fnv1a_chain;
|
|
520
|
-
log({
|
|
852
|
+
log({
|
|
853
|
+
ev: "cael-posted",
|
|
854
|
+
taskId: target.id,
|
|
855
|
+
appended: posted.appended,
|
|
856
|
+
rejected: posted.rejected
|
|
857
|
+
});
|
|
521
858
|
} catch (err) {
|
|
522
859
|
log({ ev: "cael-post-error", message: err instanceof Error ? err.message : String(err) });
|
|
523
860
|
}
|
|
861
|
+
if (reflectVerdict && !reflectVerdict.pass && brain.reflect?.escalateOnFail) {
|
|
862
|
+
try {
|
|
863
|
+
await mesh.sendMessageOnTask(
|
|
864
|
+
target.id,
|
|
865
|
+
`[${identity.handle}] reflect gate FAILED \u2014 escalating to the fleet instead of marking done. Reason: ${reflectVerdict.reason}`
|
|
866
|
+
);
|
|
867
|
+
} catch {
|
|
868
|
+
}
|
|
869
|
+
log({ ev: "reflect-escalate", taskId: target.id, reason: reflectVerdict.reason.slice(0, 120) });
|
|
870
|
+
return {
|
|
871
|
+
action: "reflect-escalate",
|
|
872
|
+
taskId: target.id,
|
|
873
|
+
spentUsd: costGuard.getState().spentUsd,
|
|
874
|
+
remainingUsd: costGuard.getRemainingUsd(),
|
|
875
|
+
message: `reflect self-evaluation failed; escalated to fleet (reason: ${reflectVerdict.reason.slice(0, 120)})`
|
|
876
|
+
};
|
|
877
|
+
}
|
|
524
878
|
if (this.opts.onTaskExecuted) {
|
|
525
879
|
await this.opts.onTaskExecuted(execResult, target);
|
|
526
880
|
} else {
|
|
@@ -531,6 +885,16 @@ var AgentRunner = class {
|
|
|
531
885
|
${response.content}`
|
|
532
886
|
);
|
|
533
887
|
}
|
|
888
|
+
try {
|
|
889
|
+
await mesh.markDone(target.id, finalText.slice(0, 500), lastCommitHash);
|
|
890
|
+
log({ ev: "mark-done", taskId: target.id, commitHash: lastCommitHash });
|
|
891
|
+
} catch (err) {
|
|
892
|
+
log({
|
|
893
|
+
ev: "mark-done-error",
|
|
894
|
+
taskId: target.id,
|
|
895
|
+
message: err instanceof Error ? err.message : String(err)
|
|
896
|
+
});
|
|
897
|
+
}
|
|
534
898
|
return {
|
|
535
899
|
action: "executed",
|
|
536
900
|
taskId: target.id,
|
|
@@ -623,7 +987,7 @@ function buildTaskPrompt(task) {
|
|
|
623
987
|
"Description:",
|
|
624
988
|
task.description ?? "(no description)",
|
|
625
989
|
"",
|
|
626
|
-
"Produce the deliverable
|
|
990
|
+
"Produce the deliverable: call write_file (or bash with a build command) to create all required output files FIRST. Apply your brain composition rules \u2014 anti-patterns, decision loop, and scope tier all bind. After calling the tool(s), return a short plain-text summary of what you did for posting to /room."
|
|
627
991
|
].join("\n");
|
|
628
992
|
}
|
|
629
993
|
function sleep(ms) {
|
|
@@ -637,8 +1001,10 @@ function jitter(base) {
|
|
|
637
1001
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
638
1002
|
import { dirname as dirname2 } from "path";
|
|
639
1003
|
var ANTHROPIC_PRICING_USD_PER_MTOK = {
|
|
640
|
-
"claude-opus-4-
|
|
641
|
-
|
|
1004
|
+
"claude-opus-4-8": { input: 10, output: 50 },
|
|
1005
|
+
// 3× cheaper than 4.7 on total cost; A-020 2026-06-08
|
|
1006
|
+
"claude-opus-4-7": { input: 5, output: 25 },
|
|
1007
|
+
"claude-opus-4-6": { input: 5, output: 25 },
|
|
642
1008
|
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
643
1009
|
"claude-haiku-4-5-20251001": { input: 1, output: 5 },
|
|
644
1010
|
"claude-haiku-4-5": { input: 1, output: 5 }
|
|
@@ -714,16 +1080,52 @@ function todayUtc() {
|
|
|
714
1080
|
// src/brain.ts
|
|
715
1081
|
import { readFile as readFile2 } from "fs/promises";
|
|
716
1082
|
async function loadBrain(brainPath, scopeTier = "warm") {
|
|
717
|
-
const
|
|
718
|
-
const { domain, capabilityTags } = extractIdentity(
|
|
719
|
-
|
|
1083
|
+
const raw = await readFile2(brainPath, "utf8");
|
|
1084
|
+
const { domain, capabilityTags, requires, prefers, avoids } = extractIdentity(raw);
|
|
1085
|
+
const systemPrompt = extractSystemPromptPreamble(raw);
|
|
1086
|
+
return {
|
|
1087
|
+
brainPath,
|
|
1088
|
+
systemPrompt,
|
|
1089
|
+
capabilityTags,
|
|
1090
|
+
domain,
|
|
1091
|
+
scopeTier,
|
|
1092
|
+
requires,
|
|
1093
|
+
prefers,
|
|
1094
|
+
avoids,
|
|
1095
|
+
reflect: extractReflect(raw)
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function extractReflect(brain) {
|
|
1099
|
+
const block = sliceNamedBlock(brain, "reflect");
|
|
1100
|
+
if (block === void 0) return void 0;
|
|
1101
|
+
const criteria = scalarField(block, "criteria") ?? scalarField(block, "scorer") ?? scalarField(block, "of") ?? "correctness, completeness, and valid HoloScript syntax";
|
|
1102
|
+
const escRaw = scalarField(block, "escalate_on_fail") ?? scalarField(block, "escalateOnFail") ?? scalarField(block, "escalate");
|
|
1103
|
+
return { criteria, escalateOnFail: (escRaw ?? "").split(",")[0].trim().toLowerCase() === "true" };
|
|
1104
|
+
}
|
|
1105
|
+
function extractSystemPromptPreamble(src) {
|
|
1106
|
+
const lines = src.split("\n");
|
|
1107
|
+
const BLOCK_START = /^(#version|#target|#mode|identity\s*\{|state\s*\{|computed\s*\{|traits\s*\[|capabilities\s*\{|directives\s*\{|behavior\s)/;
|
|
1108
|
+
let cutLine = -1;
|
|
1109
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1110
|
+
if (BLOCK_START.test(lines[i].trim())) {
|
|
1111
|
+
cutLine = i;
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (cutLine <= 0) return src;
|
|
1116
|
+
return lines.slice(0, cutLine).join("\n").trimEnd();
|
|
720
1117
|
}
|
|
721
1118
|
function extractIdentity(brain) {
|
|
722
1119
|
const identityBlock = sliceNamedBlock(brain, "identity");
|
|
723
|
-
if (!identityBlock)
|
|
1120
|
+
if (!identityBlock) {
|
|
1121
|
+
return { domain: "unknown", capabilityTags: [], requires: [], prefers: [], avoids: [] };
|
|
1122
|
+
}
|
|
724
1123
|
const domain = scalarField(identityBlock, "domain") ?? "unknown";
|
|
725
1124
|
const capabilityTags = listField(identityBlock, "capability_tags") ?? [];
|
|
726
|
-
|
|
1125
|
+
const requires = listField(identityBlock, "requires") ?? [];
|
|
1126
|
+
const prefers = listField(identityBlock, "prefers") ?? [];
|
|
1127
|
+
const avoids = listField(identityBlock, "avoids") ?? [];
|
|
1128
|
+
return { domain, capabilityTags, requires, prefers, avoids };
|
|
727
1129
|
}
|
|
728
1130
|
function sliceNamedBlock(src, name) {
|
|
729
1131
|
const re = new RegExp(`\\b${name}\\s*:?\\s*\\{`, "g");
|
|
@@ -801,7 +1203,9 @@ function makeCommitHook(opts) {
|
|
|
801
1203
|
const relPath = relativeTo(cwd, filePath);
|
|
802
1204
|
const addRes = spawn2("git", ["add", relPath], { cwd, encoding: "utf8" });
|
|
803
1205
|
if (addRes.status !== 0) {
|
|
804
|
-
throw new Error(
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`git add failed: ${addRes.stderr || addRes.stdout || `exit ${addRes.status}`}`
|
|
1208
|
+
);
|
|
805
1209
|
}
|
|
806
1210
|
const message = renderCommitMessage({ scope, task, identity, result });
|
|
807
1211
|
const commitArgs = ["commit", "-m", message];
|
|
@@ -810,7 +1214,9 @@ function makeCommitHook(opts) {
|
|
|
810
1214
|
}
|
|
811
1215
|
const commitRes = spawn2("git", commitArgs, { cwd, encoding: "utf8" });
|
|
812
1216
|
if (commitRes.status !== 0) {
|
|
813
|
-
throw new Error(
|
|
1217
|
+
throw new Error(
|
|
1218
|
+
`git commit failed: ${commitRes.stderr || commitRes.stdout || `exit ${commitRes.status}`}`
|
|
1219
|
+
);
|
|
814
1220
|
}
|
|
815
1221
|
const hashRes = spawn2("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" });
|
|
816
1222
|
const commitHash = hashRes.status === 0 ? hashRes.stdout.trim() : void 0;
|
|
@@ -994,6 +1400,181 @@ function applyFilter(events, filter) {
|
|
|
994
1400
|
return result;
|
|
995
1401
|
}
|
|
996
1402
|
|
|
1403
|
+
// src/capability-router.ts
|
|
1404
|
+
import {
|
|
1405
|
+
ANTHROPIC_CAPABILITIES,
|
|
1406
|
+
OPENAI_CAPABILITIES,
|
|
1407
|
+
GEMINI_CAPABILITIES,
|
|
1408
|
+
XAI_CAPABILITIES,
|
|
1409
|
+
OPENROUTER_CAPABILITIES,
|
|
1410
|
+
LOCAL_LLM_CAPABILITIES,
|
|
1411
|
+
BITNET_CAPABILITIES,
|
|
1412
|
+
MOCK_CAPABILITIES
|
|
1413
|
+
} from "@holoscript/llm-provider";
|
|
1414
|
+
var NoEligibleProviderError = class extends Error {
|
|
1415
|
+
constructor(requires, avoids, considered, excludedByAvoids) {
|
|
1416
|
+
super(
|
|
1417
|
+
`No provider satisfies brain requires=[${requires.join(", ")}] avoids=[${avoids.join(", ")}]. Considered: [${considered.join(", ")}]. Excluded by avoids: [${excludedByAvoids.join(", ")}].`
|
|
1418
|
+
);
|
|
1419
|
+
this.requires = requires;
|
|
1420
|
+
this.avoids = avoids;
|
|
1421
|
+
this.considered = considered;
|
|
1422
|
+
this.excludedByAvoids = excludedByAvoids;
|
|
1423
|
+
this.name = "NoEligibleProviderError";
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1426
|
+
function satisfies(capabilities, key) {
|
|
1427
|
+
const value = capabilities[key];
|
|
1428
|
+
if (typeof value === "boolean") return value;
|
|
1429
|
+
if (typeof value === "number") return value > 0;
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
function countMatches(capabilities, keys) {
|
|
1433
|
+
let count = 0;
|
|
1434
|
+
for (const key of keys) {
|
|
1435
|
+
if (satisfies(capabilities, key)) count++;
|
|
1436
|
+
}
|
|
1437
|
+
return count;
|
|
1438
|
+
}
|
|
1439
|
+
function unsatisfiedKeys(capabilities, keys) {
|
|
1440
|
+
return keys.filter((key) => !satisfies(capabilities, key));
|
|
1441
|
+
}
|
|
1442
|
+
function pickProvider(opts) {
|
|
1443
|
+
const { brain, envOverride, candidates } = opts;
|
|
1444
|
+
const tieBreaker = opts.tieBreakerOrder ?? candidates.map((c) => c.name);
|
|
1445
|
+
if (candidates.length === 0) {
|
|
1446
|
+
throw new Error("pickProvider: no candidates supplied");
|
|
1447
|
+
}
|
|
1448
|
+
const excludedByAvoids = [];
|
|
1449
|
+
const notAvoided = [];
|
|
1450
|
+
for (const candidate of candidates) {
|
|
1451
|
+
const matchesAvoid = brain.avoids.some((a) => satisfies(candidate.capabilities, a));
|
|
1452
|
+
if (matchesAvoid) {
|
|
1453
|
+
excludedByAvoids.push(candidate.name);
|
|
1454
|
+
} else {
|
|
1455
|
+
notAvoided.push(candidate);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
if (brain.requires.length === 0) {
|
|
1459
|
+
if (envOverride !== void 0) {
|
|
1460
|
+
const envCandidate = candidates.find((c) => c.name === envOverride);
|
|
1461
|
+
const matchedPrefers = envCandidate ? brain.prefers.filter((p) => satisfies(envCandidate.capabilities, p)) : [];
|
|
1462
|
+
return {
|
|
1463
|
+
picked: envOverride,
|
|
1464
|
+
reason: "env-override-no-requirements",
|
|
1465
|
+
unsatisfiedRequires: [],
|
|
1466
|
+
matchedPrefers,
|
|
1467
|
+
excludedByAvoids,
|
|
1468
|
+
alternatives: candidates.filter((c) => c.name !== envOverride).map((c) => c.name)
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
const ordered = orderCandidates(notAvoided, tieBreaker);
|
|
1472
|
+
if (ordered.length === 0) {
|
|
1473
|
+
return {
|
|
1474
|
+
picked: candidates[0].name,
|
|
1475
|
+
reason: "open-routing-default",
|
|
1476
|
+
unsatisfiedRequires: [],
|
|
1477
|
+
matchedPrefers: brain.prefers.filter((p) => satisfies(candidates[0].capabilities, p)),
|
|
1478
|
+
excludedByAvoids,
|
|
1479
|
+
alternatives: candidates.slice(1).map((c) => c.name)
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
return {
|
|
1483
|
+
picked: ordered[0].name,
|
|
1484
|
+
reason: "open-routing-default",
|
|
1485
|
+
unsatisfiedRequires: [],
|
|
1486
|
+
matchedPrefers: brain.prefers.filter((p) => satisfies(ordered[0].capabilities, p)),
|
|
1487
|
+
excludedByAvoids,
|
|
1488
|
+
alternatives: ordered.slice(1).map((c) => c.name)
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
const eligible = notAvoided.filter(
|
|
1492
|
+
(c) => unsatisfiedKeys(c.capabilities, brain.requires).length === 0
|
|
1493
|
+
);
|
|
1494
|
+
if (eligible.length === 0) {
|
|
1495
|
+
if (envOverride !== void 0) {
|
|
1496
|
+
const envCandidate = candidates.find((c) => c.name === envOverride);
|
|
1497
|
+
const unsatisfied = envCandidate ? unsatisfiedKeys(envCandidate.capabilities, brain.requires) : brain.requires.slice();
|
|
1498
|
+
const matchedPrefers = envCandidate ? brain.prefers.filter((p) => satisfies(envCandidate.capabilities, p)) : [];
|
|
1499
|
+
return {
|
|
1500
|
+
picked: envOverride,
|
|
1501
|
+
reason: "env-override-mismatch",
|
|
1502
|
+
unsatisfiedRequires: unsatisfied,
|
|
1503
|
+
matchedPrefers,
|
|
1504
|
+
excludedByAvoids,
|
|
1505
|
+
alternatives: []
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
throw new NoEligibleProviderError(
|
|
1509
|
+
brain.requires,
|
|
1510
|
+
brain.avoids,
|
|
1511
|
+
candidates.map((c) => c.name),
|
|
1512
|
+
excludedByAvoids
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
const ranked = [...eligible].sort((a, b) => {
|
|
1516
|
+
const aMatches = countMatches(a.capabilities, brain.prefers);
|
|
1517
|
+
const bMatches = countMatches(b.capabilities, brain.prefers);
|
|
1518
|
+
if (aMatches !== bMatches) return bMatches - aMatches;
|
|
1519
|
+
const aIdx = tieBreaker.indexOf(a.name);
|
|
1520
|
+
const bIdx = tieBreaker.indexOf(b.name);
|
|
1521
|
+
const aRank = aIdx === -1 ? Number.MAX_SAFE_INTEGER : aIdx;
|
|
1522
|
+
const bRank = bIdx === -1 ? Number.MAX_SAFE_INTEGER : bIdx;
|
|
1523
|
+
return aRank - bRank;
|
|
1524
|
+
});
|
|
1525
|
+
if (envOverride !== void 0) {
|
|
1526
|
+
const envEligible = ranked.find((c) => c.name === envOverride);
|
|
1527
|
+
if (envEligible) {
|
|
1528
|
+
return {
|
|
1529
|
+
picked: envOverride,
|
|
1530
|
+
reason: "env-override-satisfies",
|
|
1531
|
+
unsatisfiedRequires: [],
|
|
1532
|
+
matchedPrefers: brain.prefers.filter((p) => satisfies(envEligible.capabilities, p)),
|
|
1533
|
+
excludedByAvoids,
|
|
1534
|
+
alternatives: ranked.filter((c) => c.name !== envOverride).map((c) => c.name)
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
const envCandidate = candidates.find((c) => c.name === envOverride);
|
|
1538
|
+
const unsatisfied = envCandidate ? unsatisfiedKeys(envCandidate.capabilities, brain.requires) : brain.requires.slice();
|
|
1539
|
+
return {
|
|
1540
|
+
picked: envOverride,
|
|
1541
|
+
reason: "env-override-mismatch",
|
|
1542
|
+
unsatisfiedRequires: unsatisfied,
|
|
1543
|
+
matchedPrefers: envCandidate ? brain.prefers.filter((p) => satisfies(envCandidate.capabilities, p)) : [],
|
|
1544
|
+
excludedByAvoids,
|
|
1545
|
+
alternatives: ranked.map((c) => c.name)
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
const top = ranked[0];
|
|
1549
|
+
return {
|
|
1550
|
+
picked: top.name,
|
|
1551
|
+
reason: "capability-best-fit",
|
|
1552
|
+
unsatisfiedRequires: [],
|
|
1553
|
+
matchedPrefers: brain.prefers.filter((p) => satisfies(top.capabilities, p)),
|
|
1554
|
+
excludedByAvoids,
|
|
1555
|
+
alternatives: ranked.slice(1).map((c) => c.name)
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
var BUILT_IN_CANDIDATES = [
|
|
1559
|
+
{ name: "anthropic", capabilities: ANTHROPIC_CAPABILITIES },
|
|
1560
|
+
{ name: "openai", capabilities: OPENAI_CAPABILITIES },
|
|
1561
|
+
{ name: "gemini", capabilities: GEMINI_CAPABILITIES },
|
|
1562
|
+
{ name: "xai", capabilities: XAI_CAPABILITIES },
|
|
1563
|
+
{ name: "openrouter", capabilities: OPENROUTER_CAPABILITIES },
|
|
1564
|
+
{ name: "local-llm", capabilities: LOCAL_LLM_CAPABILITIES },
|
|
1565
|
+
{ name: "bitnet", capabilities: BITNET_CAPABILITIES },
|
|
1566
|
+
{ name: "mock", capabilities: MOCK_CAPABILITIES }
|
|
1567
|
+
];
|
|
1568
|
+
function orderCandidates(candidates, tieBreaker) {
|
|
1569
|
+
return [...candidates].sort((a, b) => {
|
|
1570
|
+
const aIdx = tieBreaker.indexOf(a.name);
|
|
1571
|
+
const bIdx = tieBreaker.indexOf(b.name);
|
|
1572
|
+
const aRank = aIdx === -1 ? Number.MAX_SAFE_INTEGER : aIdx;
|
|
1573
|
+
const bRank = bIdx === -1 ? Number.MAX_SAFE_INTEGER : bIdx;
|
|
1574
|
+
return aRank - bRank;
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
|
|
997
1578
|
// src/supervisor.ts
|
|
998
1579
|
var Supervisor = class {
|
|
999
1580
|
constructor(opts) {
|
|
@@ -1036,13 +1617,29 @@ var Supervisor = class {
|
|
|
1036
1617
|
return { ...managed.status };
|
|
1037
1618
|
}
|
|
1038
1619
|
async bootAgent(spec) {
|
|
1039
|
-
const identity = this.identityFromSpec(spec);
|
|
1040
1620
|
const brain = await loadBrain(spec.brainPath, spec.scopeTier ?? "warm");
|
|
1041
|
-
const
|
|
1621
|
+
const decision = pickProvider({
|
|
1622
|
+
brain,
|
|
1623
|
+
envOverride: spec.provider,
|
|
1624
|
+
candidates: BUILT_IN_CANDIDATES
|
|
1625
|
+
});
|
|
1626
|
+
const effectiveSpec = decision.picked === spec.provider ? spec : { ...spec, provider: decision.picked };
|
|
1627
|
+
const identity = this.identityFromSpec(effectiveSpec);
|
|
1628
|
+
if (decision.reason === "env-override-mismatch" && this.opts.logger) {
|
|
1629
|
+
this.opts.logger({
|
|
1630
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1631
|
+
ev: "capability-router-mismatch",
|
|
1632
|
+
handle: spec.handle,
|
|
1633
|
+
envOverride: spec.provider,
|
|
1634
|
+
unsatisfiedRequires: decision.unsatisfiedRequires,
|
|
1635
|
+
excludedByAvoids: decision.excludedByAvoids
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
const provider = await this.opts.providerFactory(effectiveSpec, identity);
|
|
1042
1639
|
const stateDir = this.opts.stateDir ?? join2(homedir(), ".holoscript-agent", "cost-state");
|
|
1043
|
-
const isFree =
|
|
1640
|
+
const isFree = effectiveSpec.provider === "mock" || effectiveSpec.provider === "local-llm" || effectiveSpec.provider === "bitnet";
|
|
1044
1641
|
const costGuard = new CostGuard({
|
|
1045
|
-
statePath: join2(stateDir, `${
|
|
1642
|
+
statePath: join2(stateDir, `${effectiveSpec.handle}.json`),
|
|
1046
1643
|
dailyBudgetUsd: identity.budgetUsdPerDay,
|
|
1047
1644
|
pricer: isFree ? () => 0 : void 0
|
|
1048
1645
|
});
|
|
@@ -1052,7 +1649,7 @@ var Supervisor = class {
|
|
|
1052
1649
|
teamId: identity.teamId,
|
|
1053
1650
|
fetchImpl: this.opts.fetchImpl
|
|
1054
1651
|
});
|
|
1055
|
-
const onTaskExecuted =
|
|
1652
|
+
const onTaskExecuted = effectiveSpec.enableCommitHook ? this.buildCommitHook(effectiveSpec, identity, mesh) : void 0;
|
|
1056
1653
|
const runner = new AgentRunner({
|
|
1057
1654
|
identity,
|
|
1058
1655
|
brain,
|
|
@@ -1061,16 +1658,16 @@ var Supervisor = class {
|
|
|
1061
1658
|
mesh,
|
|
1062
1659
|
onTaskExecuted,
|
|
1063
1660
|
auditLog: this.auditLog,
|
|
1064
|
-
logger: (ev) => this.log({ agent:
|
|
1661
|
+
logger: (ev) => this.log({ agent: effectiveSpec.handle, ...ev })
|
|
1065
1662
|
});
|
|
1066
1663
|
const status = {
|
|
1067
|
-
handle:
|
|
1664
|
+
handle: effectiveSpec.handle,
|
|
1068
1665
|
state: "starting",
|
|
1069
1666
|
spentUsd: 0,
|
|
1070
1667
|
remainingUsd: identity.budgetUsdPerDay,
|
|
1071
1668
|
restarts: 0
|
|
1072
1669
|
};
|
|
1073
|
-
return { spec, identity, brain, runner, costGuard, status };
|
|
1670
|
+
return { spec: effectiveSpec, identity, brain, runner, costGuard, status };
|
|
1074
1671
|
}
|
|
1075
1672
|
buildCommitHook(spec, identity, mesh) {
|
|
1076
1673
|
const writer = makeCommitHook({
|
|
@@ -1092,7 +1689,9 @@ var Supervisor = class {
|
|
|
1092
1689
|
}
|
|
1093
1690
|
const wallet = process.env[spec.walletEnvKey];
|
|
1094
1691
|
if (!wallet || !/^0x[0-9a-fA-F]{40}$/.test(wallet)) {
|
|
1095
|
-
throw new Error(
|
|
1692
|
+
throw new Error(
|
|
1693
|
+
`Missing or malformed wallet env var "${spec.walletEnvKey}" for agent "${spec.handle}"`
|
|
1694
|
+
);
|
|
1096
1695
|
}
|
|
1097
1696
|
return {
|
|
1098
1697
|
handle: spec.handle,
|