@serkanalgur/opencode-nexus 2.5.0 → 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 +526 -90
- package/dist/tui.js +3 -1
- 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 = [];
|
|
@@ -9930,16 +9979,19 @@ function per1kPricing(input, output) {
|
|
|
9930
9979
|
cacheWrite: input * CACHE_WRITE_MULTIPLE
|
|
9931
9980
|
};
|
|
9932
9981
|
}
|
|
9982
|
+
function untiered(rates) {
|
|
9983
|
+
return { tiers: [{ rates }] };
|
|
9984
|
+
}
|
|
9933
9985
|
var FALLBACK_PRICING_PER_1K = {
|
|
9934
|
-
"claude-sonnet-4-6": per1kPricing(0.015, 0.075),
|
|
9935
|
-
"claude-opus-4-7": per1kPricing(0.075, 0.375),
|
|
9936
|
-
"claude-haiku-4-5": per1kPricing(0.001, 0.005),
|
|
9937
|
-
"gpt-5-mini": per1kPricing(0.00015, 0.0006),
|
|
9938
|
-
"gpt-5": per1kPricing(0.0025, 0.01),
|
|
9939
|
-
"gemini-2.5-flash": per1kPricing(0.000075, 0.0003),
|
|
9940
|
-
"minimax-m2.5-free": per1kPricing(0, 0)
|
|
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))
|
|
9941
9993
|
};
|
|
9942
|
-
var UNKNOWN_PRICING_PER_1K = per1kPricing(0.01, 0.05);
|
|
9994
|
+
var UNKNOWN_PRICING_PER_1K = untiered(per1kPricing(0.01, 0.05));
|
|
9943
9995
|
var per1k = (rate, tokens) => rate * tokens / 1000;
|
|
9944
9996
|
function bareModelId(ref) {
|
|
9945
9997
|
return ref.slice(ref.indexOf("/") + 1);
|
|
@@ -9947,6 +9999,35 @@ function bareModelId(ref) {
|
|
|
9947
9999
|
function totalTokens(usage) {
|
|
9948
10000
|
return usage.input + usage.output + usage.reasoning + usage.cache.read + usage.cache.write;
|
|
9949
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
|
+
}
|
|
9950
10031
|
function priceTokens(usage, pricing) {
|
|
9951
10032
|
const inputCost = per1k(pricing.input, usage.input);
|
|
9952
10033
|
const outputCost = per1k(pricing.output, usage.output + usage.reasoning);
|
|
@@ -9959,7 +10040,11 @@ class CostForecaster {
|
|
|
9959
10040
|
constructor(pricing) {
|
|
9960
10041
|
this.resolvePricing = pricing;
|
|
9961
10042
|
}
|
|
9962
|
-
priceFor(model, provider) {
|
|
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) {
|
|
9963
10048
|
const real = this.resolvePricing?.(model, provider);
|
|
9964
10049
|
if (real)
|
|
9965
10050
|
return { pricing: real, source: "model-costs" };
|
|
@@ -9970,16 +10055,16 @@ class CostForecaster {
|
|
|
9970
10055
|
return { pricing: UNKNOWN_PRICING_PER_1K, source: "unknown-model" };
|
|
9971
10056
|
}
|
|
9972
10057
|
measureCost(usage, model, provider) {
|
|
9973
|
-
const { pricing, source } = this.
|
|
10058
|
+
const { pricing, source } = this.tiersFor(model, provider);
|
|
9974
10059
|
return {
|
|
9975
|
-
cost:
|
|
10060
|
+
cost: priceUsage(usage, pricing).total,
|
|
9976
10061
|
tokens: totalTokens(usage),
|
|
9977
10062
|
pricingSource: source,
|
|
9978
10063
|
...source === "model-costs" ? { confidence: 1 } : {}
|
|
9979
10064
|
};
|
|
9980
10065
|
}
|
|
9981
10066
|
costOf(usage, model, provider) {
|
|
9982
|
-
return
|
|
10067
|
+
return priceUsage(usage, this.tiersFor(model, provider).pricing).total;
|
|
9983
10068
|
}
|
|
9984
10069
|
estimateTokensFor(complexity, fileCount) {
|
|
9985
10070
|
const baseInput = 700;
|
|
@@ -9998,8 +10083,8 @@ class CostForecaster {
|
|
|
9998
10083
|
}
|
|
9999
10084
|
forecastTask(task, role, modelId, complexity) {
|
|
10000
10085
|
const { input, output } = this.estimateTokens(task, complexity);
|
|
10001
|
-
const { pricing, source } = this.
|
|
10002
|
-
const breakdown =
|
|
10086
|
+
const { pricing, source } = this.tiersFor(modelId);
|
|
10087
|
+
const breakdown = priceUsage({ input, output, reasoning: 0, cache: { read: 0, write: 0 } }, pricing);
|
|
10003
10088
|
return {
|
|
10004
10089
|
taskId: task.id,
|
|
10005
10090
|
taskName: task.name,
|
|
@@ -10208,6 +10293,13 @@ ${lines.join(`
|
|
|
10208
10293
|
}
|
|
10209
10294
|
|
|
10210
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];
|
|
10211
10303
|
function assistantMessageText(message) {
|
|
10212
10304
|
if (message.type !== "assistant")
|
|
10213
10305
|
return "";
|
|
@@ -10229,6 +10321,9 @@ function sumBy(records, select) {
|
|
|
10229
10321
|
total += select(record);
|
|
10230
10322
|
return total;
|
|
10231
10323
|
}
|
|
10324
|
+
function baseInputRate(cost) {
|
|
10325
|
+
return selectTier(cost.tiers, 0).rates.input;
|
|
10326
|
+
}
|
|
10232
10327
|
var DEFAULT_ESCALATION = {
|
|
10233
10328
|
maxRetries: 3,
|
|
10234
10329
|
retryDelay: 1000,
|
|
@@ -10277,6 +10372,11 @@ class NexusOrchestrator {
|
|
|
10277
10372
|
tokensByModel = new Map;
|
|
10278
10373
|
costProvenance = new Map;
|
|
10279
10374
|
cleanupInterval = null;
|
|
10375
|
+
deltaLedgers = new Map;
|
|
10376
|
+
uncollected = new Map;
|
|
10377
|
+
deltaTimers = new Set;
|
|
10378
|
+
shuttingDown = false;
|
|
10379
|
+
deltaReadBackoffMs = DELTA_READ_BACKOFF_MS;
|
|
10280
10380
|
get healthMonitor() {
|
|
10281
10381
|
return this._healthMonitor;
|
|
10282
10382
|
}
|
|
@@ -10337,23 +10437,40 @@ class NexusOrchestrator {
|
|
|
10337
10437
|
for (const model of data) {
|
|
10338
10438
|
if (!model?.cost || !Array.isArray(model.cost) || model.cost.length === 0)
|
|
10339
10439
|
continue;
|
|
10340
|
-
const baseCost = model.cost[0];
|
|
10341
10440
|
this.modelCosts.set(`${model.providerID}/${model.id}`, {
|
|
10342
|
-
|
|
10343
|
-
output: per1k(baseCost.output),
|
|
10344
|
-
cacheRead: per1k(baseCost.cache?.read),
|
|
10345
|
-
cacheWrite: per1k(baseCost.cache?.write)
|
|
10441
|
+
tiers: this.normaliseTiers(model.cost, per1k)
|
|
10346
10442
|
});
|
|
10347
10443
|
}
|
|
10348
10444
|
} catch {}
|
|
10349
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
|
+
}
|
|
10350
10463
|
setModelCosts(costs) {
|
|
10351
10464
|
for (const [model, cost] of Object.entries(costs)) {
|
|
10352
10465
|
this.modelCosts.set(model, {
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
|
|
10466
|
+
tiers: [{
|
|
10467
|
+
rates: {
|
|
10468
|
+
input: cost.input,
|
|
10469
|
+
output: cost.output,
|
|
10470
|
+
cacheRead: cost.cacheRead || 0,
|
|
10471
|
+
cacheWrite: cost.cacheWrite || 0
|
|
10472
|
+
}
|
|
10473
|
+
}]
|
|
10357
10474
|
});
|
|
10358
10475
|
}
|
|
10359
10476
|
}
|
|
@@ -10471,10 +10588,14 @@ class NexusOrchestrator {
|
|
|
10471
10588
|
enabled: true,
|
|
10472
10589
|
patternStorage: "memory",
|
|
10473
10590
|
minConfidence: 0.7
|
|
10591
|
+
},
|
|
10592
|
+
cost: {
|
|
10593
|
+
timeoutDeltaGraceMs: 60000
|
|
10474
10594
|
}
|
|
10475
10595
|
};
|
|
10596
|
+
const { cost: defaultCost, ...restDefaults } = defaults;
|
|
10476
10597
|
return {
|
|
10477
|
-
...
|
|
10598
|
+
...restDefaults,
|
|
10478
10599
|
...partial,
|
|
10479
10600
|
budget: { ...defaults.budget, ...partial?.budget },
|
|
10480
10601
|
agents: { ...defaults.agents, ...partial?.agents },
|
|
@@ -10483,7 +10604,8 @@ class NexusOrchestrator {
|
|
|
10483
10604
|
memory: { ...defaults.memory, ...partial?.memory },
|
|
10484
10605
|
dashboard: { ...defaults.dashboard, ...partial?.dashboard },
|
|
10485
10606
|
security: { ...defaults.security, ...partial?.security },
|
|
10486
|
-
learning: { ...defaults.learning, ...partial?.learning }
|
|
10607
|
+
learning: { ...defaults.learning, ...partial?.learning },
|
|
10608
|
+
...partial?.cost ? { cost: { ...defaultCost, ...partial.cost } } : {}
|
|
10487
10609
|
};
|
|
10488
10610
|
}
|
|
10489
10611
|
notifyStateChange() {
|
|
@@ -10537,6 +10659,7 @@ class NexusOrchestrator {
|
|
|
10537
10659
|
success: true,
|
|
10538
10660
|
tasks: results,
|
|
10539
10661
|
totalCost: this.totalSpent,
|
|
10662
|
+
...this.spendSplit(),
|
|
10540
10663
|
totalDuration,
|
|
10541
10664
|
agentsUsed: this.agents.size
|
|
10542
10665
|
};
|
|
@@ -10546,6 +10669,7 @@ class NexusOrchestrator {
|
|
|
10546
10669
|
success: false,
|
|
10547
10670
|
tasks: [],
|
|
10548
10671
|
totalCost: this.totalSpent,
|
|
10672
|
+
...this.spendSplit(),
|
|
10549
10673
|
totalDuration: Date.now() - startTime,
|
|
10550
10674
|
agentsUsed: this.agents.size
|
|
10551
10675
|
};
|
|
@@ -10695,6 +10819,9 @@ class NexusOrchestrator {
|
|
|
10695
10819
|
}
|
|
10696
10820
|
const startTime = Date.now();
|
|
10697
10821
|
const timeout = node.task.timeout || this.config.defaultTimeout;
|
|
10822
|
+
let timeoutTimer = null;
|
|
10823
|
+
let waitGuard = null;
|
|
10824
|
+
let waitAbort = null;
|
|
10698
10825
|
try {
|
|
10699
10826
|
const rolePrompt = this.buildRolePrompt(node.task.requiredRole);
|
|
10700
10827
|
let taskPrompt = `${rolePrompt}
|
|
@@ -10726,8 +10853,12 @@ Please continue from where the previous agent left off.`;
|
|
|
10726
10853
|
sessionID: agent.sessionID,
|
|
10727
10854
|
text: taskPrompt
|
|
10728
10855
|
});
|
|
10729
|
-
|
|
10730
|
-
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
|
+
});
|
|
10731
10862
|
await Promise.race([waitPromise, timeoutPromise]);
|
|
10732
10863
|
const messages = await this.ctx.session.context({ sessionID: agent.sessionID });
|
|
10733
10864
|
const output = lastAssistantText(messages) || "Task completed (no output captured)";
|
|
@@ -10753,6 +10884,7 @@ Please continue from where the previous agent left off.`;
|
|
|
10753
10884
|
success: result.success,
|
|
10754
10885
|
duration: result.duration,
|
|
10755
10886
|
cost: result.cost,
|
|
10887
|
+
costProvenance: result.costProvenance,
|
|
10756
10888
|
tokensUsed: result.tokensUsed
|
|
10757
10889
|
});
|
|
10758
10890
|
this.executionHistory.record({
|
|
@@ -10762,6 +10894,7 @@ Please continue from where the previous agent left off.`;
|
|
|
10762
10894
|
model: agent.model.model,
|
|
10763
10895
|
status: "success",
|
|
10764
10896
|
cost: result.cost,
|
|
10897
|
+
costProvenance: result.costProvenance,
|
|
10765
10898
|
duration: result.duration,
|
|
10766
10899
|
tokensUsed: result.tokensUsed,
|
|
10767
10900
|
startedAt: new Date(startTime),
|
|
@@ -10815,27 +10948,41 @@ Please continue from where the previous agent left off.`;
|
|
|
10815
10948
|
duration,
|
|
10816
10949
|
sessionID: agent.sessionID
|
|
10817
10950
|
});
|
|
10818
|
-
this.performanceTracker.record({
|
|
10951
|
+
const performanceId = this.performanceTracker.record({
|
|
10819
10952
|
model: agent.model.model,
|
|
10820
10953
|
role: node.task.requiredRole,
|
|
10821
10954
|
success: result.success,
|
|
10822
10955
|
duration: result.duration,
|
|
10823
10956
|
cost: result.cost,
|
|
10957
|
+
costProvenance: result.costProvenance,
|
|
10824
10958
|
tokensUsed: result.tokensUsed
|
|
10825
10959
|
});
|
|
10826
|
-
this.executionHistory.record({
|
|
10960
|
+
const historyId = this.executionHistory.record({
|
|
10827
10961
|
taskId: node.id,
|
|
10828
10962
|
taskName: node.task.name,
|
|
10829
10963
|
role: node.task.requiredRole,
|
|
10830
10964
|
model: agent.model.model,
|
|
10831
10965
|
status: "failed",
|
|
10832
10966
|
cost: result.cost,
|
|
10967
|
+
costProvenance: result.costProvenance,
|
|
10833
10968
|
duration: result.duration,
|
|
10834
10969
|
tokensUsed: result.tokensUsed,
|
|
10835
10970
|
startedAt: new Date(startTime),
|
|
10836
10971
|
completedAt: new Date,
|
|
10837
10972
|
error: errorMessage
|
|
10838
|
-
});
|
|
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
|
+
}
|
|
10839
10986
|
if (this.notifications?.isEnabled()) {
|
|
10840
10987
|
this.notifications.notify({ title: "Nexus: Task Failed", body: `${node.task.name} failed: ${errorMessage}`, sound: true });
|
|
10841
10988
|
}
|
|
@@ -10843,6 +10990,10 @@ Please continue from where the previous agent left off.`;
|
|
|
10843
10990
|
await this.handleFailure(agent, node, new Error(errorMessage));
|
|
10844
10991
|
}
|
|
10845
10992
|
} finally {
|
|
10993
|
+
if (timeoutTimer) {
|
|
10994
|
+
clearTimeout(timeoutTimer);
|
|
10995
|
+
timeoutTimer = null;
|
|
10996
|
+
}
|
|
10846
10997
|
if (agent.status !== "terminated" && agent.status !== "failed") {
|
|
10847
10998
|
agent.status = "idle";
|
|
10848
10999
|
}
|
|
@@ -10850,6 +11001,200 @@ Please continue from where the previous agent left off.`;
|
|
|
10850
11001
|
this.notifyStateChange();
|
|
10851
11002
|
}
|
|
10852
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
|
+
}
|
|
10853
11198
|
buildRolePrompt(role) {
|
|
10854
11199
|
if (this.customRoles.has(role)) {
|
|
10855
11200
|
return this.customRoles.getPrompt(role) || `You are a ${role}. Complete the assigned task professionally.`;
|
|
@@ -10915,8 +11260,17 @@ Please continue from where the previous agent left off.`;
|
|
|
10915
11260
|
await this.spawnAndExecute(node, { transferContext: context });
|
|
10916
11261
|
return;
|
|
10917
11262
|
}
|
|
10918
|
-
|
|
10919
|
-
|
|
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
|
+
}
|
|
10920
11274
|
if (fallbackModel) {
|
|
10921
11275
|
await this.terminateAgent(agent.id);
|
|
10922
11276
|
node.status = "pending";
|
|
@@ -11160,27 +11514,11 @@ Please continue from where the previous agent left off.`;
|
|
|
11160
11514
|
for (const [key, cost] of this.modelCosts) {
|
|
11161
11515
|
if (bareModelId(key) !== bare)
|
|
11162
11516
|
continue;
|
|
11163
|
-
if (!best || cost
|
|
11517
|
+
if (!best || baseInputRate(cost) < baseInputRate(best))
|
|
11164
11518
|
best = cost;
|
|
11165
11519
|
}
|
|
11166
11520
|
return best;
|
|
11167
11521
|
}
|
|
11168
|
-
estimateModelCost(model, provider) {
|
|
11169
|
-
const realCost = this.getModelCost(model, provider);
|
|
11170
|
-
if (realCost) {
|
|
11171
|
-
return (realCost.input + realCost.output) / 2;
|
|
11172
|
-
}
|
|
11173
|
-
const costs = {
|
|
11174
|
-
"claude-sonnet-4-6": 0.15,
|
|
11175
|
-
"claude-opus-4-7": 15,
|
|
11176
|
-
"claude-haiku-4-5": 0.8,
|
|
11177
|
-
"gpt-5-mini": 0.05,
|
|
11178
|
-
"gpt-5": 2.5,
|
|
11179
|
-
"gemini-2.5-flash": 0.075,
|
|
11180
|
-
"minimax-m2.5-free": 0
|
|
11181
|
-
};
|
|
11182
|
-
return costs[model] || 0.1;
|
|
11183
|
-
}
|
|
11184
11522
|
estimateModelQuality(model) {
|
|
11185
11523
|
const quality = {
|
|
11186
11524
|
"claude-opus-4-7": 0.95,
|
|
@@ -11205,13 +11543,17 @@ Please continue from where the previous agent left off.`;
|
|
|
11205
11543
|
};
|
|
11206
11544
|
return speeds[model] || 0.5;
|
|
11207
11545
|
}
|
|
11208
|
-
scoreModel(modelId, role, complexity) {
|
|
11546
|
+
scoreModel(modelId, role, complexity, estimates) {
|
|
11209
11547
|
const [provider, ...parts] = modelId.split("/");
|
|
11210
11548
|
const model = parts.join("/");
|
|
11211
|
-
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;
|
|
11212
11556
|
const quality = this.estimateModelQuality(model);
|
|
11213
|
-
const maxCost = 15;
|
|
11214
|
-
const costScore = 1 - cost / maxCost;
|
|
11215
11557
|
const speedScore = this.estimateModelSpeed(model);
|
|
11216
11558
|
const qualityWeight = complexity.overall > 70 ? 0.6 : complexity.overall > 40 ? 0.4 : 0.2;
|
|
11217
11559
|
const costWeight = 1 - qualityWeight;
|
|
@@ -11223,7 +11565,7 @@ Please continue from where the previous agent left off.`;
|
|
|
11223
11565
|
qualityScore: quality,
|
|
11224
11566
|
speedScore,
|
|
11225
11567
|
overallScore,
|
|
11226
|
-
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)})`
|
|
11227
11569
|
};
|
|
11228
11570
|
}
|
|
11229
11571
|
selectBestModel(role, complexity) {
|
|
@@ -11237,19 +11579,24 @@ Please continue from where the previous agent left off.`;
|
|
|
11237
11579
|
"opencode/minimax-m2.5-free"
|
|
11238
11580
|
];
|
|
11239
11581
|
const unique = [...new Set(candidates)];
|
|
11240
|
-
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) }));
|
|
11241
11588
|
const budgetRemaining = this.budget.maxTotalCost - this.totalSpent;
|
|
11242
|
-
const affordable = scored.filter((
|
|
11243
|
-
const estimate =
|
|
11589
|
+
const affordable = scored.filter(({ ref }) => {
|
|
11590
|
+
const estimate = estimates.get(ref) ?? 0;
|
|
11244
11591
|
return estimate <= budgetRemaining || estimate === 0;
|
|
11245
11592
|
});
|
|
11246
|
-
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];
|
|
11247
11594
|
return {
|
|
11248
|
-
provider: best.provider,
|
|
11249
|
-
model: best.model,
|
|
11250
|
-
estimatedCost:
|
|
11251
|
-
estimatedQuality: best.qualityScore,
|
|
11252
|
-
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
|
|
11253
11600
|
};
|
|
11254
11601
|
}
|
|
11255
11602
|
trackCost(agentId, model, cost, tokens, provenance) {
|
|
@@ -11285,11 +11632,19 @@ Please continue from where the previous agent left off.`;
|
|
|
11285
11632
|
}
|
|
11286
11633
|
this.costProvenance.set(model, prior);
|
|
11287
11634
|
}
|
|
11635
|
+
spendSplit() {
|
|
11636
|
+
return {
|
|
11637
|
+
measuredSpend: sumBy(this.costProvenance, (p) => p.measuredSpend),
|
|
11638
|
+
estimatedSpend: sumBy(this.costProvenance, (p) => p.estimatedSpend)
|
|
11639
|
+
};
|
|
11640
|
+
}
|
|
11288
11641
|
safeAccountTaskCost(agent, task) {
|
|
11289
11642
|
return this.accountTaskCost(agent, task).catch(() => ({
|
|
11290
11643
|
cost: 0,
|
|
11291
11644
|
tokensUsed: 0,
|
|
11292
|
-
provenance: { usage: "estimated", pricing: "unknown-model" }
|
|
11645
|
+
provenance: { usage: "estimated", pricing: "unknown-model" },
|
|
11646
|
+
usage: null,
|
|
11647
|
+
idleAt: 0
|
|
11293
11648
|
}));
|
|
11294
11649
|
}
|
|
11295
11650
|
async accountTaskCost(agent, task) {
|
|
@@ -11300,14 +11655,18 @@ Please continue from where the previous agent left off.`;
|
|
|
11300
11655
|
return {
|
|
11301
11656
|
cost: measured.cost,
|
|
11302
11657
|
tokensUsed: measured.tokens,
|
|
11303
|
-
provenance: { usage: "measured", pricing: measured.pricingSource }
|
|
11658
|
+
provenance: { usage: "measured", pricing: measured.pricingSource },
|
|
11659
|
+
usage: read.usage,
|
|
11660
|
+
idleAt: read.idleAt
|
|
11304
11661
|
};
|
|
11305
11662
|
}
|
|
11306
11663
|
const predicted = this.forecaster.forecastTask(task, task.requiredRole, model, task.complexity);
|
|
11307
11664
|
return {
|
|
11308
11665
|
cost: predicted.estimatedCost,
|
|
11309
11666
|
tokensUsed: predicted.estimatedInputTokens + predicted.estimatedOutputTokens,
|
|
11310
|
-
provenance: { usage: "estimated", pricing: predicted.pricingSource }
|
|
11667
|
+
provenance: { usage: "estimated", pricing: predicted.pricingSource },
|
|
11668
|
+
usage: null,
|
|
11669
|
+
idleAt: 0
|
|
11311
11670
|
};
|
|
11312
11671
|
}
|
|
11313
11672
|
async readSessionTokens(sessionID) {
|
|
@@ -11319,6 +11678,7 @@ Please continue from where the previous agent left off.`;
|
|
|
11319
11678
|
return { read: false };
|
|
11320
11679
|
return {
|
|
11321
11680
|
read: true,
|
|
11681
|
+
idleAt: typeof session?.time?.idle === "number" && Number.isFinite(session.time.idle) ? session.time.idle : 0,
|
|
11322
11682
|
usage: {
|
|
11323
11683
|
input: finite(tokens.input),
|
|
11324
11684
|
output: finite(tokens.output),
|
|
@@ -11330,6 +11690,16 @@ Please continue from where the previous agent left off.`;
|
|
|
11330
11690
|
return { read: false };
|
|
11331
11691
|
}
|
|
11332
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
|
+
}
|
|
11333
11703
|
checkBudget() {
|
|
11334
11704
|
const remaining = this.budget.maxTotalCost - this.totalSpent;
|
|
11335
11705
|
const remainingPercent = remaining / this.budget.maxTotalCost;
|
|
@@ -11396,8 +11766,9 @@ Please continue from where the previous agent left off.`;
|
|
|
11396
11766
|
global: loadInfo?.global ?? null,
|
|
11397
11767
|
models: this.configManager.getResolvedModels()
|
|
11398
11768
|
};
|
|
11769
|
+
const spend = this.spendSplit();
|
|
11399
11770
|
if (detailed)
|
|
11400
|
-
return JSON.stringify({ ...state, budgetExceeded: this.budgetExceeded, config }, null, 2);
|
|
11771
|
+
return JSON.stringify({ ...state, ...spend, budgetExceeded: this.budgetExceeded, config }, null, 2);
|
|
11401
11772
|
return JSON.stringify({
|
|
11402
11773
|
running: state.running,
|
|
11403
11774
|
paused: state.paused,
|
|
@@ -11405,6 +11776,7 @@ Please continue from where the previous agent left off.`;
|
|
|
11405
11776
|
agents: state.agents.length,
|
|
11406
11777
|
tasks: state.tasks.length,
|
|
11407
11778
|
totalCost: state.totalSpent,
|
|
11779
|
+
...spend,
|
|
11408
11780
|
budgetRemaining: state.budgetRemaining,
|
|
11409
11781
|
config
|
|
11410
11782
|
}, null, 2);
|
|
@@ -11419,14 +11791,25 @@ Please continue from where the previous agent left off.`;
|
|
|
11419
11791
|
return JSON.stringify({
|
|
11420
11792
|
totalSpent: state.totalSpent,
|
|
11421
11793
|
budgetRemaining: state.budgetRemaining,
|
|
11794
|
+
...this.spendSplit(),
|
|
11422
11795
|
byAgent: Object.fromEntries(this.costByAgent),
|
|
11423
11796
|
byModel: Object.fromEntries(this.costByModel),
|
|
11424
11797
|
tokensByModel: Object.fromEntries(this.tokensByModel),
|
|
11425
11798
|
provenance: Object.fromEntries(this.costProvenance),
|
|
11426
11799
|
measuredEntries: sumBy(this.costProvenance, (p) => p.measuredEntries),
|
|
11427
|
-
estimatedEntries: sumBy(this.costProvenance, (p) => p.estimatedEntries)
|
|
11800
|
+
estimatedEntries: sumBy(this.costProvenance, (p) => p.estimatedEntries),
|
|
11801
|
+
uncollected: this.uncollectedSummary()
|
|
11428
11802
|
}, null, 2);
|
|
11429
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
|
+
}
|
|
11430
11813
|
pause() {
|
|
11431
11814
|
this.paused = true;
|
|
11432
11815
|
this.emit("orchestrator:paused", {});
|
|
@@ -11444,6 +11827,7 @@ Please continue from where the previous agent left off.`;
|
|
|
11444
11827
|
this.budgetExceeded = false;
|
|
11445
11828
|
}
|
|
11446
11829
|
async shutdown() {
|
|
11830
|
+
this.shuttingDown = true;
|
|
11447
11831
|
await this.moduleRegistry.teardownAll();
|
|
11448
11832
|
this._healthMonitor?.stop();
|
|
11449
11833
|
this.stopDashboard();
|
|
@@ -11456,6 +11840,7 @@ Please continue from where the previous agent left off.`;
|
|
|
11456
11840
|
clearTimeout(this.stateChangeTimer);
|
|
11457
11841
|
this.stateChangeTimer = null;
|
|
11458
11842
|
}
|
|
11843
|
+
await this.flushTimeoutDeltas();
|
|
11459
11844
|
this.agents.forEach((agent) => {
|
|
11460
11845
|
agent.status = "terminated";
|
|
11461
11846
|
});
|
|
@@ -11821,8 +12206,18 @@ class AstGrep {
|
|
|
11821
12206
|
|
|
11822
12207
|
// src/index.ts
|
|
11823
12208
|
import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync4, existsSync as existsSync3, statSync } from "node:fs";
|
|
12209
|
+
import { createHash } from "node:crypto";
|
|
11824
12210
|
import { join as join6, resolve as resolve3, basename, dirname as dirname4 } from "node:path";
|
|
11825
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
|
+
}
|
|
11826
12221
|
var CONFIG_RELOAD_DEBOUNCE_MS = 150;
|
|
11827
12222
|
var CONFIG_POLL_INTERVAL_MS = 2000;
|
|
11828
12223
|
function isNexusConfigFile(file, eventDirectory, projectDirectory, projectPath, globalPath) {
|
|
@@ -11837,6 +12232,8 @@ var watchedContexts = new WeakSet;
|
|
|
11837
12232
|
var MAX_RELOAD_WAIT_MS = 1000;
|
|
11838
12233
|
var MIN_INTERVAL_MS = 25;
|
|
11839
12234
|
var clampInterval = (ms, fallback) => Number.isFinite(ms) && ms >= MIN_INTERVAL_MS ? ms : fallback;
|
|
12235
|
+
var ABSENT = "absent";
|
|
12236
|
+
var UNREADABLE = "unreadable";
|
|
11840
12237
|
function watchConfigFiles(ctx, orchestrator, debounceMs = CONFIG_RELOAD_DEBOUNCE_MS, pollMs = CONFIG_POLL_INTERVAL_MS) {
|
|
11841
12238
|
if (watchedContexts.has(ctx))
|
|
11842
12239
|
return () => {};
|
|
@@ -11856,15 +12253,24 @@ function watchConfigFiles(ctx, orchestrator, debounceMs = CONFIG_RELOAD_DEBOUNCE
|
|
|
11856
12253
|
clearTimeout(debounceTimer);
|
|
11857
12254
|
debounceTimer = null;
|
|
11858
12255
|
};
|
|
12256
|
+
const digestOf = (filePath) => {
|
|
12257
|
+
try {
|
|
12258
|
+
return createHash("sha1").update(readFileSync3(filePath)).digest("hex");
|
|
12259
|
+
} catch {
|
|
12260
|
+
return UNREADABLE;
|
|
12261
|
+
}
|
|
12262
|
+
};
|
|
11859
12263
|
const stampOf = (filePath) => {
|
|
12264
|
+
let stat;
|
|
11860
12265
|
try {
|
|
11861
12266
|
const stats = statSync(filePath);
|
|
11862
12267
|
if (!stats.isFile())
|
|
11863
|
-
return
|
|
11864
|
-
|
|
12268
|
+
return ABSENT;
|
|
12269
|
+
stat = `${stats.mtimeMs}:${stats.size}`;
|
|
11865
12270
|
} catch {
|
|
11866
|
-
return
|
|
12271
|
+
return ABSENT;
|
|
11867
12272
|
}
|
|
12273
|
+
return `${stat}|${digestOf(filePath)}`;
|
|
11868
12274
|
};
|
|
11869
12275
|
const lastSeen = new Map(watchedPaths.map((p) => [p, stampOf(p)]));
|
|
11870
12276
|
const runReload = (trigger) => {
|
|
@@ -12113,6 +12519,19 @@ Before marking a task complete:
|
|
|
12113
12519
|
4. Follows project conventions
|
|
12114
12520
|
5. Has appropriate test coverage
|
|
12115
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
|
+
}
|
|
12116
12535
|
var src_default = define({
|
|
12117
12536
|
id: "nexus",
|
|
12118
12537
|
async setup(ctx) {
|
|
@@ -12594,23 +13013,35 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12594
13013
|
});
|
|
12595
13014
|
editor.add({
|
|
12596
13015
|
name: "preset",
|
|
12597
|
-
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.",
|
|
12598
13017
|
input: {
|
|
12599
13018
|
type: "object",
|
|
12600
13019
|
properties: {
|
|
12601
|
-
|
|
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
|
+
}
|
|
12602
13029
|
},
|
|
12603
|
-
required: ["name"],
|
|
12604
13030
|
additionalProperties: false
|
|
12605
13031
|
},
|
|
12606
13032
|
options: { codemode: true },
|
|
12607
13033
|
execute: async (input) => {
|
|
12608
|
-
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
|
+
}
|
|
12609
13040
|
try {
|
|
12610
13041
|
orchestrator.configManager.applyPreset(name);
|
|
12611
13042
|
return {
|
|
12612
13043
|
content: `Applied preset: ${PRESETS[name]?.name || name}
|
|
12613
|
-
` + `⚠️ 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.`
|
|
12614
13045
|
};
|
|
12615
13046
|
} catch (error) {
|
|
12616
13047
|
return { content: `Error: ${error.message}` };
|
|
@@ -12665,6 +13096,8 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12665
13096
|
execute: async (input) => {
|
|
12666
13097
|
const { model, setInput, setOutput } = input;
|
|
12667
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
|
+
`);
|
|
12668
13101
|
if (model && setInput !== undefined && setOutput !== undefined) {
|
|
12669
13102
|
orchestrator.setModelCosts({ [model]: { input: setInput, output: setOutput } });
|
|
12670
13103
|
return { content: `Set ${model}: input=${per1k(setInput)}, output=${per1k(setOutput)}` };
|
|
@@ -12672,20 +13105,23 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12672
13105
|
if (model) {
|
|
12673
13106
|
const cost = orchestrator.getModelCost(model);
|
|
12674
13107
|
if (cost) {
|
|
12675
|
-
return { content: `${model}
|
|
13108
|
+
return { content: `${model} (real pricing, from OpenCode):
|
|
13109
|
+
${renderTiers(cost)}` };
|
|
12676
13110
|
}
|
|
12677
|
-
const
|
|
12678
|
-
|
|
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)}` };
|
|
12679
13115
|
}
|
|
12680
13116
|
if (orchestrator.modelCosts.size > 0) {
|
|
12681
13117
|
const lines = ["\uD83D\uDCCA Model Pricing (from OpenCode, per 1K tokens):"];
|
|
12682
13118
|
for (const [id, cost] of orchestrator.modelCosts) {
|
|
12683
|
-
lines.push(` ${id}: ${
|
|
13119
|
+
lines.push(` ${id}: ${renderTiers(cost)}`);
|
|
12684
13120
|
}
|
|
12685
13121
|
return { content: lines.join(`
|
|
12686
13122
|
`) };
|
|
12687
13123
|
}
|
|
12688
|
-
return { content: "No real pricing data loaded. Using
|
|
13124
|
+
return { content: "No real pricing data loaded. Using labelled fallback estimates." };
|
|
12689
13125
|
}
|
|
12690
13126
|
});
|
|
12691
13127
|
editor.add({
|
|
@@ -12915,7 +13351,7 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12915
13351
|
const scores = orchestrator.performanceTracker.getScores();
|
|
12916
13352
|
if (scores.length === 0)
|
|
12917
13353
|
return { content: "No performance data yet. Scores build up as tasks are executed." };
|
|
12918
|
-
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)`);
|
|
12919
13355
|
return { content: lines.join(`
|
|
12920
13356
|
`) };
|
|
12921
13357
|
}
|
|
@@ -12937,7 +13373,7 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12937
13373
|
const best = orchestrator.performanceTracker.getBestModel(role);
|
|
12938
13374
|
if (!best)
|
|
12939
13375
|
return { content: `No performance data for role '${role}' yet.` };
|
|
12940
|
-
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)})` };
|
|
12941
13377
|
}
|
|
12942
13378
|
});
|
|
12943
13379
|
editor.add({
|
|
@@ -12976,7 +13412,7 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12976
13412
|
const records = count ? orchestrator.executionHistory.getRecent(count) : orchestrator.executionHistory.getAll();
|
|
12977
13413
|
if (records.length === 0)
|
|
12978
13414
|
return { content: "No execution history yet." };
|
|
12979
|
-
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`);
|
|
12980
13416
|
return { content: lines.join(`
|
|
12981
13417
|
`) };
|
|
12982
13418
|
}
|
|
@@ -12992,7 +13428,7 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12992
13428
|
options: { codemode: true },
|
|
12993
13429
|
execute: async () => {
|
|
12994
13430
|
const stats = orchestrator.executionHistory.getStats();
|
|
12995
|
-
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
|
|
12996
13432
|
By role: ${JSON.stringify(stats.byRole)}` };
|
|
12997
13433
|
}
|
|
12998
13434
|
});
|
package/dist/tui.js
CHANGED
|
@@ -94,7 +94,7 @@ function describeFile(file) {
|
|
|
94
94
|
}
|
|
95
95
|
function formatConfigLoadLog(info) {
|
|
96
96
|
const models = Object.entries(info.models).map(([role, model]) => `${role}=${model}`).join(" ");
|
|
97
|
-
const override = info.sessionOverride ?
|
|
97
|
+
const override = info.sessionOverride ? ' (+session override: storage; disk edits to models are IGNORED while a preset is set \u2014 call the preset tool with mode "clear", or reset in the TUI, to hand control back to disk)' : "";
|
|
98
98
|
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}`;
|
|
99
99
|
}
|
|
100
100
|
var DEFAULT_CONFIG = {
|
|
@@ -311,7 +311,9 @@ class NexusConfigManager {
|
|
|
311
311
|
return result;
|
|
312
312
|
}
|
|
313
313
|
resetToDefaults() {
|
|
314
|
+
const hadOverride = this.storageConfig !== null;
|
|
314
315
|
this.storageConfig = null;
|
|
316
|
+
return hadOverride;
|
|
315
317
|
}
|
|
316
318
|
applyPreset(name) {
|
|
317
319
|
const preset = PRESETS[name];
|