agentlas 0.7.0 → 0.9.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/CHANGELOG.md +199 -0
- package/README.md +161 -18
- package/bin/agentlas.cjs +8 -8
- package/engine/agentlas-core-harness.cjs +212 -0
- package/engine/agentlas-desktop-loadout.cjs +527 -0
- package/engine/agentlas-doctor.cjs +1 -1
- package/engine/agentlas-experience-exchange.cjs +835 -85
- package/engine/agentlas-experience-intake.cjs +444 -0
- package/engine/agentlas-experience-mcp.cjs +580 -18
- package/engine/agentlas-i18n.cjs +10 -10
- package/engine/agentlas-input.cjs +5 -4
- package/engine/agentlas-mcp-env.cjs +219 -0
- package/engine/agentlas-mcp-wrapper.cjs +51 -0
- package/engine/agentlas-memory-governance.cjs +1029 -0
- package/engine/agentlas-native-host.cjs +129 -39
- package/engine/agentlas-parity.cjs +339 -154
- package/engine/agentlas-repl.cjs +306 -31
- package/engine/agentlas-workforce.cjs +2991 -0
- package/engine/agentlas-workload-routing.cjs +523 -0
- package/engine/agentlas.cjs +1619 -234
- package/engine/bootstrap-schema.sql +1 -1
- package/engine/experience-taxonomy-v1.json +49 -0
- package/package.json +8 -4
- package/scripts/gen-bootstrap-schema.sh +0 -23
- package/test/bootstrap-race.cjs +0 -47
- package/test/capture-runtime-guard.cjs +0 -122
- package/test/cloud-asset-restore.cjs +0 -423
- package/test/cloud-cas-client.cjs +0 -333
- package/test/cloud-owner-restore.cjs +0 -183
- package/test/cloud-runtime-paths.cjs +0 -40
- package/test/cloud-save-publish.cjs +0 -487
- package/test/credential-env-regression.cjs +0 -52
- package/test/engine-hardening-regression.cjs +0 -74
- package/test/experience-exchange-contract.cjs +0 -569
- package/test/experience-mcp-contract.cjs +0 -391
- package/test/fixtures/portable-experience-bundle-v1-golden.json +0 -124
- package/test/login-loopback-security.cjs +0 -115
- package/test/mcp-config-isolation.cjs +0 -36
- package/test/permission-mapping.cjs +0 -180
- package/test/route-regression.cjs +0 -357
- package/test/run-api-regression.cjs +0 -322
- package/test/runtime-env-protection.cjs +0 -89
- package/test/semver-precedence.cjs +0 -39
- package/test/smoke.sh +0 -93
- package/test/sqlite-driver-probe.cjs +0 -22
- package/test/terminal-ui-regression.cjs +0 -477
- package/test/timeout-regression.cjs +0 -218
- package/test/tool-workspace-boundary.cjs +0 -165
- package/test/update-safety.cjs +0 -376
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* AI-authored workload allocation for Terminal system agents.
|
|
5
|
+
*
|
|
6
|
+
* Important boundary: this module NEVER judges a task from words or regexes. A
|
|
7
|
+
* parent LLM writes the exact live-inventory decision. Host policy code only
|
|
8
|
+
* validates that decision and applies pins/policy/capability constraints. It
|
|
9
|
+
* never manufactures a provider model id from a tier.
|
|
10
|
+
*/
|
|
11
|
+
const crypto = require("node:crypto");
|
|
12
|
+
const fs = require("node:fs");
|
|
13
|
+
const os = require("node:os");
|
|
14
|
+
const path = require("node:path");
|
|
15
|
+
|
|
16
|
+
const SCHEMA_VERSION = 1;
|
|
17
|
+
const ALLOCATION_SCHEMA = "agentlas.workload-allocation.v1";
|
|
18
|
+
const TIERS = Object.freeze(["economy", "balanced", "frontier"]);
|
|
19
|
+
const TIER_RANK = Object.freeze({ economy: 0, balanced: 1, frontier: 2 });
|
|
20
|
+
const EFFORTS = Object.freeze(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
21
|
+
const CAPABILITIES = new Set(["code", "image", "tools", "long-context"]);
|
|
22
|
+
const PHASES = new Set(["plan", "delegate", "synthesize"]);
|
|
23
|
+
|
|
24
|
+
function cleanText(value, max = 240) {
|
|
25
|
+
return String(value || "").replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, max);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeTier(value) {
|
|
29
|
+
const v = String(value || "").toLowerCase().trim();
|
|
30
|
+
return TIERS.includes(v) ? v : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeEffort(value) {
|
|
34
|
+
const v = String(value || "").toLowerCase().trim();
|
|
35
|
+
return EFFORTS.includes(v) ? v : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeCapabilities(value) {
|
|
39
|
+
if (!Array.isArray(value)) return [];
|
|
40
|
+
return [...new Set(value.map((item) => String(item || "").toLowerCase().trim()).filter((item) => CAPABILITIES.has(item)))].slice(0, 8);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeReasonCodes(value) {
|
|
44
|
+
if (!Array.isArray(value)) return [];
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const item of value) {
|
|
47
|
+
const code = cleanText(item, 48).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
48
|
+
if (code && !out.includes(code)) out.push(code);
|
|
49
|
+
if (out.length >= 8) break;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeAllocation(value, expectedPhase = null) {
|
|
55
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
56
|
+
const tier = normalizeTier(value.tier || value.modelTier || value.model_tier);
|
|
57
|
+
const effort = normalizeEffort(value.effort);
|
|
58
|
+
const reason = cleanText(value.rationale || value.reason, 240);
|
|
59
|
+
if (!tier || !effort || !reason) return null;
|
|
60
|
+
const rawPhase = cleanText(value.phase, 20).toLowerCase();
|
|
61
|
+
const phase = PHASES.has(rawPhase) ? rawPhase : expectedPhase && PHASES.has(expectedPhase) ? expectedPhase : null;
|
|
62
|
+
if (expectedPhase && phase !== expectedPhase) return null;
|
|
63
|
+
const contextTokensRaw = value.estimatedContextTokens ?? value.estimated_context_tokens ?? value.contextTokens;
|
|
64
|
+
const contextTokens = Number.isSafeInteger(Number(contextTokensRaw)) && Number(contextTokensRaw) >= 0
|
|
65
|
+
? Math.min(Number(contextTokensRaw), 10_000_000)
|
|
66
|
+
: null;
|
|
67
|
+
const reasonCodes = normalizeReasonCodes(value.reasonCodes || value.reason_codes);
|
|
68
|
+
return {
|
|
69
|
+
schema: ALLOCATION_SCHEMA,
|
|
70
|
+
decisionId: cleanText(value.decisionId || value.decision_id, 255) || null,
|
|
71
|
+
selectorVersion: cleanText(value.selectorVersion || value.selector_version, 255) || "agentlas-terminal.parent-ai.v1",
|
|
72
|
+
inputFeatureHash: /^sha256:[0-9a-f]{64}$/.test(String(value.inputFeatureHash || value.input_feature_hash || ""))
|
|
73
|
+
? String(value.inputFeatureHash || value.input_feature_hash)
|
|
74
|
+
: null,
|
|
75
|
+
tier,
|
|
76
|
+
modelClass: cleanText(value.modelClass || value.model_class, 32) || null,
|
|
77
|
+
runtimeId: cleanText(value.runtimeId || value.runtime_id || value.sessionId || value.session_id, 255) || null,
|
|
78
|
+
exactModelId: cleanText(value.exactModelId || value.exact_model_id || value.modelId, 255) || null,
|
|
79
|
+
effort,
|
|
80
|
+
phase,
|
|
81
|
+
reasonCodes: reasonCodes.length ? reasonCodes : ["ai-assigned"],
|
|
82
|
+
reason,
|
|
83
|
+
requiredCapabilities: normalizeCapabilities(value.requiredCapabilities || value.required_capabilities),
|
|
84
|
+
estimatedContextTokens: contextTokens,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extractJsonObject(text) {
|
|
89
|
+
const source = String(text || "").trim();
|
|
90
|
+
const fenced = source.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
91
|
+
const candidate = fenced ? fenced[1].trim() : source;
|
|
92
|
+
const start = candidate.indexOf("{");
|
|
93
|
+
const end = candidate.lastIndexOf("}");
|
|
94
|
+
if (start < 0 || end <= start) return null;
|
|
95
|
+
try { return JSON.parse(candidate.slice(start, end + 1)); } catch { return null; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizePlan(raw, { maxTasks = 12 } = {}) {
|
|
99
|
+
const value = typeof raw === "string" ? extractJsonObject(raw) : raw;
|
|
100
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
101
|
+
if (!Array.isArray(value.tasks) || !value.tasks.length) return null;
|
|
102
|
+
const tasks = [];
|
|
103
|
+
for (const item of value.tasks.slice(0, Math.max(1, Math.min(24, maxTasks)))) {
|
|
104
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
105
|
+
const brief = cleanText(item.brief, 8_000);
|
|
106
|
+
const title = cleanText(item.title || brief, 120);
|
|
107
|
+
const allocation = normalizeAllocation(item.allocation || item, "delegate");
|
|
108
|
+
if (!brief || !title || !allocation) continue;
|
|
109
|
+
tasks.push({ title, brief, role: cleanText(item.role, 80) || undefined, allocation });
|
|
110
|
+
}
|
|
111
|
+
const synthesis = normalizeAllocation(value.synthesis, "synthesize");
|
|
112
|
+
if (!tasks.length || !synthesis) return null;
|
|
113
|
+
return { schemaVersion: SCHEMA_VERSION, tasks, synthesis };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function runtimeProvider(runtime) {
|
|
117
|
+
if (!runtime) return "";
|
|
118
|
+
if (runtime.mode === "cli") return String(runtime.kind || "");
|
|
119
|
+
if (runtime.backend === "anthropic") return "anthropic-api";
|
|
120
|
+
if (runtime.backend === "openai") return "openai-api";
|
|
121
|
+
return String(runtime.backend || "");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function defaultAvailableModels(runtime) {
|
|
125
|
+
const provider = runtimeProvider(runtime);
|
|
126
|
+
const current = runtime && runtime.model ? {
|
|
127
|
+
id: runtime.model,
|
|
128
|
+
tier: runtime.modelTier || runtime.tier || null,
|
|
129
|
+
capabilities: runtime.capabilities || [],
|
|
130
|
+
contextWindow: runtime.contextWindow || null,
|
|
131
|
+
efforts: runtime.efforts || [],
|
|
132
|
+
description: runtime.modelDescription || "host-selected current model",
|
|
133
|
+
} : null;
|
|
134
|
+
if (provider === "codex") {
|
|
135
|
+
const detected = readCodexModelInventory();
|
|
136
|
+
const currentIndex = current ? detected.findIndex((model) => model.id === current.id) : -1;
|
|
137
|
+
if (current && currentIndex < 0) {
|
|
138
|
+
detected.push(...normalizeAvailableModels([current]));
|
|
139
|
+
} else if (current && currentIndex >= 0) {
|
|
140
|
+
detected[currentIndex] = {
|
|
141
|
+
...detected[currentIndex],
|
|
142
|
+
tier: detected[currentIndex].tier || current.tier,
|
|
143
|
+
capabilities: [...new Set([...detected[currentIndex].capabilities, ...normalizeCapabilities(current.capabilities)])],
|
|
144
|
+
contextWindow: detected[currentIndex].contextWindow || current.contextWindow,
|
|
145
|
+
efforts: [...new Set([...detected[currentIndex].efforts, ...current.efforts.map(normalizeEffort).filter(Boolean)])],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return detected;
|
|
149
|
+
}
|
|
150
|
+
if (current) return normalizeAvailableModels([current]);
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readCodexModelInventory(codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex")) {
|
|
155
|
+
const file = path.join(codexHome, "models_cache.json");
|
|
156
|
+
try {
|
|
157
|
+
const stat = fs.statSync(file);
|
|
158
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > 2 * 1024 * 1024) return [];
|
|
159
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
160
|
+
if (!Array.isArray(parsed.models)) return [];
|
|
161
|
+
const out = [];
|
|
162
|
+
const seen = new Set();
|
|
163
|
+
for (const model of parsed.models) {
|
|
164
|
+
if (!model || model.visibility !== "list" || typeof model.slug !== "string") continue;
|
|
165
|
+
const id = model.slug.trim();
|
|
166
|
+
if (!/^[a-z0-9][a-z0-9._-]{0,127}$/.test(id) || seen.has(id)) continue;
|
|
167
|
+
seen.add(id);
|
|
168
|
+
const contextWindow = Number.isSafeInteger(model.context_window) && model.context_window > 0
|
|
169
|
+
? model.context_window
|
|
170
|
+
: Number.isSafeInteger(model.max_context_window) && model.max_context_window > 0
|
|
171
|
+
? model.max_context_window
|
|
172
|
+
: null;
|
|
173
|
+
const capabilities = ["code"];
|
|
174
|
+
if (model.tool_mode || model.shell_type || model.supports_parallel_tool_calls) capabilities.push("tools");
|
|
175
|
+
if (Array.isArray(model.input_modalities) && model.input_modalities.includes("image")) capabilities.push("image");
|
|
176
|
+
if (contextWindow) capabilities.push("long-context");
|
|
177
|
+
out.push({
|
|
178
|
+
id,
|
|
179
|
+
tier: normalizeTier(model.tier || model.cost_tier),
|
|
180
|
+
capabilities,
|
|
181
|
+
contextWindow,
|
|
182
|
+
efforts: Array.isArray(model.supported_reasoning_levels)
|
|
183
|
+
? model.supported_reasoning_levels.map((item) => normalizeEffort(item && item.effort)).filter(Boolean)
|
|
184
|
+
: [],
|
|
185
|
+
description: cleanText(model.description, 300) || null,
|
|
186
|
+
priority: Number.isFinite(Number(model.priority)) ? Number(model.priority) : null,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
} catch {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeAvailableModels(models) {
|
|
196
|
+
if (!Array.isArray(models)) return [];
|
|
197
|
+
const out = [];
|
|
198
|
+
for (const item of models) {
|
|
199
|
+
const value = typeof item === "string" ? { id: item } : item;
|
|
200
|
+
if (!value || typeof value !== "object") continue;
|
|
201
|
+
const id = cleanText(value.id || value.model, 160);
|
|
202
|
+
const tier = normalizeTier(value.tier || value.costTier || value.cost_tier);
|
|
203
|
+
if (!id) continue;
|
|
204
|
+
const contextWindow = Number.isSafeInteger(Number(value.contextWindow)) && Number(value.contextWindow) > 0
|
|
205
|
+
? Number(value.contextWindow)
|
|
206
|
+
: null;
|
|
207
|
+
out.push({
|
|
208
|
+
id,
|
|
209
|
+
tier,
|
|
210
|
+
capabilities: normalizeCapabilities(value.capabilities),
|
|
211
|
+
costTier: normalizeTier(value.costTier) || tier,
|
|
212
|
+
contextWindow,
|
|
213
|
+
efforts: Array.isArray(value.efforts) ? value.efforts.map(normalizeEffort).filter(Boolean) : [],
|
|
214
|
+
description: cleanText(value.description, 300) || null,
|
|
215
|
+
priority: Number.isFinite(Number(value.priority)) ? Number(value.priority) : null,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// This is intentionally a small, privacy-safe inventory. The parent LLM sees
|
|
222
|
+
// only executable runtime ids, model ids and supported effort levels; never a
|
|
223
|
+
// path, account, prompt, credential, or transcript.
|
|
224
|
+
function runtimeInventory(runtimes) {
|
|
225
|
+
return (Array.isArray(runtimes) ? runtimes : []).map((runtime, index) => ({
|
|
226
|
+
runtimeId: cleanText(runtime && (runtime.runtimeId || runtime.id), 255) || `runtime-${index + 1}`,
|
|
227
|
+
kind: cleanText(runtime && runtime.kind, 80) || null,
|
|
228
|
+
backend: cleanText(runtime && runtime.backend, 80) || null,
|
|
229
|
+
mode: cleanText(runtime && runtime.mode, 24) || null,
|
|
230
|
+
models: normalizeAvailableModels(runtime && runtime.availableModels || defaultAvailableModels(runtime)).map((model) => ({
|
|
231
|
+
id: model.id,
|
|
232
|
+
efforts: model.efforts,
|
|
233
|
+
capabilities: model.capabilities,
|
|
234
|
+
contextWindow: model.contextWindow,
|
|
235
|
+
tier: model.tier,
|
|
236
|
+
description: model.description,
|
|
237
|
+
priority: model.priority,
|
|
238
|
+
})),
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function resolveEffort(provider, requested, supported = []) {
|
|
243
|
+
if (provider === "claude-code") {
|
|
244
|
+
if (requested === "none") return null;
|
|
245
|
+
const mapped = requested === "minimal" ? "low" : requested === "xhigh" ? "max" : requested;
|
|
246
|
+
const available = Array.isArray(supported) ? supported : [];
|
|
247
|
+
return available.includes(mapped) ? mapped : null;
|
|
248
|
+
}
|
|
249
|
+
if (provider === "codex") {
|
|
250
|
+
if (requested === "none") return null;
|
|
251
|
+
const available = Array.isArray(supported) ? supported : [];
|
|
252
|
+
if (!available.length) return null;
|
|
253
|
+
if (available.includes(requested)) return requested;
|
|
254
|
+
const requestedRank = EFFORTS.indexOf(requested);
|
|
255
|
+
const lower = available
|
|
256
|
+
.filter((item) => EFFORTS.indexOf(item) <= requestedRank)
|
|
257
|
+
.sort((a, b) => EFFORTS.indexOf(b) - EFFORTS.indexOf(a))[0];
|
|
258
|
+
return lower || available[0] || null;
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function resolveAllocation(options = {}) {
|
|
264
|
+
const runtime = options.runtime || null;
|
|
265
|
+
const provider = runtimeProvider(runtime);
|
|
266
|
+
const decision = normalizeAllocation(options.decision);
|
|
267
|
+
const modelPin = cleanText(options.modelPin, 160) || null;
|
|
268
|
+
const effortPinPresent = options.effortPin !== undefined && options.effortPin !== null;
|
|
269
|
+
const effortPin = effortPinPresent ? normalizeEffort(options.effortPin) : null;
|
|
270
|
+
const reasons = [];
|
|
271
|
+
|
|
272
|
+
if (!decision) {
|
|
273
|
+
const pinnedEffort = effortPinPresent && effortPin !== "none" ? resolveEffort(provider, effortPin) : null;
|
|
274
|
+
const pinReasons = ["invalid_ai_allocation"];
|
|
275
|
+
if (modelPin) pinReasons.push("explicit_model_pin");
|
|
276
|
+
if (effortPinPresent) pinReasons.push("explicit_effort_pin");
|
|
277
|
+
return {
|
|
278
|
+
ok: Boolean(modelPin),
|
|
279
|
+
tier: null,
|
|
280
|
+
model: modelPin,
|
|
281
|
+
effort: pinnedEffort,
|
|
282
|
+
provider,
|
|
283
|
+
source: modelPin || effortPinPresent ? "user-pin" : "fallback",
|
|
284
|
+
fallbackReason: pinReasons.join(","),
|
|
285
|
+
aiReason: null,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let tier = decision.tier;
|
|
290
|
+
const maxTier = normalizeTier(options.maxTier);
|
|
291
|
+
const invalidCostPolicy = Boolean(options.maxTier && !maxTier);
|
|
292
|
+
if (invalidCostPolicy) reasons.push("invalid_cost_policy");
|
|
293
|
+
if (maxTier && TIER_RANK[tier] > TIER_RANK[maxTier]) {
|
|
294
|
+
tier = maxTier;
|
|
295
|
+
reasons.push("cost_policy_clamped");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const available = normalizeAvailableModels(options.availableModels || runtime && runtime.availableModels || defaultAvailableModels(runtime));
|
|
299
|
+
// A live parent decision names an exact model. Validate it against inventory;
|
|
300
|
+
// never choose the first model in a tier or sort models into a hidden fallback.
|
|
301
|
+
let selected = decision.exactModelId
|
|
302
|
+
? available.find((item) => item.id === decision.exactModelId) || null
|
|
303
|
+
: null;
|
|
304
|
+
if (decision.exactModelId && !selected) reasons.push("parent_model_not_in_live_inventory");
|
|
305
|
+
if (!decision.exactModelId) reasons.push("parent_exact_model_required");
|
|
306
|
+
const candidateIssue = (candidate, prefix) => {
|
|
307
|
+
if (!candidate) return `${prefix}_not_in_live_inventory`;
|
|
308
|
+
if (invalidCostPolicy) return "invalid_cost_policy";
|
|
309
|
+
const caps = new Set(candidate.capabilities);
|
|
310
|
+
if (decision.requiredCapabilities.some((required) => !caps.has(required))) return `${prefix}_capability_mismatch`;
|
|
311
|
+
if (decision.estimatedContextTokens != null && decision.estimatedContextTokens > 0) {
|
|
312
|
+
if (candidate.contextWindow == null) return `${prefix}_context_window_unknown`;
|
|
313
|
+
if (decision.estimatedContextTokens > candidate.contextWindow) return `${prefix}_context_window_exceeded`;
|
|
314
|
+
}
|
|
315
|
+
if (maxTier && !candidate.tier) return `${prefix}_cost_tier_unknown`;
|
|
316
|
+
if (maxTier && TIER_RANK[candidate.tier] > TIER_RANK[maxTier]) return `${prefix}_exceeds_cost_policy`;
|
|
317
|
+
if (candidate.tier && candidate.tier !== tier) return `${prefix}_tier_mismatch`;
|
|
318
|
+
return null;
|
|
319
|
+
};
|
|
320
|
+
if (selected) {
|
|
321
|
+
const issue = candidateIssue(selected, "selected_model");
|
|
322
|
+
if (issue) {
|
|
323
|
+
selected = null;
|
|
324
|
+
if (!reasons.includes(issue)) reasons.push(issue);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let model = selected && selected.id;
|
|
329
|
+
let effectiveModelEntry = selected;
|
|
330
|
+
let source = decision.exactModelId ? "parent-ai-exact" : "fallback";
|
|
331
|
+
if (modelPin) {
|
|
332
|
+
model = modelPin;
|
|
333
|
+
effectiveModelEntry = available.find((item) => item.id === modelPin) || null;
|
|
334
|
+
source = "user-pin";
|
|
335
|
+
reasons.push("explicit_model_pin");
|
|
336
|
+
} else if (!model) {
|
|
337
|
+
source = "fallback";
|
|
338
|
+
const activeId = cleanText(runtime && runtime.model, 160) || null;
|
|
339
|
+
const active = activeId ? available.find((item) => item.id === activeId) || null : null;
|
|
340
|
+
const issue = activeId ? candidateIssue(active, "active_model") : "active_model_unavailable";
|
|
341
|
+
if (issue) {
|
|
342
|
+
if (!reasons.includes(issue)) reasons.push(issue);
|
|
343
|
+
} else {
|
|
344
|
+
model = active.id;
|
|
345
|
+
effectiveModelEntry = active;
|
|
346
|
+
reasons.push("compliant_active_model_fallback");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let effort;
|
|
351
|
+
if (effortPinPresent) {
|
|
352
|
+
effort = effortPin === "none" ? null : resolveEffort(provider, effortPin, effectiveModelEntry && effectiveModelEntry.efforts || []);
|
|
353
|
+
if (effort == null && effortPin !== "none" && modelPin) {
|
|
354
|
+
if (provider === "codex") effort = effortPin;
|
|
355
|
+
if (provider === "claude-code") effort = effortPin === "minimal" ? "low" : effortPin === "xhigh" ? "max" : effortPin;
|
|
356
|
+
}
|
|
357
|
+
source = source === "user-pin" ? source : "user-pin";
|
|
358
|
+
reasons.push("explicit_effort_pin");
|
|
359
|
+
if (effortPin !== "none" && effort == null) reasons.push("effort_pin_unsupported_by_provider");
|
|
360
|
+
} else {
|
|
361
|
+
effort = resolveEffort(provider, decision.effort, effectiveModelEntry && effectiveModelEntry.efforts || []);
|
|
362
|
+
if (effort == null && decision.effort !== "none") reasons.push("effort_unsupported_by_provider");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
ok: Boolean(model),
|
|
367
|
+
tier,
|
|
368
|
+
model,
|
|
369
|
+
effort: effort || null,
|
|
370
|
+
provider,
|
|
371
|
+
source,
|
|
372
|
+
fallbackReason: reasons.join(",") || null,
|
|
373
|
+
aiReason: decision.reason,
|
|
374
|
+
requested: decision,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function resolveAllocationAcrossRuntimes(options = {}) {
|
|
379
|
+
const runtimes = Array.isArray(options.runtimes) ? options.runtimes : [];
|
|
380
|
+
const decision = normalizeAllocation(options.decision);
|
|
381
|
+
const fallbackRuntime = options.fallbackRuntime || options.runtime || runtimes[0] || null;
|
|
382
|
+
const fallbackId = cleanText(fallbackRuntime && (fallbackRuntime.runtimeId || fallbackRuntime.id), 255) || null;
|
|
383
|
+
const requestedId = decision && decision.runtimeId;
|
|
384
|
+
const chosen = requestedId
|
|
385
|
+
? runtimes.find((runtime, index) => (cleanText(runtime && (runtime.runtimeId || runtime.id), 255) || `runtime-${index + 1}`) === requestedId) || null
|
|
386
|
+
: fallbackRuntime;
|
|
387
|
+
const runtime = chosen || fallbackRuntime;
|
|
388
|
+
const resolution = resolveAllocation({ ...options, runtime, decision, availableModels: runtime && runtime.availableModels });
|
|
389
|
+
const requestedExact = Boolean(decision && decision.runtimeId && decision.exactModelId);
|
|
390
|
+
const runtimeId = cleanText(runtime && (runtime.runtimeId || runtime.id), 255) || fallbackId;
|
|
391
|
+
if (requestedExact && chosen && resolution.model === decision.exactModelId) {
|
|
392
|
+
resolution.source = "parent-selected-live-runtime-model";
|
|
393
|
+
} else if (requestedExact) {
|
|
394
|
+
resolution.fallbackReason = [resolution.fallbackReason, chosen ? "parent_model_not_in_live_inventory" : "parent_runtime_not_in_live_inventory"].filter(Boolean).join(",");
|
|
395
|
+
if (resolution.source !== "user-pin") resolution.source = "fallback";
|
|
396
|
+
}
|
|
397
|
+
return { ...resolution, runtime, runtimeId, requestedRuntimeId: requestedId || null };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function createDecisionReceipt({ taskId, stage, decision, resolution }) {
|
|
401
|
+
const normalized = normalizeAllocation(decision);
|
|
402
|
+
const validationIssues = [];
|
|
403
|
+
if (!normalized) validationIssues.push("invalid-ai-allocation");
|
|
404
|
+
const resolutionCodes = cleanText(resolution && resolution.fallbackReason, 500)
|
|
405
|
+
.split(",")
|
|
406
|
+
.map((code) => cleanText(code, 120))
|
|
407
|
+
.filter(Boolean);
|
|
408
|
+
const reasonCodes = [...new Set([
|
|
409
|
+
...(normalized ? normalized.reasonCodes : []),
|
|
410
|
+
...resolutionCodes,
|
|
411
|
+
])].slice(0, 32);
|
|
412
|
+
const featurePayload = JSON.stringify(normalized ? {
|
|
413
|
+
phase: normalized.phase,
|
|
414
|
+
tier: normalized.tier,
|
|
415
|
+
effort: normalized.effort,
|
|
416
|
+
reasonCodes: normalized.reasonCodes,
|
|
417
|
+
requiredCapabilities: normalized.requiredCapabilities,
|
|
418
|
+
estimatedContextTokens: normalized.estimatedContextTokens,
|
|
419
|
+
} : { phase: cleanText(stage, 80) || null, allocation: null });
|
|
420
|
+
const featureHash = `sha256:${crypto.createHash("sha256").update(featurePayload, "utf8").digest("hex")}`;
|
|
421
|
+
const source = resolution && resolution.source;
|
|
422
|
+
const status = source === "user-pin"
|
|
423
|
+
? "user-pin"
|
|
424
|
+
: source === "fallback"
|
|
425
|
+
? "fallback-current"
|
|
426
|
+
: normalized && resolution && resolution.ok
|
|
427
|
+
? "resolved"
|
|
428
|
+
: "unresolved";
|
|
429
|
+
const riskCodes = new Set(normalized ? normalized.reasonCodes : []);
|
|
430
|
+
return {
|
|
431
|
+
schemaVersion: "agentlas.model-allocation-receipt.v1",
|
|
432
|
+
decisionId: normalized && normalized.decisionId
|
|
433
|
+
? normalized.decisionId
|
|
434
|
+
: `terminal:model-allocation:${featureHash.slice("sha256:".length, "sha256:".length + 24)}`,
|
|
435
|
+
packetId: cleanText(taskId, 255) || null,
|
|
436
|
+
status,
|
|
437
|
+
requested: {
|
|
438
|
+
tier: normalized ? normalized.tier : null,
|
|
439
|
+
modelClass: normalized ? normalized.modelClass : null,
|
|
440
|
+
sessionId: normalized ? normalized.runtimeId : null,
|
|
441
|
+
modelId: normalized && normalized.exactModelId
|
|
442
|
+
? normalized.exactModelId
|
|
443
|
+
: source === "user-pin" ? cleanText(resolution && resolution.model, 255) || null : null,
|
|
444
|
+
effort: normalized ? normalized.effort : "none",
|
|
445
|
+
},
|
|
446
|
+
resolved: {
|
|
447
|
+
tier: resolution && resolution.tier ? cleanText(resolution.tier, 32) : normalized ? normalized.tier : null,
|
|
448
|
+
provider: cleanText(resolution && resolution.provider, 80) || null,
|
|
449
|
+
modelId: cleanText(resolution && resolution.model, 255) || null,
|
|
450
|
+
sessionId: cleanText(resolution && resolution.runtimeId, 255) || null,
|
|
451
|
+
effort: cleanText(resolution && resolution.effort, 16) || "none",
|
|
452
|
+
},
|
|
453
|
+
reasonCodes,
|
|
454
|
+
inputFeatureHash: normalized && normalized.inputFeatureHash ? normalized.inputFeatureHash : featureHash,
|
|
455
|
+
selectorVersion: normalized ? normalized.selectorVersion : "deterministic-host-fallback",
|
|
456
|
+
independentVerificationRequired:
|
|
457
|
+
riskCodes.has("high-risk") || riskCodes.has("critical-risk") || riskCodes.has("independent-verification"),
|
|
458
|
+
validationIssues,
|
|
459
|
+
privacy: { rawPromptIncluded: false, rawTranscriptIncluded: false },
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function defaultReceiptPath() {
|
|
464
|
+
return path.join(os.homedir(), ".agentlas", "model-routing-receipts.jsonl");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function appendDecisionReceipt(receipt, file = defaultReceiptPath()) {
|
|
468
|
+
const directory = path.dirname(file);
|
|
469
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
470
|
+
try { fs.chmodSync(directory, 0o700); } catch { /* Windows/best effort */ }
|
|
471
|
+
try {
|
|
472
|
+
const stat = fs.lstatSync(file);
|
|
473
|
+
if (stat.isSymbolicLink()) throw new Error("model routing receipt path must not be a symlink");
|
|
474
|
+
} catch (error) {
|
|
475
|
+
if (error && error.code !== "ENOENT") throw error;
|
|
476
|
+
}
|
|
477
|
+
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND | (fs.constants.O_NOFOLLOW || 0);
|
|
478
|
+
const fd = fs.openSync(file, flags, 0o600);
|
|
479
|
+
try {
|
|
480
|
+
fs.writeSync(fd, JSON.stringify(receipt) + "\n", null, "utf8");
|
|
481
|
+
fs.fsyncSync(fd);
|
|
482
|
+
} finally {
|
|
483
|
+
fs.closeSync(fd);
|
|
484
|
+
}
|
|
485
|
+
try { fs.chmodSync(file, 0o600); } catch { /* Windows/best effort */ }
|
|
486
|
+
return file;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function plannerSystemPrompt({ language = "English", maxTasks = 12, mode = "swarm", liveRuntimeInventory = [] } = {}) {
|
|
490
|
+
return [
|
|
491
|
+
`You are the higher-level workload allocator for an Agentlas ${mode}.`,
|
|
492
|
+
"Judge each child task using the full goal and planned dependency graph. Do not use a keyword lookup or fixed role-to-model table.",
|
|
493
|
+
"LIVE_RUNTIME_INVENTORY below is authoritative. For every child and synthesis, choose an exact runtimeId and exactModelId only from it. Do not infer, rename, or invent a model from a tier. If an exact choice cannot be justified, select the current-runtime fallback from the inventory.",
|
|
494
|
+
`LIVE_RUNTIME_INVENTORY=${JSON.stringify(liveRuntimeInventory)}`,
|
|
495
|
+
"Choose effort none|minimal|low|medium|high|xhigh|max. Spend frontier/high effort only when the task's complexity, risk, context, or synthesis burden justifies it.",
|
|
496
|
+
`Return strict JSON only with at most ${Math.max(1, Math.min(24, maxTasks))} tasks:`,
|
|
497
|
+
'{"tasks":[{"title":"short","brief":"concrete child task","role":"optional","allocation":{"schema":"agentlas.workload-allocation.v1","runtimeId":"runtime-1","exactModelId":"model-from-inventory","tier":"economy|balanced|frontier","effort":"none|minimal|low|medium|high|xhigh|max","phase":"delegate","reasonCodes":["bounded-scope|parallel-throughput|complex-reasoning|large-context|high-risk"],"rationale":"short observable rationale","requiredCapabilities":["code|image|tools|long-context"],"estimatedContextTokens":0}}],"synthesis":{"schema":"agentlas.workload-allocation.v1","runtimeId":"runtime-1","exactModelId":"model-from-inventory","tier":"economy|balanced|frontier","effort":"none|minimal|low|medium|high|xhigh|max","phase":"synthesize","reasonCodes":["cross-result-synthesis"],"rationale":"short observable rationale","requiredCapabilities":["code|image|tools|long-context"],"estimatedContextTokens":0}}',
|
|
498
|
+
"Every task and synthesis MUST include an allocation. Keep tasks independent where safe and sequential where dependencies require it.",
|
|
499
|
+
`Write task text and reasons in ${language}.`,
|
|
500
|
+
].filter(Boolean).join("\n");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
module.exports = {
|
|
504
|
+
SCHEMA_VERSION,
|
|
505
|
+
ALLOCATION_SCHEMA,
|
|
506
|
+
TIERS,
|
|
507
|
+
EFFORTS,
|
|
508
|
+
normalizeTier,
|
|
509
|
+
normalizeEffort,
|
|
510
|
+
normalizeAllocation,
|
|
511
|
+
normalizePlan,
|
|
512
|
+
extractJsonObject,
|
|
513
|
+
runtimeProvider,
|
|
514
|
+
defaultAvailableModels,
|
|
515
|
+
runtimeInventory,
|
|
516
|
+
readCodexModelInventory,
|
|
517
|
+
resolveAllocation,
|
|
518
|
+
resolveAllocationAcrossRuntimes,
|
|
519
|
+
createDecisionReceipt,
|
|
520
|
+
appendDecisionReceipt,
|
|
521
|
+
defaultReceiptPath,
|
|
522
|
+
plannerSystemPrompt,
|
|
523
|
+
};
|