@serkanalgur/opencode-nexus 2.4.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +696 -110
- package/dist/tui.js +89 -60
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8451,7 +8451,7 @@ function describeFile(file) {
|
|
|
8451
8451
|
}
|
|
8452
8452
|
function formatConfigLoadLog(info) {
|
|
8453
8453
|
const models = Object.entries(info.models).map(([role, model]) => `${role}=${model}`).join(" ");
|
|
8454
|
-
const override = info.sessionOverride ?
|
|
8454
|
+
const override = info.sessionOverride ? ' (+session override: storage; disk edits to models are IGNORED while a preset is set — call the preset tool with mode "clear", or reset in the TUI, to hand control back to disk)' : "";
|
|
8455
8455
|
return `[nexus] config loaded (#${info.loadCount} trigger=${info.trigger} at=${info.loadedAt}) ` + `project=${info.project.path} [${describeFile(info.project)}] ` + `global=${info.global.path} [${describeFile(info.global)}] ` + `models:${override} ${models}`;
|
|
8456
8456
|
}
|
|
8457
8457
|
var DEFAULT_CONFIG = {
|
|
@@ -8668,7 +8668,9 @@ class NexusConfigManager {
|
|
|
8668
8668
|
return result;
|
|
8669
8669
|
}
|
|
8670
8670
|
resetToDefaults() {
|
|
8671
|
+
const hadOverride = this.storageConfig !== null;
|
|
8671
8672
|
this.storageConfig = null;
|
|
8673
|
+
return hadOverride;
|
|
8672
8674
|
}
|
|
8673
8675
|
applyPreset(name) {
|
|
8674
8676
|
const preset = PRESETS[name];
|
|
@@ -9797,10 +9799,20 @@ class PerformanceTracker {
|
|
|
9797
9799
|
this.maxEntries = maxEntries;
|
|
9798
9800
|
}
|
|
9799
9801
|
record(entry) {
|
|
9800
|
-
|
|
9802
|
+
const id = `perf-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
|
|
9803
|
+
this.entries.push({ ...entry, id, timestamp: new Date });
|
|
9801
9804
|
if (this.entries.length > this.maxEntries) {
|
|
9802
9805
|
this.entries = this.entries.slice(-this.maxEntries);
|
|
9803
9806
|
}
|
|
9807
|
+
return id;
|
|
9808
|
+
}
|
|
9809
|
+
adjust(id, { cost }) {
|
|
9810
|
+
const entry = this.entries.find((e) => e.id === id);
|
|
9811
|
+
if (!entry)
|
|
9812
|
+
return false;
|
|
9813
|
+
if (cost !== undefined)
|
|
9814
|
+
entry.cost += cost;
|
|
9815
|
+
return true;
|
|
9804
9816
|
}
|
|
9805
9817
|
getScores() {
|
|
9806
9818
|
const groups = new Map;
|
|
@@ -9818,11 +9830,28 @@ class PerformanceTracker {
|
|
|
9818
9830
|
const successRate = total > 0 ? successful / total : 0;
|
|
9819
9831
|
const avgDuration = entries.reduce((s, e) => s + e.duration, 0) / total;
|
|
9820
9832
|
const avgCost = entries.reduce((s, e) => s + e.cost, 0) / total;
|
|
9821
|
-
const
|
|
9833
|
+
const measured = entries.filter((e) => e.costProvenance.usage === "measured");
|
|
9834
|
+
const measuredTasks = measured.length;
|
|
9835
|
+
const estimatedTasks = total - measuredTasks;
|
|
9836
|
+
const measuredSuccessRate = measuredTasks > 0 ? measured.filter((e) => e.success).length / measuredTasks : 0;
|
|
9837
|
+
const measuredAvgCost = measuredTasks > 0 ? measured.reduce((s, e) => s + e.cost, 0) / measuredTasks : 0;
|
|
9838
|
+
const costEfficiency = measuredAvgCost > 0 ? measuredSuccessRate / measuredAvgCost : 0;
|
|
9822
9839
|
const speedScore = Math.max(0, 1 - avgDuration / 30000);
|
|
9823
9840
|
const costScore = Math.min(1, costEfficiency * 100);
|
|
9824
9841
|
const overallScore = successRate * 40 + speedScore * 30 + costScore * 30;
|
|
9825
|
-
scores.push({
|
|
9842
|
+
scores.push({
|
|
9843
|
+
model,
|
|
9844
|
+
role,
|
|
9845
|
+
totalTasks: total,
|
|
9846
|
+
successRate,
|
|
9847
|
+
avgDuration,
|
|
9848
|
+
avgCost,
|
|
9849
|
+
costEfficiency,
|
|
9850
|
+
overallScore,
|
|
9851
|
+
measuredTasks,
|
|
9852
|
+
estimatedTasks,
|
|
9853
|
+
costBasis: measuredTasks > 0 ? "measured" : "none"
|
|
9854
|
+
});
|
|
9826
9855
|
}
|
|
9827
9856
|
return scores.sort((a, b) => b.overallScore - a.overallScore);
|
|
9828
9857
|
}
|
|
@@ -9858,6 +9887,16 @@ class ExecutionHistory {
|
|
|
9858
9887
|
}
|
|
9859
9888
|
return full;
|
|
9860
9889
|
}
|
|
9890
|
+
adjust(id, { cost, tokensUsed }) {
|
|
9891
|
+
const record = this.records.find((r) => r.id === id);
|
|
9892
|
+
if (!record)
|
|
9893
|
+
return false;
|
|
9894
|
+
if (cost !== undefined)
|
|
9895
|
+
record.cost += cost;
|
|
9896
|
+
if (tokensUsed !== undefined)
|
|
9897
|
+
record.tokensUsed += tokensUsed;
|
|
9898
|
+
return true;
|
|
9899
|
+
}
|
|
9861
9900
|
getAll() {
|
|
9862
9901
|
return [...this.records];
|
|
9863
9902
|
}
|
|
@@ -9879,7 +9918,17 @@ class ExecutionHistory {
|
|
|
9879
9918
|
for (const r of this.records) {
|
|
9880
9919
|
byRole[r.role] = (byRole[r.role] || 0) + 1;
|
|
9881
9920
|
}
|
|
9882
|
-
|
|
9921
|
+
const costSplit = this.records.reduce((split, r) => {
|
|
9922
|
+
if (r.costProvenance.usage === "measured") {
|
|
9923
|
+
split.measuredCost += r.cost;
|
|
9924
|
+
split.measuredEntries++;
|
|
9925
|
+
} else {
|
|
9926
|
+
split.estimatedCost += r.cost;
|
|
9927
|
+
split.estimatedEntries++;
|
|
9928
|
+
}
|
|
9929
|
+
return split;
|
|
9930
|
+
}, { measuredCost: 0, estimatedCost: 0, measuredEntries: 0, estimatedEntries: 0 });
|
|
9931
|
+
return { total, successRate: total > 0 ? successful / total : 0, totalCost, costSplit, avgDuration, byRole };
|
|
9883
9932
|
}
|
|
9884
9933
|
clear() {
|
|
9885
9934
|
this.records = [];
|
|
@@ -9920,33 +9969,122 @@ class CustomRoleManager {
|
|
|
9920
9969
|
}
|
|
9921
9970
|
|
|
9922
9971
|
// src/forecast.ts
|
|
9972
|
+
var CACHE_READ_MULTIPLE = 0.1;
|
|
9973
|
+
var CACHE_WRITE_MULTIPLE = 1.25;
|
|
9974
|
+
function per1kPricing(input, output) {
|
|
9975
|
+
return {
|
|
9976
|
+
input,
|
|
9977
|
+
output,
|
|
9978
|
+
cacheRead: input * CACHE_READ_MULTIPLE,
|
|
9979
|
+
cacheWrite: input * CACHE_WRITE_MULTIPLE
|
|
9980
|
+
};
|
|
9981
|
+
}
|
|
9982
|
+
function untiered(rates) {
|
|
9983
|
+
return { tiers: [{ rates }] };
|
|
9984
|
+
}
|
|
9985
|
+
var FALLBACK_PRICING_PER_1K = {
|
|
9986
|
+
"claude-sonnet-4-6": untiered(per1kPricing(0.015, 0.075)),
|
|
9987
|
+
"claude-opus-4-7": untiered(per1kPricing(0.075, 0.375)),
|
|
9988
|
+
"claude-haiku-4-5": untiered(per1kPricing(0.001, 0.005)),
|
|
9989
|
+
"gpt-5-mini": untiered(per1kPricing(0.00015, 0.0006)),
|
|
9990
|
+
"gpt-5": untiered(per1kPricing(0.0025, 0.01)),
|
|
9991
|
+
"gemini-2.5-flash": untiered(per1kPricing(0.000075, 0.0003)),
|
|
9992
|
+
"minimax-m2.5-free": untiered(per1kPricing(0, 0))
|
|
9993
|
+
};
|
|
9994
|
+
var UNKNOWN_PRICING_PER_1K = untiered(per1kPricing(0.01, 0.05));
|
|
9995
|
+
var per1k = (rate, tokens) => rate * tokens / 1000;
|
|
9996
|
+
function bareModelId(ref) {
|
|
9997
|
+
return ref.slice(ref.indexOf("/") + 1);
|
|
9998
|
+
}
|
|
9999
|
+
function totalTokens(usage) {
|
|
10000
|
+
return usage.input + usage.output + usage.reasoning + usage.cache.read + usage.cache.write;
|
|
10001
|
+
}
|
|
10002
|
+
function promptSizeOf(usage) {
|
|
10003
|
+
return usage.input + usage.cache.read + usage.cache.write;
|
|
10004
|
+
}
|
|
10005
|
+
var ZERO_RATES = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
10006
|
+
function selectTier(tiers, promptSize) {
|
|
10007
|
+
let best;
|
|
10008
|
+
let bestThreshold = Number.NEGATIVE_INFINITY;
|
|
10009
|
+
for (const tier of tiers) {
|
|
10010
|
+
const { threshold } = tier;
|
|
10011
|
+
if (threshold === undefined)
|
|
10012
|
+
continue;
|
|
10013
|
+
if (!(promptSize > threshold))
|
|
10014
|
+
continue;
|
|
10015
|
+
if (threshold > bestThreshold) {
|
|
10016
|
+
best = tier;
|
|
10017
|
+
bestThreshold = threshold;
|
|
10018
|
+
}
|
|
10019
|
+
}
|
|
10020
|
+
if (best)
|
|
10021
|
+
return best;
|
|
10022
|
+
const base = tiers.find((t) => t.threshold === undefined);
|
|
10023
|
+
return base ?? { rates: ZERO_RATES };
|
|
10024
|
+
}
|
|
10025
|
+
function priceUsage(usage, pricing) {
|
|
10026
|
+
return priceTokens(usage, selectTier(pricing.tiers, promptSizeOf(usage)).rates);
|
|
10027
|
+
}
|
|
10028
|
+
function priceUsageAtSettledTier(increment, pricing, settledUsage) {
|
|
10029
|
+
return priceTokens(increment, selectTier(pricing.tiers, promptSizeOf(settledUsage)).rates);
|
|
10030
|
+
}
|
|
10031
|
+
function priceTokens(usage, pricing) {
|
|
10032
|
+
const inputCost = per1k(pricing.input, usage.input);
|
|
10033
|
+
const outputCost = per1k(pricing.output, usage.output + usage.reasoning);
|
|
10034
|
+
const cacheCost = per1k(pricing.cacheRead, usage.cache.read) + per1k(pricing.cacheWrite, usage.cache.write);
|
|
10035
|
+
return { inputCost, outputCost, cacheCost, total: inputCost + outputCost + cacheCost };
|
|
10036
|
+
}
|
|
10037
|
+
|
|
9923
10038
|
class CostForecaster {
|
|
9924
|
-
|
|
9925
|
-
constructor() {
|
|
9926
|
-
this.
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
10039
|
+
resolvePricing;
|
|
10040
|
+
constructor(pricing) {
|
|
10041
|
+
this.resolvePricing = pricing;
|
|
10042
|
+
}
|
|
10043
|
+
priceFor(model, provider, promptSize) {
|
|
10044
|
+
const { pricing, source } = this.tiersFor(model, provider);
|
|
10045
|
+
return { pricing: selectTier(pricing.tiers, promptSize ?? 0).rates, source };
|
|
10046
|
+
}
|
|
10047
|
+
tiersFor(model, provider) {
|
|
10048
|
+
const real = this.resolvePricing?.(model, provider);
|
|
10049
|
+
if (real)
|
|
10050
|
+
return { pricing: real, source: "model-costs" };
|
|
10051
|
+
const bare = bareModelId(model);
|
|
10052
|
+
const fallback = FALLBACK_PRICING_PER_1K[model] ?? FALLBACK_PRICING_PER_1K[bare];
|
|
10053
|
+
if (fallback)
|
|
10054
|
+
return { pricing: fallback, source: "fallback-table" };
|
|
10055
|
+
return { pricing: UNKNOWN_PRICING_PER_1K, source: "unknown-model" };
|
|
10056
|
+
}
|
|
10057
|
+
measureCost(usage, model, provider) {
|
|
10058
|
+
const { pricing, source } = this.tiersFor(model, provider);
|
|
10059
|
+
return {
|
|
10060
|
+
cost: priceUsage(usage, pricing).total,
|
|
10061
|
+
tokens: totalTokens(usage),
|
|
10062
|
+
pricingSource: source,
|
|
10063
|
+
...source === "model-costs" ? { confidence: 1 } : {}
|
|
10064
|
+
};
|
|
9932
10065
|
}
|
|
9933
|
-
|
|
9934
|
-
this.
|
|
10066
|
+
costOf(usage, model, provider) {
|
|
10067
|
+
return priceUsage(usage, this.tiersFor(model, provider).pricing).total;
|
|
9935
10068
|
}
|
|
9936
|
-
|
|
10069
|
+
estimateTokensFor(complexity, fileCount) {
|
|
9937
10070
|
const baseInput = 700;
|
|
9938
10071
|
const complexityMultiplier = 1 + complexity.overall / 100 * 3;
|
|
9939
|
-
const fileMultiplier = 1 +
|
|
10072
|
+
const fileMultiplier = 1 + fileCount * 0.2;
|
|
9940
10073
|
const input = Math.round(baseInput * complexityMultiplier * fileMultiplier);
|
|
9941
10074
|
const output = Math.round(input * 0.5);
|
|
9942
10075
|
return { input, output };
|
|
9943
10076
|
}
|
|
10077
|
+
estimateTokens(task, complexity) {
|
|
10078
|
+
return this.estimateTokensFor(complexity, task.files.include.length);
|
|
10079
|
+
}
|
|
10080
|
+
estimateCost(complexity, model, provider) {
|
|
10081
|
+
const { input, output } = this.estimateTokensFor(complexity, 0);
|
|
10082
|
+
return this.costOf({ input, output, reasoning: 0, cache: { read: 0, write: 0 } }, model, provider);
|
|
10083
|
+
}
|
|
9944
10084
|
forecastTask(task, role, modelId, complexity) {
|
|
9945
10085
|
const { input, output } = this.estimateTokens(task, complexity);
|
|
9946
|
-
const pricing = this.
|
|
9947
|
-
const
|
|
9948
|
-
const outputCost = output * pricing.output;
|
|
9949
|
-
const overhead = 0.0001;
|
|
10086
|
+
const { pricing, source } = this.tiersFor(modelId);
|
|
10087
|
+
const breakdown = priceUsage({ input, output, reasoning: 0, cache: { read: 0, write: 0 } }, pricing);
|
|
9950
10088
|
return {
|
|
9951
10089
|
taskId: task.id,
|
|
9952
10090
|
taskName: task.name,
|
|
@@ -9954,9 +10092,15 @@ class CostForecaster {
|
|
|
9954
10092
|
model: modelId,
|
|
9955
10093
|
estimatedInputTokens: input,
|
|
9956
10094
|
estimatedOutputTokens: output,
|
|
9957
|
-
estimatedCost:
|
|
9958
|
-
|
|
9959
|
-
|
|
10095
|
+
estimatedCost: breakdown.total,
|
|
10096
|
+
source: "estimated",
|
|
10097
|
+
pricingSource: source,
|
|
10098
|
+
confidence: undefined,
|
|
10099
|
+
breakdown: {
|
|
10100
|
+
inputCost: breakdown.inputCost,
|
|
10101
|
+
outputCost: breakdown.outputCost,
|
|
10102
|
+
cacheCost: breakdown.cacheCost
|
|
10103
|
+
}
|
|
9960
10104
|
};
|
|
9961
10105
|
}
|
|
9962
10106
|
forecastAll(tasks, budgetRemaining) {
|
|
@@ -10149,6 +10293,13 @@ ${lines.join(`
|
|
|
10149
10293
|
}
|
|
10150
10294
|
|
|
10151
10295
|
// src/orchestrator.ts
|
|
10296
|
+
class TaskTimeoutError extends Error {
|
|
10297
|
+
constructor(message = "Task timed out") {
|
|
10298
|
+
super(message);
|
|
10299
|
+
this.name = "TaskTimeoutError";
|
|
10300
|
+
}
|
|
10301
|
+
}
|
|
10302
|
+
var DELTA_READ_BACKOFF_MS = [1000, 2000, 4000];
|
|
10152
10303
|
function assistantMessageText(message) {
|
|
10153
10304
|
if (message.type !== "assistant")
|
|
10154
10305
|
return "";
|
|
@@ -10164,6 +10315,15 @@ function lastAssistantText(messages) {
|
|
|
10164
10315
|
}
|
|
10165
10316
|
return "";
|
|
10166
10317
|
}
|
|
10318
|
+
function sumBy(records, select) {
|
|
10319
|
+
let total = 0;
|
|
10320
|
+
for (const record of records.values())
|
|
10321
|
+
total += select(record);
|
|
10322
|
+
return total;
|
|
10323
|
+
}
|
|
10324
|
+
function baseInputRate(cost) {
|
|
10325
|
+
return selectTier(cost.tiers, 0).rates.input;
|
|
10326
|
+
}
|
|
10167
10327
|
var DEFAULT_ESCALATION = {
|
|
10168
10328
|
maxRetries: 3,
|
|
10169
10329
|
retryDelay: 1000,
|
|
@@ -10209,7 +10369,14 @@ class NexusOrchestrator {
|
|
|
10209
10369
|
onStateChange = null;
|
|
10210
10370
|
stateChangeTimer = null;
|
|
10211
10371
|
costHistory = [];
|
|
10372
|
+
tokensByModel = new Map;
|
|
10373
|
+
costProvenance = new Map;
|
|
10212
10374
|
cleanupInterval = null;
|
|
10375
|
+
deltaLedgers = new Map;
|
|
10376
|
+
uncollected = new Map;
|
|
10377
|
+
deltaTimers = new Set;
|
|
10378
|
+
shuttingDown = false;
|
|
10379
|
+
deltaReadBackoffMs = DELTA_READ_BACKOFF_MS;
|
|
10213
10380
|
get healthMonitor() {
|
|
10214
10381
|
return this._healthMonitor;
|
|
10215
10382
|
}
|
|
@@ -10226,6 +10393,7 @@ class NexusOrchestrator {
|
|
|
10226
10393
|
this.messageRouter = new MessageRouter;
|
|
10227
10394
|
this.escalationPolicy = {
|
|
10228
10395
|
...DEFAULT_ESCALATION,
|
|
10396
|
+
fallbackModels: [...DEFAULT_ESCALATION.fallbackModels],
|
|
10229
10397
|
maxRetries: this.config.selfHealing.maxRetries,
|
|
10230
10398
|
retryDelay: this.config.selfHealing.retryDelay,
|
|
10231
10399
|
enableRespawn: this.config.selfHealing.contextTransfer
|
|
@@ -10235,7 +10403,7 @@ class NexusOrchestrator {
|
|
|
10235
10403
|
this.performanceTracker = new PerformanceTracker;
|
|
10236
10404
|
this.executionHistory = new ExecutionHistory;
|
|
10237
10405
|
this.customRoles = new CustomRoleManager;
|
|
10238
|
-
this.forecaster = new CostForecaster;
|
|
10406
|
+
this.forecaster = new CostForecaster((model, provider) => this.getModelCost(model, provider));
|
|
10239
10407
|
}
|
|
10240
10408
|
async initialize(ctx, onStateChange) {
|
|
10241
10409
|
this.ctx = ctx;
|
|
@@ -10269,23 +10437,40 @@ class NexusOrchestrator {
|
|
|
10269
10437
|
for (const model of data) {
|
|
10270
10438
|
if (!model?.cost || !Array.isArray(model.cost) || model.cost.length === 0)
|
|
10271
10439
|
continue;
|
|
10272
|
-
const baseCost = model.cost[0];
|
|
10273
10440
|
this.modelCosts.set(`${model.providerID}/${model.id}`, {
|
|
10274
|
-
|
|
10275
|
-
output: per1k(baseCost.output),
|
|
10276
|
-
cacheRead: per1k(baseCost.cache?.read),
|
|
10277
|
-
cacheWrite: per1k(baseCost.cache?.write)
|
|
10441
|
+
tiers: this.normaliseTiers(model.cost, per1k)
|
|
10278
10442
|
});
|
|
10279
10443
|
}
|
|
10280
10444
|
} catch {}
|
|
10281
10445
|
}
|
|
10446
|
+
normaliseTiers(cost, per1k) {
|
|
10447
|
+
const rates = (row) => ({
|
|
10448
|
+
input: per1k(row.input),
|
|
10449
|
+
output: per1k(row.output),
|
|
10450
|
+
cacheRead: per1k(row.cache?.read),
|
|
10451
|
+
cacheWrite: per1k(row.cache?.write)
|
|
10452
|
+
});
|
|
10453
|
+
const kept = cost.filter((row) => row?.tier === undefined || row.tier?.type === "context").map((row) => ({
|
|
10454
|
+
...row.tier?.type === "context" && typeof row.tier.size === "number" ? { threshold: row.tier.size } : {},
|
|
10455
|
+
rates: rates(row)
|
|
10456
|
+
}));
|
|
10457
|
+
if (kept.length === 0)
|
|
10458
|
+
return [{ rates: rates(cost[0]) }];
|
|
10459
|
+
const base = kept.find((t) => t.threshold === undefined);
|
|
10460
|
+
const contextual = kept.filter((t) => t.threshold !== undefined).sort((a, b) => a.threshold - b.threshold);
|
|
10461
|
+
return base ? [{ rates: { ...base.rates } }, ...contextual] : [{ rates: { ...kept[0].rates } }, ...contextual];
|
|
10462
|
+
}
|
|
10282
10463
|
setModelCosts(costs) {
|
|
10283
10464
|
for (const [model, cost] of Object.entries(costs)) {
|
|
10284
10465
|
this.modelCosts.set(model, {
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
|
|
10288
|
-
|
|
10466
|
+
tiers: [{
|
|
10467
|
+
rates: {
|
|
10468
|
+
input: cost.input,
|
|
10469
|
+
output: cost.output,
|
|
10470
|
+
cacheRead: cost.cacheRead || 0,
|
|
10471
|
+
cacheWrite: cost.cacheWrite || 0
|
|
10472
|
+
}
|
|
10473
|
+
}]
|
|
10289
10474
|
});
|
|
10290
10475
|
}
|
|
10291
10476
|
}
|
|
@@ -10403,10 +10588,14 @@ class NexusOrchestrator {
|
|
|
10403
10588
|
enabled: true,
|
|
10404
10589
|
patternStorage: "memory",
|
|
10405
10590
|
minConfidence: 0.7
|
|
10591
|
+
},
|
|
10592
|
+
cost: {
|
|
10593
|
+
timeoutDeltaGraceMs: 60000
|
|
10406
10594
|
}
|
|
10407
10595
|
};
|
|
10596
|
+
const { cost: defaultCost, ...restDefaults } = defaults;
|
|
10408
10597
|
return {
|
|
10409
|
-
...
|
|
10598
|
+
...restDefaults,
|
|
10410
10599
|
...partial,
|
|
10411
10600
|
budget: { ...defaults.budget, ...partial?.budget },
|
|
10412
10601
|
agents: { ...defaults.agents, ...partial?.agents },
|
|
@@ -10415,7 +10604,8 @@ class NexusOrchestrator {
|
|
|
10415
10604
|
memory: { ...defaults.memory, ...partial?.memory },
|
|
10416
10605
|
dashboard: { ...defaults.dashboard, ...partial?.dashboard },
|
|
10417
10606
|
security: { ...defaults.security, ...partial?.security },
|
|
10418
|
-
learning: { ...defaults.learning, ...partial?.learning }
|
|
10607
|
+
learning: { ...defaults.learning, ...partial?.learning },
|
|
10608
|
+
...partial?.cost ? { cost: { ...defaultCost, ...partial.cost } } : {}
|
|
10419
10609
|
};
|
|
10420
10610
|
}
|
|
10421
10611
|
notifyStateChange() {
|
|
@@ -10469,6 +10659,7 @@ class NexusOrchestrator {
|
|
|
10469
10659
|
success: true,
|
|
10470
10660
|
tasks: results,
|
|
10471
10661
|
totalCost: this.totalSpent,
|
|
10662
|
+
...this.spendSplit(),
|
|
10472
10663
|
totalDuration,
|
|
10473
10664
|
agentsUsed: this.agents.size
|
|
10474
10665
|
};
|
|
@@ -10478,6 +10669,7 @@ class NexusOrchestrator {
|
|
|
10478
10669
|
success: false,
|
|
10479
10670
|
tasks: [],
|
|
10480
10671
|
totalCost: this.totalSpent,
|
|
10672
|
+
...this.spendSplit(),
|
|
10481
10673
|
totalDuration: Date.now() - startTime,
|
|
10482
10674
|
agentsUsed: this.agents.size
|
|
10483
10675
|
};
|
|
@@ -10590,14 +10782,17 @@ class NexusOrchestrator {
|
|
|
10590
10782
|
await this.sleep(this.config.schedulerInterval);
|
|
10591
10783
|
}
|
|
10592
10784
|
}
|
|
10593
|
-
|
|
10785
|
+
selectQualifiedModel(node) {
|
|
10594
10786
|
const complexity = this.analyzeComplexity(node.task);
|
|
10595
10787
|
const model = this.selectModel(node.task.requiredRole, complexity);
|
|
10596
10788
|
if (!model.provider || !model.model) {
|
|
10597
10789
|
const missing = !model.provider && !model.model ? "provider and model" : !model.provider ? "provider" : "model";
|
|
10598
10790
|
throw new Error(`Model selection for role "${node.task.requiredRole}" is missing its ${missing}: ` + `got { provider: ${JSON.stringify(model.provider)}, model: ${JSON.stringify(model.model)} }. ` + `Cannot build a "providerID/modelID" reference.`);
|
|
10599
10791
|
}
|
|
10600
|
-
|
|
10792
|
+
return `${model.provider}/${model.model}`;
|
|
10793
|
+
}
|
|
10794
|
+
async spawnAndExecute(node, options = {}) {
|
|
10795
|
+
const qualifiedModel = options.modelOverride ?? this.selectQualifiedModel(node);
|
|
10601
10796
|
const agent = await this.spawnAgent({
|
|
10602
10797
|
role: node.task.requiredRole,
|
|
10603
10798
|
task: node.task,
|
|
@@ -10607,7 +10802,7 @@ class NexusOrchestrator {
|
|
|
10607
10802
|
node.status = "running";
|
|
10608
10803
|
node.task.assignedAgent = agent.id;
|
|
10609
10804
|
this.notifyStateChange();
|
|
10610
|
-
await this.executeTask(agent, node, transferContext);
|
|
10805
|
+
await this.executeTask(agent, node, options.transferContext);
|
|
10611
10806
|
}
|
|
10612
10807
|
async executeTask(agent, node, transferContext) {
|
|
10613
10808
|
if (!this.ctx || !agent.sessionID) {
|
|
@@ -10624,6 +10819,9 @@ class NexusOrchestrator {
|
|
|
10624
10819
|
}
|
|
10625
10820
|
const startTime = Date.now();
|
|
10626
10821
|
const timeout = node.task.timeout || this.config.defaultTimeout;
|
|
10822
|
+
let timeoutTimer = null;
|
|
10823
|
+
let waitGuard = null;
|
|
10824
|
+
let waitAbort = null;
|
|
10627
10825
|
try {
|
|
10628
10826
|
const rolePrompt = this.buildRolePrompt(node.task.requiredRole);
|
|
10629
10827
|
let taskPrompt = `${rolePrompt}
|
|
@@ -10655,32 +10853,38 @@ Please continue from where the previous agent left off.`;
|
|
|
10655
10853
|
sessionID: agent.sessionID,
|
|
10656
10854
|
text: taskPrompt
|
|
10657
10855
|
});
|
|
10658
|
-
|
|
10659
|
-
const
|
|
10856
|
+
waitAbort = new AbortController;
|
|
10857
|
+
const waitPromise = this.ctx.session.wait({ sessionID: agent.sessionID }, { signal: waitAbort.signal });
|
|
10858
|
+
waitGuard = waitPromise.then(() => ({ outcome: "idle" }), () => ({ outcome: "poll-failed" }));
|
|
10859
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
10860
|
+
timeoutTimer = setTimeout(() => reject(new TaskTimeoutError), timeout);
|
|
10861
|
+
});
|
|
10660
10862
|
await Promise.race([waitPromise, timeoutPromise]);
|
|
10661
10863
|
const messages = await this.ctx.session.context({ sessionID: agent.sessionID });
|
|
10662
10864
|
const output = lastAssistantText(messages) || "Task completed (no output captured)";
|
|
10663
10865
|
const duration = Date.now() - startTime;
|
|
10866
|
+
const cost = await this.safeAccountTaskCost(agent, node.task);
|
|
10664
10867
|
const result = {
|
|
10665
10868
|
success: true,
|
|
10666
10869
|
output,
|
|
10667
10870
|
duration,
|
|
10668
|
-
tokensUsed:
|
|
10669
|
-
cost:
|
|
10871
|
+
tokensUsed: cost.tokensUsed,
|
|
10872
|
+
cost: cost.cost,
|
|
10873
|
+
costProvenance: cost.provenance
|
|
10670
10874
|
};
|
|
10671
10875
|
this.dag.markComplete(node.id, result);
|
|
10672
10876
|
this.todoEnforcer.completeTask(agent.id);
|
|
10673
10877
|
agent.metrics.tasksCompleted++;
|
|
10674
10878
|
agent.metrics.totalCost += result.cost;
|
|
10675
|
-
|
|
10676
|
-
this.
|
|
10677
|
-
this.checkBudget();
|
|
10879
|
+
agent.metrics.totalTokens += result.tokensUsed;
|
|
10880
|
+
this.trackCost(agent.id, `${agent.model.provider}/${agent.model.model}`, result.cost, result.tokensUsed, result.costProvenance);
|
|
10678
10881
|
this.performanceTracker.record({
|
|
10679
10882
|
model: agent.model.model,
|
|
10680
10883
|
role: node.task.requiredRole,
|
|
10681
10884
|
success: result.success,
|
|
10682
10885
|
duration: result.duration,
|
|
10683
10886
|
cost: result.cost,
|
|
10887
|
+
costProvenance: result.costProvenance,
|
|
10684
10888
|
tokensUsed: result.tokensUsed
|
|
10685
10889
|
});
|
|
10686
10890
|
this.executionHistory.record({
|
|
@@ -10690,6 +10894,7 @@ Please continue from where the previous agent left off.`;
|
|
|
10690
10894
|
model: agent.model.model,
|
|
10691
10895
|
status: "success",
|
|
10692
10896
|
cost: result.cost,
|
|
10897
|
+
costProvenance: result.costProvenance,
|
|
10693
10898
|
duration: result.duration,
|
|
10694
10899
|
tokensUsed: result.tokensUsed,
|
|
10695
10900
|
startedAt: new Date(startTime),
|
|
@@ -10716,14 +10921,20 @@ Please continue from where the previous agent left off.`;
|
|
|
10716
10921
|
} catch (error) {
|
|
10717
10922
|
const duration = Date.now() - startTime;
|
|
10718
10923
|
const errorMessage = error.message || "Task failed";
|
|
10924
|
+
const cost = await this.safeAccountTaskCost(agent, node.task);
|
|
10719
10925
|
const result = {
|
|
10720
10926
|
success: false,
|
|
10721
10927
|
error: errorMessage,
|
|
10722
10928
|
duration,
|
|
10723
|
-
tokensUsed:
|
|
10724
|
-
cost:
|
|
10929
|
+
tokensUsed: cost.tokensUsed,
|
|
10930
|
+
cost: cost.cost,
|
|
10931
|
+
costProvenance: cost.provenance
|
|
10725
10932
|
};
|
|
10726
10933
|
this.dag.markFailed(node.id, new Error(errorMessage));
|
|
10934
|
+
node.result = result;
|
|
10935
|
+
this.trackCost(agent.id, `${agent.model.provider}/${agent.model.model}`, result.cost, result.tokensUsed, result.costProvenance);
|
|
10936
|
+
agent.metrics.totalCost += result.cost;
|
|
10937
|
+
agent.metrics.totalTokens += result.tokensUsed;
|
|
10727
10938
|
this.todoEnforcer.completeTask(agent.id);
|
|
10728
10939
|
agent.metrics.tasksFailed++;
|
|
10729
10940
|
agent.status = "failed";
|
|
@@ -10737,27 +10948,41 @@ Please continue from where the previous agent left off.`;
|
|
|
10737
10948
|
duration,
|
|
10738
10949
|
sessionID: agent.sessionID
|
|
10739
10950
|
});
|
|
10740
|
-
this.performanceTracker.record({
|
|
10951
|
+
const performanceId = this.performanceTracker.record({
|
|
10741
10952
|
model: agent.model.model,
|
|
10742
10953
|
role: node.task.requiredRole,
|
|
10743
10954
|
success: result.success,
|
|
10744
10955
|
duration: result.duration,
|
|
10745
10956
|
cost: result.cost,
|
|
10957
|
+
costProvenance: result.costProvenance,
|
|
10746
10958
|
tokensUsed: result.tokensUsed
|
|
10747
10959
|
});
|
|
10748
|
-
this.executionHistory.record({
|
|
10960
|
+
const historyId = this.executionHistory.record({
|
|
10749
10961
|
taskId: node.id,
|
|
10750
10962
|
taskName: node.task.name,
|
|
10751
10963
|
role: node.task.requiredRole,
|
|
10752
10964
|
model: agent.model.model,
|
|
10753
10965
|
status: "failed",
|
|
10754
10966
|
cost: result.cost,
|
|
10967
|
+
costProvenance: result.costProvenance,
|
|
10755
10968
|
duration: result.duration,
|
|
10756
10969
|
tokensUsed: result.tokensUsed,
|
|
10757
10970
|
startedAt: new Date(startTime),
|
|
10758
10971
|
completedAt: new Date,
|
|
10759
10972
|
error: errorMessage
|
|
10760
|
-
});
|
|
10973
|
+
}).id;
|
|
10974
|
+
if (error instanceof TaskTimeoutError && cost.usage && waitGuard && agent.sessionID) {
|
|
10975
|
+
this.armTimeoutDelta(agent, node, {
|
|
10976
|
+
usage: cost.usage,
|
|
10977
|
+
idleAt: cost.idleAt,
|
|
10978
|
+
result,
|
|
10979
|
+
waitGuard,
|
|
10980
|
+
abort: waitAbort,
|
|
10981
|
+
historyId,
|
|
10982
|
+
performanceId,
|
|
10983
|
+
timeout
|
|
10984
|
+
});
|
|
10985
|
+
}
|
|
10761
10986
|
if (this.notifications?.isEnabled()) {
|
|
10762
10987
|
this.notifications.notify({ title: "Nexus: Task Failed", body: `${node.task.name} failed: ${errorMessage}`, sound: true });
|
|
10763
10988
|
}
|
|
@@ -10765,6 +10990,10 @@ Please continue from where the previous agent left off.`;
|
|
|
10765
10990
|
await this.handleFailure(agent, node, new Error(errorMessage));
|
|
10766
10991
|
}
|
|
10767
10992
|
} finally {
|
|
10993
|
+
if (timeoutTimer) {
|
|
10994
|
+
clearTimeout(timeoutTimer);
|
|
10995
|
+
timeoutTimer = null;
|
|
10996
|
+
}
|
|
10768
10997
|
if (agent.status !== "terminated" && agent.status !== "failed") {
|
|
10769
10998
|
agent.status = "idle";
|
|
10770
10999
|
}
|
|
@@ -10772,6 +11001,200 @@ Please continue from where the previous agent left off.`;
|
|
|
10772
11001
|
this.notifyStateChange();
|
|
10773
11002
|
}
|
|
10774
11003
|
}
|
|
11004
|
+
armTimeoutDelta(agent, node, args) {
|
|
11005
|
+
const sessionID = agent.sessionID;
|
|
11006
|
+
if (!sessionID)
|
|
11007
|
+
return;
|
|
11008
|
+
const model = `${agent.model.provider}/${agent.model.model}`;
|
|
11009
|
+
if (this.shuttingDown)
|
|
11010
|
+
return;
|
|
11011
|
+
if (this.deltaLedgers.has(sessionID))
|
|
11012
|
+
return;
|
|
11013
|
+
const ledger = {
|
|
11014
|
+
sessionID,
|
|
11015
|
+
taskId: node.id,
|
|
11016
|
+
agentId: agent.id,
|
|
11017
|
+
agent,
|
|
11018
|
+
node,
|
|
11019
|
+
result: args.result,
|
|
11020
|
+
model,
|
|
11021
|
+
provider: agent.model.provider,
|
|
11022
|
+
charged: args.usage,
|
|
11023
|
+
idleAtTimeout: args.idleAt,
|
|
11024
|
+
pending: false,
|
|
11025
|
+
historyId: args.historyId,
|
|
11026
|
+
performanceId: args.performanceId,
|
|
11027
|
+
abort: args.abort ?? new AbortController
|
|
11028
|
+
};
|
|
11029
|
+
this.deltaLedgers.set(sessionID, ledger);
|
|
11030
|
+
const graceMs = this.config.cost?.timeoutDeltaGraceMs ?? Math.min(180000, Math.max(30000, args.timeout / 2));
|
|
11031
|
+
const timer = setTimeout(() => {
|
|
11032
|
+
this.deltaTimers.delete(timer);
|
|
11033
|
+
this.settleTimeoutDelta(ledger, "deadline").catch(() => {});
|
|
11034
|
+
}, graceMs);
|
|
11035
|
+
timer.unref?.();
|
|
11036
|
+
this.deltaTimers.add(timer);
|
|
11037
|
+
(async () => {
|
|
11038
|
+
try {
|
|
11039
|
+
const { outcome } = await args.waitGuard;
|
|
11040
|
+
if (outcome === "idle")
|
|
11041
|
+
await this.settleTimeoutDelta(ledger, "session-idle");
|
|
11042
|
+
else
|
|
11043
|
+
await this.settleTimeoutDelta(ledger, "poll-failed");
|
|
11044
|
+
} catch {}
|
|
11045
|
+
})();
|
|
11046
|
+
}
|
|
11047
|
+
async settleTimeoutDelta(ledger, trigger) {
|
|
11048
|
+
if (ledger.pending)
|
|
11049
|
+
return;
|
|
11050
|
+
if (this.deltaLedgers.get(ledger.sessionID) !== ledger)
|
|
11051
|
+
return;
|
|
11052
|
+
ledger.pending = true;
|
|
11053
|
+
try {
|
|
11054
|
+
let reason = trigger === "shutdown" ? "shutdown" : trigger === "session-idle" ? "session-idle" : "abandoned";
|
|
11055
|
+
let now;
|
|
11056
|
+
if (trigger === "deadline" || trigger === "poll-failed") {
|
|
11057
|
+
const probe = await this.readSessionTokens(ledger.sessionID);
|
|
11058
|
+
if (!probe.read || probe.idleAt <= ledger.idleAtTimeout) {
|
|
11059
|
+
this.abandonTimeoutDelta(ledger, probe.read ? probe.usage : undefined, reason);
|
|
11060
|
+
return;
|
|
11061
|
+
}
|
|
11062
|
+
now = probe.usage;
|
|
11063
|
+
reason = "session-idle";
|
|
11064
|
+
} else {
|
|
11065
|
+
const read = trigger === "shutdown" ? await this.readSessionTokens(ledger.sessionID) : await this.readSessionTokensWithRetry(ledger.sessionID);
|
|
11066
|
+
if (!read.read) {
|
|
11067
|
+
this.abandonTimeoutDelta(ledger, undefined, reason);
|
|
11068
|
+
return;
|
|
11069
|
+
}
|
|
11070
|
+
now = read.usage;
|
|
11071
|
+
}
|
|
11072
|
+
const delta = {
|
|
11073
|
+
input: Math.max(0, now.input - ledger.charged.input),
|
|
11074
|
+
output: Math.max(0, now.output - ledger.charged.output),
|
|
11075
|
+
reasoning: Math.max(0, now.reasoning - ledger.charged.reasoning),
|
|
11076
|
+
cache: {
|
|
11077
|
+
read: Math.max(0, now.cache.read - ledger.charged.cache.read),
|
|
11078
|
+
write: Math.max(0, now.cache.write - ledger.charged.cache.write)
|
|
11079
|
+
}
|
|
11080
|
+
};
|
|
11081
|
+
const deltaTokens = totalTokens(delta);
|
|
11082
|
+
if (deltaTokens === 0) {
|
|
11083
|
+
this.emit("cost:delta", {
|
|
11084
|
+
taskId: ledger.taskId,
|
|
11085
|
+
nodeId: ledger.taskId,
|
|
11086
|
+
agentId: ledger.agentId,
|
|
11087
|
+
sessionID: ledger.sessionID,
|
|
11088
|
+
model: ledger.model,
|
|
11089
|
+
deltaCost: 0,
|
|
11090
|
+
deltaTokens: 0,
|
|
11091
|
+
sessionTotalCost: priceUsage(ledger.charged, this.forecaster.tiersFor(ledger.model, ledger.provider).pricing).total,
|
|
11092
|
+
reason,
|
|
11093
|
+
settledTier: this.settledTierOf(ledger, ledger.charged)
|
|
11094
|
+
});
|
|
11095
|
+
return;
|
|
11096
|
+
}
|
|
11097
|
+
const { pricing, source } = this.forecaster.tiersFor(ledger.model, ledger.provider);
|
|
11098
|
+
const deltaCost = priceUsageAtSettledTier(delta, pricing, now).total;
|
|
11099
|
+
const provenance = { usage: "measured", pricing: source };
|
|
11100
|
+
ledger.charged = now;
|
|
11101
|
+
let historyAdjusted = false;
|
|
11102
|
+
let performanceAdjusted = false;
|
|
11103
|
+
let nodeAdjusted = false;
|
|
11104
|
+
let agentAdjusted = false;
|
|
11105
|
+
let error;
|
|
11106
|
+
try {
|
|
11107
|
+
this.trackCost(ledger.agentId, ledger.model, deltaCost, deltaTokens, provenance);
|
|
11108
|
+
historyAdjusted = this.executionHistory.adjust(ledger.historyId, {
|
|
11109
|
+
cost: deltaCost,
|
|
11110
|
+
tokensUsed: deltaTokens
|
|
11111
|
+
});
|
|
11112
|
+
performanceAdjusted = this.performanceTracker.adjust(ledger.performanceId, { cost: deltaCost });
|
|
11113
|
+
ledger.agent.metrics.totalCost += deltaCost;
|
|
11114
|
+
ledger.agent.metrics.totalTokens += deltaTokens;
|
|
11115
|
+
agentAdjusted = true;
|
|
11116
|
+
if (ledger.node.result === ledger.result) {
|
|
11117
|
+
ledger.result.cost += deltaCost;
|
|
11118
|
+
ledger.result.tokensUsed += deltaTokens;
|
|
11119
|
+
nodeAdjusted = true;
|
|
11120
|
+
}
|
|
11121
|
+
this.uncollected.delete(ledger.sessionID);
|
|
11122
|
+
} catch (thrown) {
|
|
11123
|
+
error = thrown instanceof Error ? thrown.message : String(thrown);
|
|
11124
|
+
}
|
|
11125
|
+
this.emit("cost:delta", {
|
|
11126
|
+
taskId: ledger.taskId,
|
|
11127
|
+
nodeId: ledger.taskId,
|
|
11128
|
+
agentId: ledger.agentId,
|
|
11129
|
+
sessionID: ledger.sessionID,
|
|
11130
|
+
model: ledger.model,
|
|
11131
|
+
deltaCost,
|
|
11132
|
+
deltaTokens,
|
|
11133
|
+
sessionTotalCost: priceUsage(now, pricing).total,
|
|
11134
|
+
reason,
|
|
11135
|
+
settledTier: this.settledTierOf(ledger, now),
|
|
11136
|
+
recordsAdjusted: { history: historyAdjusted, performance: performanceAdjusted, node: nodeAdjusted, agent: agentAdjusted },
|
|
11137
|
+
...error === undefined ? {} : { error }
|
|
11138
|
+
});
|
|
11139
|
+
} finally {
|
|
11140
|
+
this.deltaLedgers.delete(ledger.sessionID);
|
|
11141
|
+
ledger.pending = false;
|
|
11142
|
+
ledger.abort.abort();
|
|
11143
|
+
}
|
|
11144
|
+
}
|
|
11145
|
+
abandonTimeoutDelta(ledger, probeUsage, reason) {
|
|
11146
|
+
const eventReason = reason === "shutdown" ? "shutdown" : "abandoned";
|
|
11147
|
+
const known = probeUsage ?? ledger.charged;
|
|
11148
|
+
const { pricing } = this.forecaster.tiersFor(ledger.model, ledger.provider);
|
|
11149
|
+
const observedIncrement = {
|
|
11150
|
+
input: Math.max(0, known.input - ledger.charged.input),
|
|
11151
|
+
output: Math.max(0, known.output - ledger.charged.output),
|
|
11152
|
+
reasoning: Math.max(0, known.reasoning - ledger.charged.reasoning),
|
|
11153
|
+
cache: {
|
|
11154
|
+
read: Math.max(0, known.cache.read - ledger.charged.cache.read),
|
|
11155
|
+
write: Math.max(0, known.cache.write - ledger.charged.cache.write)
|
|
11156
|
+
}
|
|
11157
|
+
};
|
|
11158
|
+
const observedUncollected = totalTokens(observedIncrement) > 0 ? priceUsageAtSettledTier(observedIncrement, pricing, known).total : 0;
|
|
11159
|
+
this.uncollected.set(ledger.sessionID, {
|
|
11160
|
+
sessionID: ledger.sessionID,
|
|
11161
|
+
taskId: ledger.taskId,
|
|
11162
|
+
agentId: ledger.agentId,
|
|
11163
|
+
model: ledger.model,
|
|
11164
|
+
lastKnownTokens: totalTokens(known),
|
|
11165
|
+
observedUncollected
|
|
11166
|
+
});
|
|
11167
|
+
this.emit("cost:delta", {
|
|
11168
|
+
taskId: ledger.taskId,
|
|
11169
|
+
nodeId: ledger.taskId,
|
|
11170
|
+
agentId: ledger.agentId,
|
|
11171
|
+
sessionID: ledger.sessionID,
|
|
11172
|
+
model: ledger.model,
|
|
11173
|
+
deltaCost: 0,
|
|
11174
|
+
deltaTokens: 0,
|
|
11175
|
+
sessionTotalCost: priceUsage(known, pricing).total,
|
|
11176
|
+
reason: eventReason,
|
|
11177
|
+
settledTier: this.settledTierOf(ledger, known),
|
|
11178
|
+
uncollected: { lastKnownTokens: totalTokens(known), observedUncollected }
|
|
11179
|
+
});
|
|
11180
|
+
}
|
|
11181
|
+
settledTierOf(ledger, usage) {
|
|
11182
|
+
const { pricing, source } = this.forecaster.tiersFor(ledger.model, ledger.provider);
|
|
11183
|
+
const promptSize = promptSizeOf(usage);
|
|
11184
|
+
return {
|
|
11185
|
+
pricing: source,
|
|
11186
|
+
promptSizeAtSettlement: promptSize,
|
|
11187
|
+
threshold: selectTier(pricing.tiers, promptSize).threshold ?? null
|
|
11188
|
+
};
|
|
11189
|
+
}
|
|
11190
|
+
async flushTimeoutDeltas() {
|
|
11191
|
+
for (const timer of this.deltaTimers)
|
|
11192
|
+
clearTimeout(timer);
|
|
11193
|
+
this.deltaTimers.clear();
|
|
11194
|
+
for (const ledger of [...this.deltaLedgers.values()]) {
|
|
11195
|
+
await this.settleTimeoutDelta(ledger, "shutdown").catch(() => {});
|
|
11196
|
+
}
|
|
11197
|
+
}
|
|
10775
11198
|
buildRolePrompt(role) {
|
|
10776
11199
|
if (this.customRoles.has(role)) {
|
|
10777
11200
|
return this.customRoles.getPrompt(role) || `You are a ${role}. Complete the assigned task professionally.`;
|
|
@@ -10834,16 +11257,25 @@ Please continue from where the previous agent left off.`;
|
|
|
10834
11257
|
await this.terminateAgent(agent.id);
|
|
10835
11258
|
node.status = "pending";
|
|
10836
11259
|
this.notifyStateChange();
|
|
10837
|
-
await this.spawnAndExecute(node, context);
|
|
11260
|
+
await this.spawnAndExecute(node, { transferContext: context });
|
|
10838
11261
|
return;
|
|
10839
11262
|
}
|
|
10840
|
-
|
|
10841
|
-
|
|
11263
|
+
const failedRef = `${agent.model.provider}/${agent.model.model}`;
|
|
11264
|
+
const hasUsableFallback = policy.fallbackModels.some((m) => m !== failedRef);
|
|
11265
|
+
if (policy.fallbackModels.length > 0 && hasUsableFallback) {
|
|
11266
|
+
let fallbackModel;
|
|
11267
|
+
while (policy.fallbackModels.length > 0) {
|
|
11268
|
+
const candidate = policy.fallbackModels.shift();
|
|
11269
|
+
if (candidate !== failedRef) {
|
|
11270
|
+
fallbackModel = candidate;
|
|
11271
|
+
break;
|
|
11272
|
+
}
|
|
11273
|
+
}
|
|
10842
11274
|
if (fallbackModel) {
|
|
10843
11275
|
await this.terminateAgent(agent.id);
|
|
10844
11276
|
node.status = "pending";
|
|
10845
11277
|
this.notifyStateChange();
|
|
10846
|
-
await this.
|
|
11278
|
+
await this.spawnAndExecute(node, { modelOverride: fallbackModel });
|
|
10847
11279
|
return;
|
|
10848
11280
|
}
|
|
10849
11281
|
}
|
|
@@ -11077,32 +11509,16 @@ Please continue from where the previous agent left off.`;
|
|
|
11077
11509
|
if (qualified)
|
|
11078
11510
|
return qualified;
|
|
11079
11511
|
}
|
|
11080
|
-
const bare =
|
|
11512
|
+
const bare = bareModelId(model);
|
|
11081
11513
|
let best;
|
|
11082
11514
|
for (const [key, cost] of this.modelCosts) {
|
|
11083
|
-
if (
|
|
11515
|
+
if (bareModelId(key) !== bare)
|
|
11084
11516
|
continue;
|
|
11085
|
-
if (!best || cost
|
|
11517
|
+
if (!best || baseInputRate(cost) < baseInputRate(best))
|
|
11086
11518
|
best = cost;
|
|
11087
11519
|
}
|
|
11088
11520
|
return best;
|
|
11089
11521
|
}
|
|
11090
|
-
estimateModelCost(model, provider) {
|
|
11091
|
-
const realCost = this.getModelCost(model, provider);
|
|
11092
|
-
if (realCost) {
|
|
11093
|
-
return (realCost.input + realCost.output) / 2;
|
|
11094
|
-
}
|
|
11095
|
-
const costs = {
|
|
11096
|
-
"claude-sonnet-4-6": 0.15,
|
|
11097
|
-
"claude-opus-4-7": 15,
|
|
11098
|
-
"claude-haiku-4-5": 0.8,
|
|
11099
|
-
"gpt-5-mini": 0.05,
|
|
11100
|
-
"gpt-5": 2.5,
|
|
11101
|
-
"gemini-2.5-flash": 0.075,
|
|
11102
|
-
"minimax-m2.5-free": 0
|
|
11103
|
-
};
|
|
11104
|
-
return costs[model] || 0.1;
|
|
11105
|
-
}
|
|
11106
11522
|
estimateModelQuality(model) {
|
|
11107
11523
|
const quality = {
|
|
11108
11524
|
"claude-opus-4-7": 0.95,
|
|
@@ -11127,13 +11543,17 @@ Please continue from where the previous agent left off.`;
|
|
|
11127
11543
|
};
|
|
11128
11544
|
return speeds[model] || 0.5;
|
|
11129
11545
|
}
|
|
11130
|
-
scoreModel(modelId, role, complexity) {
|
|
11546
|
+
scoreModel(modelId, role, complexity, estimates) {
|
|
11131
11547
|
const [provider, ...parts] = modelId.split("/");
|
|
11132
11548
|
const model = parts.join("/");
|
|
11133
|
-
const
|
|
11549
|
+
const estimate = estimates?.get(modelId) ?? this.forecaster.estimateCost(complexity, model, provider);
|
|
11550
|
+
let maxEstimate = 0;
|
|
11551
|
+
for (const value of estimates?.values() ?? []) {
|
|
11552
|
+
if (value > maxEstimate)
|
|
11553
|
+
maxEstimate = value;
|
|
11554
|
+
}
|
|
11555
|
+
const costScore = maxEstimate === 0 ? 1 : 1 - estimate / maxEstimate;
|
|
11134
11556
|
const quality = this.estimateModelQuality(model);
|
|
11135
|
-
const maxCost = 15;
|
|
11136
|
-
const costScore = 1 - cost / maxCost;
|
|
11137
11557
|
const speedScore = this.estimateModelSpeed(model);
|
|
11138
11558
|
const qualityWeight = complexity.overall > 70 ? 0.6 : complexity.overall > 40 ? 0.4 : 0.2;
|
|
11139
11559
|
const costWeight = 1 - qualityWeight;
|
|
@@ -11145,7 +11565,7 @@ Please continue from where the previous agent left off.`;
|
|
|
11145
11565
|
qualityScore: quality,
|
|
11146
11566
|
speedScore,
|
|
11147
11567
|
overallScore,
|
|
11148
|
-
reasoning: `Score: ${overallScore.toFixed(2)} (quality: ${quality.toFixed(2)}, cost: ${costScore.toFixed(2)}, speed: ${speedScore.toFixed(2)})`
|
|
11568
|
+
reasoning: `Score: ${overallScore.toFixed(2)} (quality: ${quality.toFixed(2)}, cost: ${costScore.toFixed(2)} [~$${estimate.toFixed(4)}/task], speed: ${speedScore.toFixed(2)})`
|
|
11149
11569
|
};
|
|
11150
11570
|
}
|
|
11151
11571
|
selectBestModel(role, complexity) {
|
|
@@ -11159,31 +11579,127 @@ Please continue from where the previous agent left off.`;
|
|
|
11159
11579
|
"opencode/minimax-m2.5-free"
|
|
11160
11580
|
];
|
|
11161
11581
|
const unique = [...new Set(candidates)];
|
|
11162
|
-
const
|
|
11582
|
+
const estimates = new Map;
|
|
11583
|
+
for (const ref of unique) {
|
|
11584
|
+
const [provider, ...parts] = ref.split("/");
|
|
11585
|
+
estimates.set(ref, this.forecaster.estimateCost(complexity, parts.join("/"), provider));
|
|
11586
|
+
}
|
|
11587
|
+
const scored = unique.map((ref) => ({ ref, score: this.scoreModel(ref, role, complexity, estimates) }));
|
|
11163
11588
|
const budgetRemaining = this.budget.maxTotalCost - this.totalSpent;
|
|
11164
|
-
const affordable = scored.filter((
|
|
11165
|
-
const
|
|
11166
|
-
return
|
|
11589
|
+
const affordable = scored.filter(({ ref }) => {
|
|
11590
|
+
const estimate = estimates.get(ref) ?? 0;
|
|
11591
|
+
return estimate <= budgetRemaining || estimate === 0;
|
|
11167
11592
|
});
|
|
11168
|
-
const best = (affordable.length > 0 ? affordable : scored).sort((a, b) => b.overallScore - a.overallScore)[0];
|
|
11593
|
+
const best = (affordable.length > 0 ? affordable : scored).sort((a, b) => b.score.overallScore - a.score.overallScore)[0];
|
|
11169
11594
|
return {
|
|
11170
|
-
provider: best.provider,
|
|
11171
|
-
model: best.model,
|
|
11172
|
-
estimatedCost:
|
|
11173
|
-
estimatedQuality: best.qualityScore,
|
|
11174
|
-
reasoning: best.reasoning
|
|
11595
|
+
provider: best.score.provider,
|
|
11596
|
+
model: best.score.model,
|
|
11597
|
+
estimatedCost: estimates.get(best.ref) ?? 0,
|
|
11598
|
+
estimatedQuality: best.score.qualityScore,
|
|
11599
|
+
reasoning: best.score.reasoning
|
|
11175
11600
|
};
|
|
11176
11601
|
}
|
|
11177
|
-
trackCost(agentId, model, cost, tokens) {
|
|
11602
|
+
trackCost(agentId, model, cost, tokens, provenance) {
|
|
11178
11603
|
this.totalSpent += cost;
|
|
11179
11604
|
const agentCost = this.costByAgent.get(agentId) || 0;
|
|
11180
11605
|
this.costByAgent.set(agentId, agentCost + cost);
|
|
11181
11606
|
const modelCost = this.costByModel.get(model) || 0;
|
|
11182
11607
|
this.costByModel.set(model, modelCost + cost);
|
|
11183
|
-
this.
|
|
11608
|
+
this.tokensByModel.set(model, (this.tokensByModel.get(model) || 0) + tokens);
|
|
11609
|
+
this.recordProvenance(model, cost, provenance);
|
|
11610
|
+
this.costHistory.push({ timestamp: Date.now(), cost, agentId, model, tokens, provenance });
|
|
11184
11611
|
this.checkBudget();
|
|
11185
11612
|
this.notifyStateChange();
|
|
11186
11613
|
}
|
|
11614
|
+
recordProvenance(model, cost, provenance) {
|
|
11615
|
+
const measured = provenance.usage === "measured";
|
|
11616
|
+
const prior = this.costProvenance.get(model) ?? {
|
|
11617
|
+
usage: provenance.usage,
|
|
11618
|
+
pricing: provenance.pricing,
|
|
11619
|
+
measuredEntries: 0,
|
|
11620
|
+
estimatedEntries: 0,
|
|
11621
|
+
measuredSpend: 0,
|
|
11622
|
+
estimatedSpend: 0
|
|
11623
|
+
};
|
|
11624
|
+
prior.usage = provenance.usage;
|
|
11625
|
+
prior.pricing = provenance.pricing;
|
|
11626
|
+
if (measured) {
|
|
11627
|
+
prior.measuredEntries++;
|
|
11628
|
+
prior.measuredSpend += cost;
|
|
11629
|
+
} else {
|
|
11630
|
+
prior.estimatedEntries++;
|
|
11631
|
+
prior.estimatedSpend += cost;
|
|
11632
|
+
}
|
|
11633
|
+
this.costProvenance.set(model, prior);
|
|
11634
|
+
}
|
|
11635
|
+
spendSplit() {
|
|
11636
|
+
return {
|
|
11637
|
+
measuredSpend: sumBy(this.costProvenance, (p) => p.measuredSpend),
|
|
11638
|
+
estimatedSpend: sumBy(this.costProvenance, (p) => p.estimatedSpend)
|
|
11639
|
+
};
|
|
11640
|
+
}
|
|
11641
|
+
safeAccountTaskCost(agent, task) {
|
|
11642
|
+
return this.accountTaskCost(agent, task).catch(() => ({
|
|
11643
|
+
cost: 0,
|
|
11644
|
+
tokensUsed: 0,
|
|
11645
|
+
provenance: { usage: "estimated", pricing: "unknown-model" },
|
|
11646
|
+
usage: null,
|
|
11647
|
+
idleAt: 0
|
|
11648
|
+
}));
|
|
11649
|
+
}
|
|
11650
|
+
async accountTaskCost(agent, task) {
|
|
11651
|
+
const model = `${agent.model.provider}/${agent.model.model}`;
|
|
11652
|
+
const read = agent.sessionID ? await this.readSessionTokens(agent.sessionID) : { read: false };
|
|
11653
|
+
if (read.read) {
|
|
11654
|
+
const measured = this.forecaster.measureCost(read.usage, model, agent.model.provider);
|
|
11655
|
+
return {
|
|
11656
|
+
cost: measured.cost,
|
|
11657
|
+
tokensUsed: measured.tokens,
|
|
11658
|
+
provenance: { usage: "measured", pricing: measured.pricingSource },
|
|
11659
|
+
usage: read.usage,
|
|
11660
|
+
idleAt: read.idleAt
|
|
11661
|
+
};
|
|
11662
|
+
}
|
|
11663
|
+
const predicted = this.forecaster.forecastTask(task, task.requiredRole, model, task.complexity);
|
|
11664
|
+
return {
|
|
11665
|
+
cost: predicted.estimatedCost,
|
|
11666
|
+
tokensUsed: predicted.estimatedInputTokens + predicted.estimatedOutputTokens,
|
|
11667
|
+
provenance: { usage: "estimated", pricing: predicted.pricingSource },
|
|
11668
|
+
usage: null,
|
|
11669
|
+
idleAt: 0
|
|
11670
|
+
};
|
|
11671
|
+
}
|
|
11672
|
+
async readSessionTokens(sessionID) {
|
|
11673
|
+
const finite = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
11674
|
+
try {
|
|
11675
|
+
const session = await this.ctx?.session.get({ sessionID });
|
|
11676
|
+
const tokens = session?.tokens;
|
|
11677
|
+
if (!tokens)
|
|
11678
|
+
return { read: false };
|
|
11679
|
+
return {
|
|
11680
|
+
read: true,
|
|
11681
|
+
idleAt: typeof session?.time?.idle === "number" && Number.isFinite(session.time.idle) ? session.time.idle : 0,
|
|
11682
|
+
usage: {
|
|
11683
|
+
input: finite(tokens.input),
|
|
11684
|
+
output: finite(tokens.output),
|
|
11685
|
+
reasoning: finite(tokens.reasoning),
|
|
11686
|
+
cache: { read: finite(tokens.cache?.read), write: finite(tokens.cache?.write) }
|
|
11687
|
+
}
|
|
11688
|
+
};
|
|
11689
|
+
} catch {
|
|
11690
|
+
return { read: false };
|
|
11691
|
+
}
|
|
11692
|
+
}
|
|
11693
|
+
async readSessionTokensWithRetry(sessionID) {
|
|
11694
|
+
for (let attempt = 0;attempt < this.deltaReadBackoffMs.length; attempt++) {
|
|
11695
|
+
if (attempt > 0)
|
|
11696
|
+
await this.sleep(this.deltaReadBackoffMs[attempt] ?? 0);
|
|
11697
|
+
const read = await this.readSessionTokens(sessionID);
|
|
11698
|
+
if (read.read)
|
|
11699
|
+
return read;
|
|
11700
|
+
}
|
|
11701
|
+
return { read: false };
|
|
11702
|
+
}
|
|
11187
11703
|
checkBudget() {
|
|
11188
11704
|
const remaining = this.budget.maxTotalCost - this.totalSpent;
|
|
11189
11705
|
const remainingPercent = remaining / this.budget.maxTotalCost;
|
|
@@ -11250,8 +11766,9 @@ Please continue from where the previous agent left off.`;
|
|
|
11250
11766
|
global: loadInfo?.global ?? null,
|
|
11251
11767
|
models: this.configManager.getResolvedModels()
|
|
11252
11768
|
};
|
|
11769
|
+
const spend = this.spendSplit();
|
|
11253
11770
|
if (detailed)
|
|
11254
|
-
return JSON.stringify({ ...state, budgetExceeded: this.budgetExceeded, config }, null, 2);
|
|
11771
|
+
return JSON.stringify({ ...state, ...spend, budgetExceeded: this.budgetExceeded, config }, null, 2);
|
|
11255
11772
|
return JSON.stringify({
|
|
11256
11773
|
running: state.running,
|
|
11257
11774
|
paused: state.paused,
|
|
@@ -11259,6 +11776,7 @@ Please continue from where the previous agent left off.`;
|
|
|
11259
11776
|
agents: state.agents.length,
|
|
11260
11777
|
tasks: state.tasks.length,
|
|
11261
11778
|
totalCost: state.totalSpent,
|
|
11779
|
+
...spend,
|
|
11262
11780
|
budgetRemaining: state.budgetRemaining,
|
|
11263
11781
|
config
|
|
11264
11782
|
}, null, 2);
|
|
@@ -11273,10 +11791,25 @@ Please continue from where the previous agent left off.`;
|
|
|
11273
11791
|
return JSON.stringify({
|
|
11274
11792
|
totalSpent: state.totalSpent,
|
|
11275
11793
|
budgetRemaining: state.budgetRemaining,
|
|
11794
|
+
...this.spendSplit(),
|
|
11276
11795
|
byAgent: Object.fromEntries(this.costByAgent),
|
|
11277
|
-
byModel: Object.fromEntries(this.costByModel)
|
|
11796
|
+
byModel: Object.fromEntries(this.costByModel),
|
|
11797
|
+
tokensByModel: Object.fromEntries(this.tokensByModel),
|
|
11798
|
+
provenance: Object.fromEntries(this.costProvenance),
|
|
11799
|
+
measuredEntries: sumBy(this.costProvenance, (p) => p.measuredEntries),
|
|
11800
|
+
estimatedEntries: sumBy(this.costProvenance, (p) => p.estimatedEntries),
|
|
11801
|
+
uncollected: this.uncollectedSummary()
|
|
11278
11802
|
}, null, 2);
|
|
11279
11803
|
}
|
|
11804
|
+
uncollectedSummary() {
|
|
11805
|
+
const all = [...this.uncollected.values()];
|
|
11806
|
+
return {
|
|
11807
|
+
sessions: all.length,
|
|
11808
|
+
lastKnownTokens: all.reduce((sum, u) => sum + u.lastKnownTokens, 0),
|
|
11809
|
+
observedUncollected: all.reduce((sum, u) => sum + u.observedUncollected, 0),
|
|
11810
|
+
taskIds: all.map((u) => u.taskId)
|
|
11811
|
+
};
|
|
11812
|
+
}
|
|
11280
11813
|
pause() {
|
|
11281
11814
|
this.paused = true;
|
|
11282
11815
|
this.emit("orchestrator:paused", {});
|
|
@@ -11294,6 +11827,7 @@ Please continue from where the previous agent left off.`;
|
|
|
11294
11827
|
this.budgetExceeded = false;
|
|
11295
11828
|
}
|
|
11296
11829
|
async shutdown() {
|
|
11830
|
+
this.shuttingDown = true;
|
|
11297
11831
|
await this.moduleRegistry.teardownAll();
|
|
11298
11832
|
this._healthMonitor?.stop();
|
|
11299
11833
|
this.stopDashboard();
|
|
@@ -11306,6 +11840,7 @@ Please continue from where the previous agent left off.`;
|
|
|
11306
11840
|
clearTimeout(this.stateChangeTimer);
|
|
11307
11841
|
this.stateChangeTimer = null;
|
|
11308
11842
|
}
|
|
11843
|
+
await this.flushTimeoutDeltas();
|
|
11309
11844
|
this.agents.forEach((agent) => {
|
|
11310
11845
|
agent.status = "terminated";
|
|
11311
11846
|
});
|
|
@@ -11671,8 +12206,18 @@ class AstGrep {
|
|
|
11671
12206
|
|
|
11672
12207
|
// src/index.ts
|
|
11673
12208
|
import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync4, existsSync as existsSync3, statSync } from "node:fs";
|
|
12209
|
+
import { createHash } from "node:crypto";
|
|
11674
12210
|
import { join as join6, resolve as resolve3, basename, dirname as dirname4 } from "node:path";
|
|
11675
12211
|
import { homedir as homedir2 } from "node:os";
|
|
12212
|
+
function renderCost(cost, provenance) {
|
|
12213
|
+
return provenance.usage === "measured" ? `$${cost.toFixed(4)} measured` : `$${cost.toFixed(4)} estimated`;
|
|
12214
|
+
}
|
|
12215
|
+
function renderCostBasis(score) {
|
|
12216
|
+
if (score.costBasis === "none") {
|
|
12217
|
+
return "cost unscored: 0 measured tasks, 0% of score from cost";
|
|
12218
|
+
}
|
|
12219
|
+
return `avg=$${score.avgCost.toFixed(4)} (${score.measuredTasks}/${score.totalTasks} measured)`;
|
|
12220
|
+
}
|
|
11676
12221
|
var CONFIG_RELOAD_DEBOUNCE_MS = 150;
|
|
11677
12222
|
var CONFIG_POLL_INTERVAL_MS = 2000;
|
|
11678
12223
|
function isNexusConfigFile(file, eventDirectory, projectDirectory, projectPath, globalPath) {
|
|
@@ -11687,6 +12232,8 @@ var watchedContexts = new WeakSet;
|
|
|
11687
12232
|
var MAX_RELOAD_WAIT_MS = 1000;
|
|
11688
12233
|
var MIN_INTERVAL_MS = 25;
|
|
11689
12234
|
var clampInterval = (ms, fallback) => Number.isFinite(ms) && ms >= MIN_INTERVAL_MS ? ms : fallback;
|
|
12235
|
+
var ABSENT = "absent";
|
|
12236
|
+
var UNREADABLE = "unreadable";
|
|
11690
12237
|
function watchConfigFiles(ctx, orchestrator, debounceMs = CONFIG_RELOAD_DEBOUNCE_MS, pollMs = CONFIG_POLL_INTERVAL_MS) {
|
|
11691
12238
|
if (watchedContexts.has(ctx))
|
|
11692
12239
|
return () => {};
|
|
@@ -11706,15 +12253,24 @@ function watchConfigFiles(ctx, orchestrator, debounceMs = CONFIG_RELOAD_DEBOUNCE
|
|
|
11706
12253
|
clearTimeout(debounceTimer);
|
|
11707
12254
|
debounceTimer = null;
|
|
11708
12255
|
};
|
|
12256
|
+
const digestOf = (filePath) => {
|
|
12257
|
+
try {
|
|
12258
|
+
return createHash("sha1").update(readFileSync3(filePath)).digest("hex");
|
|
12259
|
+
} catch {
|
|
12260
|
+
return UNREADABLE;
|
|
12261
|
+
}
|
|
12262
|
+
};
|
|
11709
12263
|
const stampOf = (filePath) => {
|
|
12264
|
+
let stat;
|
|
11710
12265
|
try {
|
|
11711
12266
|
const stats = statSync(filePath);
|
|
11712
12267
|
if (!stats.isFile())
|
|
11713
|
-
return
|
|
11714
|
-
|
|
12268
|
+
return ABSENT;
|
|
12269
|
+
stat = `${stats.mtimeMs}:${stats.size}`;
|
|
11715
12270
|
} catch {
|
|
11716
|
-
return
|
|
12271
|
+
return ABSENT;
|
|
11717
12272
|
}
|
|
12273
|
+
return `${stat}|${digestOf(filePath)}`;
|
|
11718
12274
|
};
|
|
11719
12275
|
const lastSeen = new Map(watchedPaths.map((p) => [p, stampOf(p)]));
|
|
11720
12276
|
const runReload = (trigger) => {
|
|
@@ -11963,6 +12519,19 @@ Before marking a task complete:
|
|
|
11963
12519
|
4. Follows project conventions
|
|
11964
12520
|
5. Has appropriate test coverage
|
|
11965
12521
|
`;
|
|
12522
|
+
function clearPresetOverride(orchestrator) {
|
|
12523
|
+
const configManager = orchestrator.configManager;
|
|
12524
|
+
const hadOverride = configManager.resetToDefaults();
|
|
12525
|
+
const models = configManager.getResolvedModels();
|
|
12526
|
+
const resolved = Object.entries(models).map(([role, model]) => `${role}=${model}`).join(" ");
|
|
12527
|
+
return {
|
|
12528
|
+
content: hadOverride ? `Cleared the session preset override. nexus.jsonc is in control again — no config file was modified.
|
|
12529
|
+
` + `Resolved models now: ${resolved}
|
|
12530
|
+
` + `sessionOverride: false` : `No session preset override was set, so nothing was cleared — nexus.jsonc was already in control. No config file was modified.
|
|
12531
|
+
` + `Resolved models now: ${resolved}
|
|
12532
|
+
` + `sessionOverride: false`
|
|
12533
|
+
};
|
|
12534
|
+
}
|
|
11966
12535
|
var src_default = define({
|
|
11967
12536
|
id: "nexus",
|
|
11968
12537
|
async setup(ctx) {
|
|
@@ -12444,23 +13013,35 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12444
13013
|
});
|
|
12445
13014
|
editor.add({
|
|
12446
13015
|
name: "preset",
|
|
12447
|
-
description: "Apply a preset
|
|
13016
|
+
description: "Apply a session preset (model/budget selection), or clear it with mode 'clear' to hand control back to nexus.jsonc. A preset shadows the config file's models, so call mode 'clear' if the user edited nexus.jsonc but no model changed.",
|
|
12448
13017
|
input: {
|
|
12449
13018
|
type: "object",
|
|
12450
13019
|
properties: {
|
|
12451
|
-
|
|
13020
|
+
mode: {
|
|
13021
|
+
type: "string",
|
|
13022
|
+
enum: ["apply", "clear"],
|
|
13023
|
+
description: "'apply' (default) applies the named preset for this session. 'clear' drops the session preset override so nexus.jsonc is in control again — use it whenever edits to nexus.jsonc appear to have no effect."
|
|
13024
|
+
},
|
|
13025
|
+
name: {
|
|
13026
|
+
type: "string",
|
|
13027
|
+
description: "Preset name (minimal, balanced, enterprise, cost-optimized). Required for mode 'apply'; ignored by 'clear'."
|
|
13028
|
+
}
|
|
12452
13029
|
},
|
|
12453
|
-
required: ["name"],
|
|
12454
13030
|
additionalProperties: false
|
|
12455
13031
|
},
|
|
12456
13032
|
options: { codemode: true },
|
|
12457
13033
|
execute: async (input) => {
|
|
12458
|
-
const { name } = input;
|
|
13034
|
+
const { mode, name } = input;
|
|
13035
|
+
if (mode === "clear")
|
|
13036
|
+
return clearPresetOverride(orchestrator);
|
|
13037
|
+
if (!name) {
|
|
13038
|
+
return { content: `Error: mode 'apply' needs a \`name\` (${orchestrator.configManager.listPresets().join(", ")}). To hand control back to nexus.jsonc instead, call preset with mode: 'clear'.` };
|
|
13039
|
+
}
|
|
12459
13040
|
try {
|
|
12460
13041
|
orchestrator.configManager.applyPreset(name);
|
|
12461
13042
|
return {
|
|
12462
13043
|
content: `Applied preset: ${PRESETS[name]?.name || name}
|
|
12463
|
-
` + `⚠️ This preset now overrides the \`models\` section of nexus.jsonc. ` + `Edits to models in the config file will NOT take effect until the preset is cleared. ` + `Clear it from the TUI (config manager) to hand control back to disk. ` + `Budget and self-healing values from the file still apply.`
|
|
13044
|
+
` + `⚠️ This preset now overrides the \`models\` section of nexus.jsonc. ` + `Edits to models in the config file will NOT take effect until the preset is cleared. ` + `Clear it with this tool (mode: 'clear'), or from the TUI (config manager), to hand control back to disk. ` + `Budget and self-healing values from the file still apply.`
|
|
12464
13045
|
};
|
|
12465
13046
|
} catch (error) {
|
|
12466
13047
|
return { content: `Error: ${error.message}` };
|
|
@@ -12515,6 +13096,8 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12515
13096
|
execute: async (input) => {
|
|
12516
13097
|
const { model, setInput, setOutput } = input;
|
|
12517
13098
|
const per1k = (v) => `$${v}/1K tokens`;
|
|
13099
|
+
const renderTiers = (cost) => cost.tiers.map((t) => t.threshold === undefined ? `base: in=${per1k(t.rates.input)}, out=${per1k(t.rates.output)}, cache_read=${per1k(t.rates.cacheRead)}, cache_write=${per1k(t.rates.cacheWrite)}` : `over ${t.threshold} prompt tokens: in=${per1k(t.rates.input)}, out=${per1k(t.rates.output)}, cache_read=${per1k(t.rates.cacheRead)}, cache_write=${per1k(t.rates.cacheWrite)}`).join(`
|
|
13100
|
+
`);
|
|
12518
13101
|
if (model && setInput !== undefined && setOutput !== undefined) {
|
|
12519
13102
|
orchestrator.setModelCosts({ [model]: { input: setInput, output: setOutput } });
|
|
12520
13103
|
return { content: `Set ${model}: input=${per1k(setInput)}, output=${per1k(setOutput)}` };
|
|
@@ -12522,20 +13105,23 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12522
13105
|
if (model) {
|
|
12523
13106
|
const cost = orchestrator.getModelCost(model);
|
|
12524
13107
|
if (cost) {
|
|
12525
|
-
return { content: `${model}
|
|
13108
|
+
return { content: `${model} (real pricing, from OpenCode):
|
|
13109
|
+
${renderTiers(cost)}` };
|
|
12526
13110
|
}
|
|
12527
|
-
const
|
|
12528
|
-
|
|
13111
|
+
const { pricing, source } = orchestrator.forecaster.priceFor(model);
|
|
13112
|
+
const label = source === "model-costs" ? "real pricing" : source === "fallback-table" ? "ESTIMATE (fallback table, not provider pricing)" : "ESTIMATE (unknown model — no table knows this rate)";
|
|
13113
|
+
return { content: `${model}: no real pricing data.
|
|
13114
|
+
${label}: in=${per1k(pricing.input)}, out=${per1k(pricing.output)}, cache_read=${per1k(pricing.cacheRead)}, cache_write=${per1k(pricing.cacheWrite)}` };
|
|
12529
13115
|
}
|
|
12530
13116
|
if (orchestrator.modelCosts.size > 0) {
|
|
12531
13117
|
const lines = ["\uD83D\uDCCA Model Pricing (from OpenCode, per 1K tokens):"];
|
|
12532
13118
|
for (const [id, cost] of orchestrator.modelCosts) {
|
|
12533
|
-
lines.push(` ${id}: ${
|
|
13119
|
+
lines.push(` ${id}: ${renderTiers(cost)}`);
|
|
12534
13120
|
}
|
|
12535
13121
|
return { content: lines.join(`
|
|
12536
13122
|
`) };
|
|
12537
13123
|
}
|
|
12538
|
-
return { content: "No real pricing data loaded. Using
|
|
13124
|
+
return { content: "No real pricing data loaded. Using labelled fallback estimates." };
|
|
12539
13125
|
}
|
|
12540
13126
|
});
|
|
12541
13127
|
editor.add({
|
|
@@ -12765,7 +13351,7 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12765
13351
|
const scores = orchestrator.performanceTracker.getScores();
|
|
12766
13352
|
if (scores.length === 0)
|
|
12767
13353
|
return { content: "No performance data yet. Scores build up as tasks are executed." };
|
|
12768
|
-
const lines = scores.map((s) => `${s.role}/${s.model}: score=${s.overallScore.toFixed(1)} success=${(s.successRate * 100).toFixed(0)}%
|
|
13354
|
+
const lines = scores.map((s) => `${s.role}/${s.model}: score=${s.overallScore.toFixed(1)} success=${(s.successRate * 100).toFixed(0)}% ${renderCostBasis(s)} (${s.totalTasks} tasks)`);
|
|
12769
13355
|
return { content: lines.join(`
|
|
12770
13356
|
`) };
|
|
12771
13357
|
}
|
|
@@ -12787,7 +13373,7 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12787
13373
|
const best = orchestrator.performanceTracker.getBestModel(role);
|
|
12788
13374
|
if (!best)
|
|
12789
13375
|
return { content: `No performance data for role '${role}' yet.` };
|
|
12790
|
-
return { content: `Best for ${role}: ${best.model} (score: ${best.overallScore.toFixed(1)}, success: ${(best.successRate * 100).toFixed(0)}%,
|
|
13376
|
+
return { content: `Best for ${role}: ${best.model} (score: ${best.overallScore.toFixed(1)}, success: ${(best.successRate * 100).toFixed(0)}%, ${renderCostBasis(best)})` };
|
|
12791
13377
|
}
|
|
12792
13378
|
});
|
|
12793
13379
|
editor.add({
|
|
@@ -12826,7 +13412,7 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12826
13412
|
const records = count ? orchestrator.executionHistory.getRecent(count) : orchestrator.executionHistory.getAll();
|
|
12827
13413
|
if (records.length === 0)
|
|
12828
13414
|
return { content: "No execution history yet." };
|
|
12829
|
-
const lines = records.map((r) => `${r.status === "success" ? "✅" : "❌"} ${r.taskName} (${r.role}) —
|
|
13415
|
+
const lines = records.map((r) => `${r.status === "success" ? "✅" : "❌"} ${r.taskName} (${r.role}) — ${renderCost(r.cost, r.costProvenance)} — ${r.duration}ms`);
|
|
12830
13416
|
return { content: lines.join(`
|
|
12831
13417
|
`) };
|
|
12832
13418
|
}
|
|
@@ -12842,7 +13428,7 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12842
13428
|
options: { codemode: true },
|
|
12843
13429
|
execute: async () => {
|
|
12844
13430
|
const stats = orchestrator.executionHistory.getStats();
|
|
12845
|
-
return { content: `Total: ${stats.total} | Success: ${(stats.successRate * 100).toFixed(1)}% | Cost: $${stats.totalCost.toFixed(4)} | Avg: ${stats.avgDuration.toFixed(0)}ms
|
|
13431
|
+
return { content: `Total: ${stats.total} | Success: ${(stats.successRate * 100).toFixed(1)}% | Cost: $${stats.totalCost.toFixed(4)} (measured $${stats.costSplit.measuredCost.toFixed(4)} over ${stats.costSplit.measuredEntries} tasks, estimated $${stats.costSplit.estimatedCost.toFixed(4)} over ${stats.costSplit.estimatedEntries} tasks) | Avg: ${stats.avgDuration.toFixed(0)}ms
|
|
12846
13432
|
By role: ${JSON.stringify(stats.byRole)}` };
|
|
12847
13433
|
}
|
|
12848
13434
|
});
|