@holoscript/holoscript-agent 2.0.0 → 2.0.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/bin/holoscript-agent.cjs +16 -0
- package/dist/brain.js +18 -4
- package/dist/brain.js.map +1 -1
- package/dist/cost-guard.d.ts +17 -2
- package/dist/cost-guard.js +29 -3
- package/dist/cost-guard.js.map +1 -1
- package/dist/holomesh-client.d.ts +50 -1
- package/dist/holomesh-client.js +45 -0
- 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 +489 -70
- package/dist/index.js.map +1 -1
- package/dist/runner.d.ts +57 -0
- package/dist/runner.js +86 -15
- package/dist/runner.js.map +1 -1
- package/dist/supervisor-config.js +12 -5
- package/dist/supervisor-config.js.map +1 -1
- package/dist/supervisor.js +350 -29
- package/dist/supervisor.js.map +1 -1
- package/dist/types.d.ts +30 -1
- package/package.json +7 -5
package/dist/supervisor.js
CHANGED
|
@@ -65,6 +65,51 @@ var HolomeshClient = class {
|
|
|
65
65
|
wallet: raw.wallet
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
+
// ── Team Message Surface (E4 delegated-authority protocol) ───────────────────
|
|
69
|
+
/** Read recent team messages. */
|
|
70
|
+
async getTeamMessages(limit = 20) {
|
|
71
|
+
const data = await this.req(
|
|
72
|
+
"GET",
|
|
73
|
+
`/team/${this.teamId}/messages?limit=${limit}`
|
|
74
|
+
);
|
|
75
|
+
return data.messages ?? [];
|
|
76
|
+
}
|
|
77
|
+
/** Post a message to the team feed. */
|
|
78
|
+
async sendTeamMessage(content, messageType = "text") {
|
|
79
|
+
await this.req("POST", `/team/${this.teamId}/message`, {
|
|
80
|
+
content,
|
|
81
|
+
type: messageType
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// ── Owner-op API wrappers (E4) ─────────────────────────────────────────────
|
|
85
|
+
/** Switch team mode. Requires owner or founder role. */
|
|
86
|
+
async setTeamMode(mode, reason) {
|
|
87
|
+
return this.req("POST", `/team/${this.teamId}/mode`, { mode, reason });
|
|
88
|
+
}
|
|
89
|
+
/** Update room preferences. Requires config:write permission. */
|
|
90
|
+
async patchRoomPrefs(prefs) {
|
|
91
|
+
return this.req("PATCH", `/team/${this.teamId}/room`, prefs);
|
|
92
|
+
}
|
|
93
|
+
/** Update a board task. */
|
|
94
|
+
async updateTask(taskId, updates) {
|
|
95
|
+
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, {
|
|
96
|
+
action: "update",
|
|
97
|
+
...updates
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** Delete a board task. */
|
|
101
|
+
async deleteTask(taskId) {
|
|
102
|
+
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, {
|
|
103
|
+
action: "delete"
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** Delegate a board task to another agent. */
|
|
107
|
+
async delegateTask(taskId, toAgentId) {
|
|
108
|
+
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, {
|
|
109
|
+
action: "delegate",
|
|
110
|
+
toAgentId
|
|
111
|
+
});
|
|
112
|
+
}
|
|
68
113
|
async req(method, path, body) {
|
|
69
114
|
const url = `${this.apiBase}${path}`;
|
|
70
115
|
const res = await this.fetchImpl(url, {
|
|
@@ -148,7 +193,8 @@ function buildCaelRecord(input) {
|
|
|
148
193
|
prev_hash: prevChain,
|
|
149
194
|
fnv1a_chain,
|
|
150
195
|
version_vector_fingerprint: `agent@${runtimeVersion}|brain@${brainClassOf(brain)}|provider@${identity.llmProvider}|model@${identity.llmModel}`,
|
|
151
|
-
brain_class: brainClassOf(brain)
|
|
196
|
+
brain_class: brainClassOf(brain),
|
|
197
|
+
trust_epoch: "post-w107"
|
|
152
198
|
};
|
|
153
199
|
}
|
|
154
200
|
|
|
@@ -168,11 +214,7 @@ var ALLOWED_WRITE_ROOTS = [
|
|
|
168
214
|
"/root/agent-output"
|
|
169
215
|
// Single write sink — keeps deliverables in one place
|
|
170
216
|
];
|
|
171
|
-
var
|
|
172
|
-
"lake build",
|
|
173
|
-
"lake env",
|
|
174
|
-
"lake clean",
|
|
175
|
-
"lean ",
|
|
217
|
+
var BASH_READ_ONLY_PREFIXES = [
|
|
176
218
|
"ls ",
|
|
177
219
|
"ls\n",
|
|
178
220
|
"ls$",
|
|
@@ -187,12 +229,24 @@ var BASH_WHITELIST = [
|
|
|
187
229
|
"git log",
|
|
188
230
|
"git diff",
|
|
189
231
|
"git show",
|
|
232
|
+
"pwd",
|
|
233
|
+
"echo ",
|
|
234
|
+
"lake env"
|
|
235
|
+
];
|
|
236
|
+
var BASH_PRODUCTIVE_PREFIXES = [
|
|
237
|
+
"lake build",
|
|
238
|
+
"lake clean",
|
|
239
|
+
"lean ",
|
|
190
240
|
"pnpm --filter",
|
|
191
241
|
"pnpm vitest",
|
|
192
|
-
"vitest run"
|
|
193
|
-
"pwd",
|
|
194
|
-
"echo "
|
|
242
|
+
"vitest run"
|
|
195
243
|
];
|
|
244
|
+
var BASH_WHITELIST = [...BASH_READ_ONLY_PREFIXES, ...BASH_PRODUCTIVE_PREFIXES];
|
|
245
|
+
function isProductiveBashCommand(cmd) {
|
|
246
|
+
const trimmed = String(cmd ?? "").trim();
|
|
247
|
+
if (!trimmed) return false;
|
|
248
|
+
return BASH_PRODUCTIVE_PREFIXES.some((prefix) => trimmed.startsWith(prefix.trim()));
|
|
249
|
+
}
|
|
196
250
|
var MESH_TOOLS = [
|
|
197
251
|
{
|
|
198
252
|
name: "read_file",
|
|
@@ -315,6 +369,13 @@ ${result.stderr || result.stdout}`);
|
|
|
315
369
|
}
|
|
316
370
|
}
|
|
317
371
|
function runBash(cmd, cwd) {
|
|
372
|
+
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
|
|
373
|
+
return Promise.resolve({
|
|
374
|
+
code: 0,
|
|
375
|
+
stdout: `[mock-bash under vitest] cmd="${cmd}" cwd="${cwd}"`,
|
|
376
|
+
stderr: ""
|
|
377
|
+
});
|
|
378
|
+
}
|
|
318
379
|
return new Promise((resolveProm) => {
|
|
319
380
|
const child = spawn("bash", ["-c", cmd], { cwd, env: process.env });
|
|
320
381
|
let stdout = "";
|
|
@@ -383,6 +444,35 @@ var AgentRunner = class {
|
|
|
383
444
|
const { identity, brain, mesh, costGuard, provider, logger } = this.opts;
|
|
384
445
|
const log = logger ?? (() => void 0);
|
|
385
446
|
await this.heartbeatWithAutoRejoin();
|
|
447
|
+
if (this.opts.messageHandler) {
|
|
448
|
+
try {
|
|
449
|
+
const receipts = await this.opts.messageHandler.processMessages();
|
|
450
|
+
if (receipts.length > 0) {
|
|
451
|
+
log({
|
|
452
|
+
ev: "messages-processed",
|
|
453
|
+
count: receipts.length,
|
|
454
|
+
statuses: receipts.map((r) => r.status)
|
|
455
|
+
});
|
|
456
|
+
if (brain.capabilityTags.length === 0 || brain.capabilityTags.every((t) => t.startsWith("delegated"))) {
|
|
457
|
+
return {
|
|
458
|
+
action: "messages-processed",
|
|
459
|
+
spentUsd: costGuard.getState().spentUsd,
|
|
460
|
+
remainingUsd: costGuard.getRemainingUsd(),
|
|
461
|
+
receipts: receipts.map((r) => ({
|
|
462
|
+
status: r.status,
|
|
463
|
+
action: r.action,
|
|
464
|
+
reason: r.reason
|
|
465
|
+
}))
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
} catch (err) {
|
|
470
|
+
log({
|
|
471
|
+
ev: "message-handler-error",
|
|
472
|
+
message: err instanceof Error ? err.message : String(err)
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
386
476
|
if (costGuard.isOverBudget()) {
|
|
387
477
|
const state = costGuard.getState();
|
|
388
478
|
log({ ev: "over-budget", spentUsd: state.spentUsd, budget: identity.budgetUsdPerDay });
|
|
@@ -416,6 +506,8 @@ var AgentRunner = class {
|
|
|
416
506
|
const MAX_TOOL_ITERS = 30;
|
|
417
507
|
let lastResponse;
|
|
418
508
|
const toolsCalled = /* @__PURE__ */ new Set();
|
|
509
|
+
let productiveCallCount = 0;
|
|
510
|
+
let lastCommitHash;
|
|
419
511
|
while (true) {
|
|
420
512
|
iters++;
|
|
421
513
|
if (iters > MAX_TOOL_ITERS) {
|
|
@@ -440,12 +532,31 @@ var AgentRunner = class {
|
|
|
440
532
|
};
|
|
441
533
|
if (resp.finishReason === "tool_use" && resp.toolUses && resp.toolUses.length > 0) {
|
|
442
534
|
log({ ev: "tool-call", taskId: target.id, iter: iters, tools: resp.toolUses.map((t) => t.name) });
|
|
443
|
-
for (const u of resp.toolUses)
|
|
535
|
+
for (const u of resp.toolUses) {
|
|
536
|
+
toolsCalled.add(u.name);
|
|
537
|
+
if (u.name === "write_file") {
|
|
538
|
+
const content = String(u.input?.content ?? "");
|
|
539
|
+
if (content.length > 0) productiveCallCount++;
|
|
540
|
+
} else if (u.name === "bash") {
|
|
541
|
+
const cmd = String(u.input?.cmd ?? "");
|
|
542
|
+
if (isProductiveBashCommand(cmd)) productiveCallCount++;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
444
545
|
messages.push({
|
|
445
546
|
role: "assistant",
|
|
446
547
|
content: resp.assistantBlocks ?? []
|
|
447
548
|
});
|
|
448
549
|
const toolResults = await Promise.all(resp.toolUses.map((u) => runTool(u)));
|
|
550
|
+
for (let ti = 0; ti < resp.toolUses.length; ti++) {
|
|
551
|
+
const tu = resp.toolUses[ti];
|
|
552
|
+
if (tu.name === "bash") {
|
|
553
|
+
const tr = toolResults[ti];
|
|
554
|
+
if (tr && !tr.is_error) {
|
|
555
|
+
const shaMatch = tr.content.match(/\b([0-9a-f]{7,40})\b/);
|
|
556
|
+
if (shaMatch) lastCommitHash = shaMatch[1];
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
449
560
|
messages.push({
|
|
450
561
|
role: "user",
|
|
451
562
|
content: toolResults
|
|
@@ -456,22 +567,21 @@ var AgentRunner = class {
|
|
|
456
567
|
break;
|
|
457
568
|
}
|
|
458
569
|
const durationMs = Date.now() - start;
|
|
459
|
-
|
|
460
|
-
const sideEffectingCalled = [...toolsCalled].some((t) => SIDE_EFFECTING_TOOLS.has(t));
|
|
461
|
-
if (!sideEffectingCalled) {
|
|
570
|
+
if (productiveCallCount === 0) {
|
|
462
571
|
log({
|
|
463
572
|
ev: "no-artifact",
|
|
464
573
|
taskId: target.id,
|
|
465
574
|
tool_iters: iters,
|
|
466
575
|
toolsCalled: [...toolsCalled],
|
|
467
|
-
|
|
576
|
+
productiveCallCount,
|
|
577
|
+
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
578
|
});
|
|
469
579
|
return {
|
|
470
580
|
action: "no-artifact",
|
|
471
581
|
taskId: target.id,
|
|
472
582
|
spentUsd: costGuard.getState().spentUsd,
|
|
473
583
|
remainingUsd: costGuard.getRemainingUsd(),
|
|
474
|
-
message: `no
|
|
584
|
+
message: `no productive tool call observed (toolsCalled=[${[...toolsCalled].join(",")}], productiveCallCount=${productiveCallCount}, iters=${iters})`
|
|
475
585
|
};
|
|
476
586
|
}
|
|
477
587
|
const cost = costGuard.recordUsage(identity.llmModel, aggUsage);
|
|
@@ -531,6 +641,12 @@ var AgentRunner = class {
|
|
|
531
641
|
${response.content}`
|
|
532
642
|
);
|
|
533
643
|
}
|
|
644
|
+
try {
|
|
645
|
+
await mesh.markDone(target.id, finalText.slice(0, 500), lastCommitHash);
|
|
646
|
+
log({ ev: "mark-done", taskId: target.id, commitHash: lastCommitHash });
|
|
647
|
+
} catch (err) {
|
|
648
|
+
log({ ev: "mark-done-error", taskId: target.id, message: err instanceof Error ? err.message : String(err) });
|
|
649
|
+
}
|
|
534
650
|
return {
|
|
535
651
|
action: "executed",
|
|
536
652
|
taskId: target.id,
|
|
@@ -637,8 +753,8 @@ function jitter(base) {
|
|
|
637
753
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
638
754
|
import { dirname as dirname2 } from "path";
|
|
639
755
|
var ANTHROPIC_PRICING_USD_PER_MTOK = {
|
|
640
|
-
"claude-opus-4-7": { input:
|
|
641
|
-
"claude-opus-4-6": { input:
|
|
756
|
+
"claude-opus-4-7": { input: 5, output: 25 },
|
|
757
|
+
"claude-opus-4-6": { input: 5, output: 25 },
|
|
642
758
|
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
643
759
|
"claude-haiku-4-5-20251001": { input: 1, output: 5 },
|
|
644
760
|
"claude-haiku-4-5": { input: 1, output: 5 }
|
|
@@ -715,15 +831,29 @@ function todayUtc() {
|
|
|
715
831
|
import { readFile as readFile2 } from "fs/promises";
|
|
716
832
|
async function loadBrain(brainPath, scopeTier = "warm") {
|
|
717
833
|
const systemPrompt = await readFile2(brainPath, "utf8");
|
|
718
|
-
const { domain, capabilityTags } = extractIdentity(systemPrompt);
|
|
719
|
-
return {
|
|
834
|
+
const { domain, capabilityTags, requires, prefers, avoids } = extractIdentity(systemPrompt);
|
|
835
|
+
return {
|
|
836
|
+
brainPath,
|
|
837
|
+
systemPrompt,
|
|
838
|
+
capabilityTags,
|
|
839
|
+
domain,
|
|
840
|
+
scopeTier,
|
|
841
|
+
requires,
|
|
842
|
+
prefers,
|
|
843
|
+
avoids
|
|
844
|
+
};
|
|
720
845
|
}
|
|
721
846
|
function extractIdentity(brain) {
|
|
722
847
|
const identityBlock = sliceNamedBlock(brain, "identity");
|
|
723
|
-
if (!identityBlock)
|
|
848
|
+
if (!identityBlock) {
|
|
849
|
+
return { domain: "unknown", capabilityTags: [], requires: [], prefers: [], avoids: [] };
|
|
850
|
+
}
|
|
724
851
|
const domain = scalarField(identityBlock, "domain") ?? "unknown";
|
|
725
852
|
const capabilityTags = listField(identityBlock, "capability_tags") ?? [];
|
|
726
|
-
|
|
853
|
+
const requires = listField(identityBlock, "requires") ?? [];
|
|
854
|
+
const prefers = listField(identityBlock, "prefers") ?? [];
|
|
855
|
+
const avoids = listField(identityBlock, "avoids") ?? [];
|
|
856
|
+
return { domain, capabilityTags, requires, prefers, avoids };
|
|
727
857
|
}
|
|
728
858
|
function sliceNamedBlock(src, name) {
|
|
729
859
|
const re = new RegExp(`\\b${name}\\s*:?\\s*\\{`, "g");
|
|
@@ -994,6 +1124,181 @@ function applyFilter(events, filter) {
|
|
|
994
1124
|
return result;
|
|
995
1125
|
}
|
|
996
1126
|
|
|
1127
|
+
// src/capability-router.ts
|
|
1128
|
+
import {
|
|
1129
|
+
ANTHROPIC_CAPABILITIES,
|
|
1130
|
+
OPENAI_CAPABILITIES,
|
|
1131
|
+
GEMINI_CAPABILITIES,
|
|
1132
|
+
XAI_CAPABILITIES,
|
|
1133
|
+
OPENROUTER_CAPABILITIES,
|
|
1134
|
+
LOCAL_LLM_CAPABILITIES,
|
|
1135
|
+
BITNET_CAPABILITIES,
|
|
1136
|
+
MOCK_CAPABILITIES
|
|
1137
|
+
} from "@holoscript/llm-provider";
|
|
1138
|
+
var NoEligibleProviderError = class extends Error {
|
|
1139
|
+
constructor(requires, avoids, considered, excludedByAvoids) {
|
|
1140
|
+
super(
|
|
1141
|
+
`No provider satisfies brain requires=[${requires.join(", ")}] avoids=[${avoids.join(", ")}]. Considered: [${considered.join(", ")}]. Excluded by avoids: [${excludedByAvoids.join(", ")}].`
|
|
1142
|
+
);
|
|
1143
|
+
this.requires = requires;
|
|
1144
|
+
this.avoids = avoids;
|
|
1145
|
+
this.considered = considered;
|
|
1146
|
+
this.excludedByAvoids = excludedByAvoids;
|
|
1147
|
+
this.name = "NoEligibleProviderError";
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
function satisfies(capabilities, key) {
|
|
1151
|
+
const value = capabilities[key];
|
|
1152
|
+
if (typeof value === "boolean") return value;
|
|
1153
|
+
if (typeof value === "number") return value > 0;
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
function countMatches(capabilities, keys) {
|
|
1157
|
+
let count = 0;
|
|
1158
|
+
for (const key of keys) {
|
|
1159
|
+
if (satisfies(capabilities, key)) count++;
|
|
1160
|
+
}
|
|
1161
|
+
return count;
|
|
1162
|
+
}
|
|
1163
|
+
function unsatisfiedKeys(capabilities, keys) {
|
|
1164
|
+
return keys.filter((key) => !satisfies(capabilities, key));
|
|
1165
|
+
}
|
|
1166
|
+
function pickProvider(opts) {
|
|
1167
|
+
const { brain, envOverride, candidates } = opts;
|
|
1168
|
+
const tieBreaker = opts.tieBreakerOrder ?? candidates.map((c) => c.name);
|
|
1169
|
+
if (candidates.length === 0) {
|
|
1170
|
+
throw new Error("pickProvider: no candidates supplied");
|
|
1171
|
+
}
|
|
1172
|
+
const excludedByAvoids = [];
|
|
1173
|
+
const notAvoided = [];
|
|
1174
|
+
for (const candidate of candidates) {
|
|
1175
|
+
const matchesAvoid = brain.avoids.some((a) => satisfies(candidate.capabilities, a));
|
|
1176
|
+
if (matchesAvoid) {
|
|
1177
|
+
excludedByAvoids.push(candidate.name);
|
|
1178
|
+
} else {
|
|
1179
|
+
notAvoided.push(candidate);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (brain.requires.length === 0) {
|
|
1183
|
+
if (envOverride !== void 0) {
|
|
1184
|
+
const envCandidate = candidates.find((c) => c.name === envOverride);
|
|
1185
|
+
const matchedPrefers = envCandidate ? brain.prefers.filter((p) => satisfies(envCandidate.capabilities, p)) : [];
|
|
1186
|
+
return {
|
|
1187
|
+
picked: envOverride,
|
|
1188
|
+
reason: "env-override-no-requirements",
|
|
1189
|
+
unsatisfiedRequires: [],
|
|
1190
|
+
matchedPrefers,
|
|
1191
|
+
excludedByAvoids,
|
|
1192
|
+
alternatives: candidates.filter((c) => c.name !== envOverride).map((c) => c.name)
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
const ordered = orderCandidates(notAvoided, tieBreaker);
|
|
1196
|
+
if (ordered.length === 0) {
|
|
1197
|
+
return {
|
|
1198
|
+
picked: candidates[0].name,
|
|
1199
|
+
reason: "open-routing-default",
|
|
1200
|
+
unsatisfiedRequires: [],
|
|
1201
|
+
matchedPrefers: brain.prefers.filter(
|
|
1202
|
+
(p) => satisfies(candidates[0].capabilities, p)
|
|
1203
|
+
),
|
|
1204
|
+
excludedByAvoids,
|
|
1205
|
+
alternatives: candidates.slice(1).map((c) => c.name)
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
return {
|
|
1209
|
+
picked: ordered[0].name,
|
|
1210
|
+
reason: "open-routing-default",
|
|
1211
|
+
unsatisfiedRequires: [],
|
|
1212
|
+
matchedPrefers: brain.prefers.filter((p) => satisfies(ordered[0].capabilities, p)),
|
|
1213
|
+
excludedByAvoids,
|
|
1214
|
+
alternatives: ordered.slice(1).map((c) => c.name)
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
const eligible = notAvoided.filter((c) => unsatisfiedKeys(c.capabilities, brain.requires).length === 0);
|
|
1218
|
+
if (eligible.length === 0) {
|
|
1219
|
+
if (envOverride !== void 0) {
|
|
1220
|
+
const envCandidate = candidates.find((c) => c.name === envOverride);
|
|
1221
|
+
const unsatisfied = envCandidate ? unsatisfiedKeys(envCandidate.capabilities, brain.requires) : brain.requires.slice();
|
|
1222
|
+
const matchedPrefers = envCandidate ? brain.prefers.filter((p) => satisfies(envCandidate.capabilities, p)) : [];
|
|
1223
|
+
return {
|
|
1224
|
+
picked: envOverride,
|
|
1225
|
+
reason: "env-override-mismatch",
|
|
1226
|
+
unsatisfiedRequires: unsatisfied,
|
|
1227
|
+
matchedPrefers,
|
|
1228
|
+
excludedByAvoids,
|
|
1229
|
+
alternatives: []
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
throw new NoEligibleProviderError(
|
|
1233
|
+
brain.requires,
|
|
1234
|
+
brain.avoids,
|
|
1235
|
+
candidates.map((c) => c.name),
|
|
1236
|
+
excludedByAvoids
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
const ranked = [...eligible].sort((a, b) => {
|
|
1240
|
+
const aMatches = countMatches(a.capabilities, brain.prefers);
|
|
1241
|
+
const bMatches = countMatches(b.capabilities, brain.prefers);
|
|
1242
|
+
if (aMatches !== bMatches) return bMatches - aMatches;
|
|
1243
|
+
const aIdx = tieBreaker.indexOf(a.name);
|
|
1244
|
+
const bIdx = tieBreaker.indexOf(b.name);
|
|
1245
|
+
const aRank = aIdx === -1 ? Number.MAX_SAFE_INTEGER : aIdx;
|
|
1246
|
+
const bRank = bIdx === -1 ? Number.MAX_SAFE_INTEGER : bIdx;
|
|
1247
|
+
return aRank - bRank;
|
|
1248
|
+
});
|
|
1249
|
+
if (envOverride !== void 0) {
|
|
1250
|
+
const envEligible = ranked.find((c) => c.name === envOverride);
|
|
1251
|
+
if (envEligible) {
|
|
1252
|
+
return {
|
|
1253
|
+
picked: envOverride,
|
|
1254
|
+
reason: "env-override-satisfies",
|
|
1255
|
+
unsatisfiedRequires: [],
|
|
1256
|
+
matchedPrefers: brain.prefers.filter((p) => satisfies(envEligible.capabilities, p)),
|
|
1257
|
+
excludedByAvoids,
|
|
1258
|
+
alternatives: ranked.filter((c) => c.name !== envOverride).map((c) => c.name)
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
const envCandidate = candidates.find((c) => c.name === envOverride);
|
|
1262
|
+
const unsatisfied = envCandidate ? unsatisfiedKeys(envCandidate.capabilities, brain.requires) : brain.requires.slice();
|
|
1263
|
+
return {
|
|
1264
|
+
picked: envOverride,
|
|
1265
|
+
reason: "env-override-mismatch",
|
|
1266
|
+
unsatisfiedRequires: unsatisfied,
|
|
1267
|
+
matchedPrefers: envCandidate ? brain.prefers.filter((p) => satisfies(envCandidate.capabilities, p)) : [],
|
|
1268
|
+
excludedByAvoids,
|
|
1269
|
+
alternatives: ranked.map((c) => c.name)
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
const top = ranked[0];
|
|
1273
|
+
return {
|
|
1274
|
+
picked: top.name,
|
|
1275
|
+
reason: "capability-best-fit",
|
|
1276
|
+
unsatisfiedRequires: [],
|
|
1277
|
+
matchedPrefers: brain.prefers.filter((p) => satisfies(top.capabilities, p)),
|
|
1278
|
+
excludedByAvoids,
|
|
1279
|
+
alternatives: ranked.slice(1).map((c) => c.name)
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
var BUILT_IN_CANDIDATES = [
|
|
1283
|
+
{ name: "anthropic", capabilities: ANTHROPIC_CAPABILITIES },
|
|
1284
|
+
{ name: "openai", capabilities: OPENAI_CAPABILITIES },
|
|
1285
|
+
{ name: "gemini", capabilities: GEMINI_CAPABILITIES },
|
|
1286
|
+
{ name: "xai", capabilities: XAI_CAPABILITIES },
|
|
1287
|
+
{ name: "openrouter", capabilities: OPENROUTER_CAPABILITIES },
|
|
1288
|
+
{ name: "local-llm", capabilities: LOCAL_LLM_CAPABILITIES },
|
|
1289
|
+
{ name: "bitnet", capabilities: BITNET_CAPABILITIES },
|
|
1290
|
+
{ name: "mock", capabilities: MOCK_CAPABILITIES }
|
|
1291
|
+
];
|
|
1292
|
+
function orderCandidates(candidates, tieBreaker) {
|
|
1293
|
+
return [...candidates].sort((a, b) => {
|
|
1294
|
+
const aIdx = tieBreaker.indexOf(a.name);
|
|
1295
|
+
const bIdx = tieBreaker.indexOf(b.name);
|
|
1296
|
+
const aRank = aIdx === -1 ? Number.MAX_SAFE_INTEGER : aIdx;
|
|
1297
|
+
const bRank = bIdx === -1 ? Number.MAX_SAFE_INTEGER : bIdx;
|
|
1298
|
+
return aRank - bRank;
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
|
|
997
1302
|
// src/supervisor.ts
|
|
998
1303
|
var Supervisor = class {
|
|
999
1304
|
constructor(opts) {
|
|
@@ -1036,13 +1341,29 @@ var Supervisor = class {
|
|
|
1036
1341
|
return { ...managed.status };
|
|
1037
1342
|
}
|
|
1038
1343
|
async bootAgent(spec) {
|
|
1039
|
-
const identity = this.identityFromSpec(spec);
|
|
1040
1344
|
const brain = await loadBrain(spec.brainPath, spec.scopeTier ?? "warm");
|
|
1041
|
-
const
|
|
1345
|
+
const decision = pickProvider({
|
|
1346
|
+
brain,
|
|
1347
|
+
envOverride: spec.provider,
|
|
1348
|
+
candidates: BUILT_IN_CANDIDATES
|
|
1349
|
+
});
|
|
1350
|
+
const effectiveSpec = decision.picked === spec.provider ? spec : { ...spec, provider: decision.picked };
|
|
1351
|
+
const identity = this.identityFromSpec(effectiveSpec);
|
|
1352
|
+
if (decision.reason === "env-override-mismatch" && this.opts.logger) {
|
|
1353
|
+
this.opts.logger({
|
|
1354
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1355
|
+
ev: "capability-router-mismatch",
|
|
1356
|
+
handle: spec.handle,
|
|
1357
|
+
envOverride: spec.provider,
|
|
1358
|
+
unsatisfiedRequires: decision.unsatisfiedRequires,
|
|
1359
|
+
excludedByAvoids: decision.excludedByAvoids
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
const provider = await this.opts.providerFactory(effectiveSpec, identity);
|
|
1042
1363
|
const stateDir = this.opts.stateDir ?? join2(homedir(), ".holoscript-agent", "cost-state");
|
|
1043
|
-
const isFree =
|
|
1364
|
+
const isFree = effectiveSpec.provider === "mock" || effectiveSpec.provider === "local-llm" || effectiveSpec.provider === "bitnet";
|
|
1044
1365
|
const costGuard = new CostGuard({
|
|
1045
|
-
statePath: join2(stateDir, `${
|
|
1366
|
+
statePath: join2(stateDir, `${effectiveSpec.handle}.json`),
|
|
1046
1367
|
dailyBudgetUsd: identity.budgetUsdPerDay,
|
|
1047
1368
|
pricer: isFree ? () => 0 : void 0
|
|
1048
1369
|
});
|
|
@@ -1052,7 +1373,7 @@ var Supervisor = class {
|
|
|
1052
1373
|
teamId: identity.teamId,
|
|
1053
1374
|
fetchImpl: this.opts.fetchImpl
|
|
1054
1375
|
});
|
|
1055
|
-
const onTaskExecuted =
|
|
1376
|
+
const onTaskExecuted = effectiveSpec.enableCommitHook ? this.buildCommitHook(effectiveSpec, identity, mesh) : void 0;
|
|
1056
1377
|
const runner = new AgentRunner({
|
|
1057
1378
|
identity,
|
|
1058
1379
|
brain,
|
|
@@ -1061,16 +1382,16 @@ var Supervisor = class {
|
|
|
1061
1382
|
mesh,
|
|
1062
1383
|
onTaskExecuted,
|
|
1063
1384
|
auditLog: this.auditLog,
|
|
1064
|
-
logger: (ev) => this.log({ agent:
|
|
1385
|
+
logger: (ev) => this.log({ agent: effectiveSpec.handle, ...ev })
|
|
1065
1386
|
});
|
|
1066
1387
|
const status = {
|
|
1067
|
-
handle:
|
|
1388
|
+
handle: effectiveSpec.handle,
|
|
1068
1389
|
state: "starting",
|
|
1069
1390
|
spentUsd: 0,
|
|
1070
1391
|
remainingUsd: identity.budgetUsdPerDay,
|
|
1071
1392
|
restarts: 0
|
|
1072
1393
|
};
|
|
1073
|
-
return { spec, identity, brain, runner, costGuard, status };
|
|
1394
|
+
return { spec: effectiveSpec, identity, brain, runner, costGuard, status };
|
|
1074
1395
|
}
|
|
1075
1396
|
buildCommitHook(spec, identity, mesh) {
|
|
1076
1397
|
const writer = makeCommitHook({
|