@serkanalgur/opencode-nexus 2.4.1 → 2.5.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 +190 -40
- package/dist/tui.js +86 -59
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9920,33 +9920,86 @@ class CustomRoleManager {
|
|
|
9920
9920
|
}
|
|
9921
9921
|
|
|
9922
9922
|
// src/forecast.ts
|
|
9923
|
+
var CACHE_READ_MULTIPLE = 0.1;
|
|
9924
|
+
var CACHE_WRITE_MULTIPLE = 1.25;
|
|
9925
|
+
function per1kPricing(input, output) {
|
|
9926
|
+
return {
|
|
9927
|
+
input,
|
|
9928
|
+
output,
|
|
9929
|
+
cacheRead: input * CACHE_READ_MULTIPLE,
|
|
9930
|
+
cacheWrite: input * CACHE_WRITE_MULTIPLE
|
|
9931
|
+
};
|
|
9932
|
+
}
|
|
9933
|
+
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)
|
|
9941
|
+
};
|
|
9942
|
+
var UNKNOWN_PRICING_PER_1K = per1kPricing(0.01, 0.05);
|
|
9943
|
+
var per1k = (rate, tokens) => rate * tokens / 1000;
|
|
9944
|
+
function bareModelId(ref) {
|
|
9945
|
+
return ref.slice(ref.indexOf("/") + 1);
|
|
9946
|
+
}
|
|
9947
|
+
function totalTokens(usage) {
|
|
9948
|
+
return usage.input + usage.output + usage.reasoning + usage.cache.read + usage.cache.write;
|
|
9949
|
+
}
|
|
9950
|
+
function priceTokens(usage, pricing) {
|
|
9951
|
+
const inputCost = per1k(pricing.input, usage.input);
|
|
9952
|
+
const outputCost = per1k(pricing.output, usage.output + usage.reasoning);
|
|
9953
|
+
const cacheCost = per1k(pricing.cacheRead, usage.cache.read) + per1k(pricing.cacheWrite, usage.cache.write);
|
|
9954
|
+
return { inputCost, outputCost, cacheCost, total: inputCost + outputCost + cacheCost };
|
|
9955
|
+
}
|
|
9956
|
+
|
|
9923
9957
|
class CostForecaster {
|
|
9924
|
-
|
|
9925
|
-
constructor() {
|
|
9926
|
-
this.
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9958
|
+
resolvePricing;
|
|
9959
|
+
constructor(pricing) {
|
|
9960
|
+
this.resolvePricing = pricing;
|
|
9961
|
+
}
|
|
9962
|
+
priceFor(model, provider) {
|
|
9963
|
+
const real = this.resolvePricing?.(model, provider);
|
|
9964
|
+
if (real)
|
|
9965
|
+
return { pricing: real, source: "model-costs" };
|
|
9966
|
+
const bare = bareModelId(model);
|
|
9967
|
+
const fallback = FALLBACK_PRICING_PER_1K[model] ?? FALLBACK_PRICING_PER_1K[bare];
|
|
9968
|
+
if (fallback)
|
|
9969
|
+
return { pricing: fallback, source: "fallback-table" };
|
|
9970
|
+
return { pricing: UNKNOWN_PRICING_PER_1K, source: "unknown-model" };
|
|
9971
|
+
}
|
|
9972
|
+
measureCost(usage, model, provider) {
|
|
9973
|
+
const { pricing, source } = this.priceFor(model, provider);
|
|
9974
|
+
return {
|
|
9975
|
+
cost: priceTokens(usage, pricing).total,
|
|
9976
|
+
tokens: totalTokens(usage),
|
|
9977
|
+
pricingSource: source,
|
|
9978
|
+
...source === "model-costs" ? { confidence: 1 } : {}
|
|
9979
|
+
};
|
|
9932
9980
|
}
|
|
9933
|
-
|
|
9934
|
-
this.
|
|
9981
|
+
costOf(usage, model, provider) {
|
|
9982
|
+
return priceTokens(usage, this.priceFor(model, provider).pricing).total;
|
|
9935
9983
|
}
|
|
9936
|
-
|
|
9984
|
+
estimateTokensFor(complexity, fileCount) {
|
|
9937
9985
|
const baseInput = 700;
|
|
9938
9986
|
const complexityMultiplier = 1 + complexity.overall / 100 * 3;
|
|
9939
|
-
const fileMultiplier = 1 +
|
|
9987
|
+
const fileMultiplier = 1 + fileCount * 0.2;
|
|
9940
9988
|
const input = Math.round(baseInput * complexityMultiplier * fileMultiplier);
|
|
9941
9989
|
const output = Math.round(input * 0.5);
|
|
9942
9990
|
return { input, output };
|
|
9943
9991
|
}
|
|
9992
|
+
estimateTokens(task, complexity) {
|
|
9993
|
+
return this.estimateTokensFor(complexity, task.files.include.length);
|
|
9994
|
+
}
|
|
9995
|
+
estimateCost(complexity, model, provider) {
|
|
9996
|
+
const { input, output } = this.estimateTokensFor(complexity, 0);
|
|
9997
|
+
return this.costOf({ input, output, reasoning: 0, cache: { read: 0, write: 0 } }, model, provider);
|
|
9998
|
+
}
|
|
9944
9999
|
forecastTask(task, role, modelId, complexity) {
|
|
9945
10000
|
const { input, output } = this.estimateTokens(task, complexity);
|
|
9946
|
-
const pricing = this.
|
|
9947
|
-
const
|
|
9948
|
-
const outputCost = output * pricing.output;
|
|
9949
|
-
const overhead = 0.0001;
|
|
10001
|
+
const { pricing, source } = this.priceFor(modelId);
|
|
10002
|
+
const breakdown = priceTokens({ input, output, reasoning: 0, cache: { read: 0, write: 0 } }, pricing);
|
|
9950
10003
|
return {
|
|
9951
10004
|
taskId: task.id,
|
|
9952
10005
|
taskName: task.name,
|
|
@@ -9954,9 +10007,15 @@ class CostForecaster {
|
|
|
9954
10007
|
model: modelId,
|
|
9955
10008
|
estimatedInputTokens: input,
|
|
9956
10009
|
estimatedOutputTokens: output,
|
|
9957
|
-
estimatedCost:
|
|
9958
|
-
|
|
9959
|
-
|
|
10010
|
+
estimatedCost: breakdown.total,
|
|
10011
|
+
source: "estimated",
|
|
10012
|
+
pricingSource: source,
|
|
10013
|
+
confidence: undefined,
|
|
10014
|
+
breakdown: {
|
|
10015
|
+
inputCost: breakdown.inputCost,
|
|
10016
|
+
outputCost: breakdown.outputCost,
|
|
10017
|
+
cacheCost: breakdown.cacheCost
|
|
10018
|
+
}
|
|
9960
10019
|
};
|
|
9961
10020
|
}
|
|
9962
10021
|
forecastAll(tasks, budgetRemaining) {
|
|
@@ -10164,6 +10223,12 @@ function lastAssistantText(messages) {
|
|
|
10164
10223
|
}
|
|
10165
10224
|
return "";
|
|
10166
10225
|
}
|
|
10226
|
+
function sumBy(records, select) {
|
|
10227
|
+
let total = 0;
|
|
10228
|
+
for (const record of records.values())
|
|
10229
|
+
total += select(record);
|
|
10230
|
+
return total;
|
|
10231
|
+
}
|
|
10167
10232
|
var DEFAULT_ESCALATION = {
|
|
10168
10233
|
maxRetries: 3,
|
|
10169
10234
|
retryDelay: 1000,
|
|
@@ -10209,6 +10274,8 @@ class NexusOrchestrator {
|
|
|
10209
10274
|
onStateChange = null;
|
|
10210
10275
|
stateChangeTimer = null;
|
|
10211
10276
|
costHistory = [];
|
|
10277
|
+
tokensByModel = new Map;
|
|
10278
|
+
costProvenance = new Map;
|
|
10212
10279
|
cleanupInterval = null;
|
|
10213
10280
|
get healthMonitor() {
|
|
10214
10281
|
return this._healthMonitor;
|
|
@@ -10226,6 +10293,7 @@ class NexusOrchestrator {
|
|
|
10226
10293
|
this.messageRouter = new MessageRouter;
|
|
10227
10294
|
this.escalationPolicy = {
|
|
10228
10295
|
...DEFAULT_ESCALATION,
|
|
10296
|
+
fallbackModels: [...DEFAULT_ESCALATION.fallbackModels],
|
|
10229
10297
|
maxRetries: this.config.selfHealing.maxRetries,
|
|
10230
10298
|
retryDelay: this.config.selfHealing.retryDelay,
|
|
10231
10299
|
enableRespawn: this.config.selfHealing.contextTransfer
|
|
@@ -10235,7 +10303,7 @@ class NexusOrchestrator {
|
|
|
10235
10303
|
this.performanceTracker = new PerformanceTracker;
|
|
10236
10304
|
this.executionHistory = new ExecutionHistory;
|
|
10237
10305
|
this.customRoles = new CustomRoleManager;
|
|
10238
|
-
this.forecaster = new CostForecaster;
|
|
10306
|
+
this.forecaster = new CostForecaster((model, provider) => this.getModelCost(model, provider));
|
|
10239
10307
|
}
|
|
10240
10308
|
async initialize(ctx, onStateChange) {
|
|
10241
10309
|
this.ctx = ctx;
|
|
@@ -10590,14 +10658,17 @@ class NexusOrchestrator {
|
|
|
10590
10658
|
await this.sleep(this.config.schedulerInterval);
|
|
10591
10659
|
}
|
|
10592
10660
|
}
|
|
10593
|
-
|
|
10661
|
+
selectQualifiedModel(node) {
|
|
10594
10662
|
const complexity = this.analyzeComplexity(node.task);
|
|
10595
10663
|
const model = this.selectModel(node.task.requiredRole, complexity);
|
|
10596
10664
|
if (!model.provider || !model.model) {
|
|
10597
10665
|
const missing = !model.provider && !model.model ? "provider and model" : !model.provider ? "provider" : "model";
|
|
10598
10666
|
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
10667
|
}
|
|
10600
|
-
|
|
10668
|
+
return `${model.provider}/${model.model}`;
|
|
10669
|
+
}
|
|
10670
|
+
async spawnAndExecute(node, options = {}) {
|
|
10671
|
+
const qualifiedModel = options.modelOverride ?? this.selectQualifiedModel(node);
|
|
10601
10672
|
const agent = await this.spawnAgent({
|
|
10602
10673
|
role: node.task.requiredRole,
|
|
10603
10674
|
task: node.task,
|
|
@@ -10607,7 +10678,7 @@ class NexusOrchestrator {
|
|
|
10607
10678
|
node.status = "running";
|
|
10608
10679
|
node.task.assignedAgent = agent.id;
|
|
10609
10680
|
this.notifyStateChange();
|
|
10610
|
-
await this.executeTask(agent, node, transferContext);
|
|
10681
|
+
await this.executeTask(agent, node, options.transferContext);
|
|
10611
10682
|
}
|
|
10612
10683
|
async executeTask(agent, node, transferContext) {
|
|
10613
10684
|
if (!this.ctx || !agent.sessionID) {
|
|
@@ -10661,20 +10732,21 @@ Please continue from where the previous agent left off.`;
|
|
|
10661
10732
|
const messages = await this.ctx.session.context({ sessionID: agent.sessionID });
|
|
10662
10733
|
const output = lastAssistantText(messages) || "Task completed (no output captured)";
|
|
10663
10734
|
const duration = Date.now() - startTime;
|
|
10735
|
+
const cost = await this.safeAccountTaskCost(agent, node.task);
|
|
10664
10736
|
const result = {
|
|
10665
10737
|
success: true,
|
|
10666
10738
|
output,
|
|
10667
10739
|
duration,
|
|
10668
|
-
tokensUsed:
|
|
10669
|
-
cost:
|
|
10740
|
+
tokensUsed: cost.tokensUsed,
|
|
10741
|
+
cost: cost.cost,
|
|
10742
|
+
costProvenance: cost.provenance
|
|
10670
10743
|
};
|
|
10671
10744
|
this.dag.markComplete(node.id, result);
|
|
10672
10745
|
this.todoEnforcer.completeTask(agent.id);
|
|
10673
10746
|
agent.metrics.tasksCompleted++;
|
|
10674
10747
|
agent.metrics.totalCost += result.cost;
|
|
10675
|
-
|
|
10676
|
-
this.
|
|
10677
|
-
this.checkBudget();
|
|
10748
|
+
agent.metrics.totalTokens += result.tokensUsed;
|
|
10749
|
+
this.trackCost(agent.id, `${agent.model.provider}/${agent.model.model}`, result.cost, result.tokensUsed, result.costProvenance);
|
|
10678
10750
|
this.performanceTracker.record({
|
|
10679
10751
|
model: agent.model.model,
|
|
10680
10752
|
role: node.task.requiredRole,
|
|
@@ -10716,14 +10788,20 @@ Please continue from where the previous agent left off.`;
|
|
|
10716
10788
|
} catch (error) {
|
|
10717
10789
|
const duration = Date.now() - startTime;
|
|
10718
10790
|
const errorMessage = error.message || "Task failed";
|
|
10791
|
+
const cost = await this.safeAccountTaskCost(agent, node.task);
|
|
10719
10792
|
const result = {
|
|
10720
10793
|
success: false,
|
|
10721
10794
|
error: errorMessage,
|
|
10722
10795
|
duration,
|
|
10723
|
-
tokensUsed:
|
|
10724
|
-
cost:
|
|
10796
|
+
tokensUsed: cost.tokensUsed,
|
|
10797
|
+
cost: cost.cost,
|
|
10798
|
+
costProvenance: cost.provenance
|
|
10725
10799
|
};
|
|
10726
10800
|
this.dag.markFailed(node.id, new Error(errorMessage));
|
|
10801
|
+
node.result = result;
|
|
10802
|
+
this.trackCost(agent.id, `${agent.model.provider}/${agent.model.model}`, result.cost, result.tokensUsed, result.costProvenance);
|
|
10803
|
+
agent.metrics.totalCost += result.cost;
|
|
10804
|
+
agent.metrics.totalTokens += result.tokensUsed;
|
|
10727
10805
|
this.todoEnforcer.completeTask(agent.id);
|
|
10728
10806
|
agent.metrics.tasksFailed++;
|
|
10729
10807
|
agent.status = "failed";
|
|
@@ -10834,7 +10912,7 @@ Please continue from where the previous agent left off.`;
|
|
|
10834
10912
|
await this.terminateAgent(agent.id);
|
|
10835
10913
|
node.status = "pending";
|
|
10836
10914
|
this.notifyStateChange();
|
|
10837
|
-
await this.spawnAndExecute(node, context);
|
|
10915
|
+
await this.spawnAndExecute(node, { transferContext: context });
|
|
10838
10916
|
return;
|
|
10839
10917
|
}
|
|
10840
10918
|
if (policy.fallbackModels.length > 0) {
|
|
@@ -10843,7 +10921,7 @@ Please continue from where the previous agent left off.`;
|
|
|
10843
10921
|
await this.terminateAgent(agent.id);
|
|
10844
10922
|
node.status = "pending";
|
|
10845
10923
|
this.notifyStateChange();
|
|
10846
|
-
await this.
|
|
10924
|
+
await this.spawnAndExecute(node, { modelOverride: fallbackModel });
|
|
10847
10925
|
return;
|
|
10848
10926
|
}
|
|
10849
10927
|
}
|
|
@@ -11077,10 +11155,10 @@ Please continue from where the previous agent left off.`;
|
|
|
11077
11155
|
if (qualified)
|
|
11078
11156
|
return qualified;
|
|
11079
11157
|
}
|
|
11080
|
-
const bare =
|
|
11158
|
+
const bare = bareModelId(model);
|
|
11081
11159
|
let best;
|
|
11082
11160
|
for (const [key, cost] of this.modelCosts) {
|
|
11083
|
-
if (
|
|
11161
|
+
if (bareModelId(key) !== bare)
|
|
11084
11162
|
continue;
|
|
11085
11163
|
if (!best || cost.input < best.input)
|
|
11086
11164
|
best = cost;
|
|
@@ -11162,28 +11240,96 @@ Please continue from where the previous agent left off.`;
|
|
|
11162
11240
|
const scored = unique.map((m) => this.scoreModel(m, role, complexity));
|
|
11163
11241
|
const budgetRemaining = this.budget.maxTotalCost - this.totalSpent;
|
|
11164
11242
|
const affordable = scored.filter((s) => {
|
|
11165
|
-
const
|
|
11166
|
-
return
|
|
11243
|
+
const estimate = this.forecaster.estimateCost(complexity, s.model, s.provider);
|
|
11244
|
+
return estimate <= budgetRemaining || estimate === 0;
|
|
11167
11245
|
});
|
|
11168
11246
|
const best = (affordable.length > 0 ? affordable : scored).sort((a, b) => b.overallScore - a.overallScore)[0];
|
|
11169
11247
|
return {
|
|
11170
11248
|
provider: best.provider,
|
|
11171
11249
|
model: best.model,
|
|
11172
|
-
estimatedCost: this.
|
|
11250
|
+
estimatedCost: this.forecaster.estimateCost(complexity, best.model, best.provider),
|
|
11173
11251
|
estimatedQuality: best.qualityScore,
|
|
11174
11252
|
reasoning: best.reasoning
|
|
11175
11253
|
};
|
|
11176
11254
|
}
|
|
11177
|
-
trackCost(agentId, model, cost, tokens) {
|
|
11255
|
+
trackCost(agentId, model, cost, tokens, provenance) {
|
|
11178
11256
|
this.totalSpent += cost;
|
|
11179
11257
|
const agentCost = this.costByAgent.get(agentId) || 0;
|
|
11180
11258
|
this.costByAgent.set(agentId, agentCost + cost);
|
|
11181
11259
|
const modelCost = this.costByModel.get(model) || 0;
|
|
11182
11260
|
this.costByModel.set(model, modelCost + cost);
|
|
11183
|
-
this.
|
|
11261
|
+
this.tokensByModel.set(model, (this.tokensByModel.get(model) || 0) + tokens);
|
|
11262
|
+
this.recordProvenance(model, cost, provenance);
|
|
11263
|
+
this.costHistory.push({ timestamp: Date.now(), cost, agentId, model, tokens, provenance });
|
|
11184
11264
|
this.checkBudget();
|
|
11185
11265
|
this.notifyStateChange();
|
|
11186
11266
|
}
|
|
11267
|
+
recordProvenance(model, cost, provenance) {
|
|
11268
|
+
const measured = provenance.usage === "measured";
|
|
11269
|
+
const prior = this.costProvenance.get(model) ?? {
|
|
11270
|
+
usage: provenance.usage,
|
|
11271
|
+
pricing: provenance.pricing,
|
|
11272
|
+
measuredEntries: 0,
|
|
11273
|
+
estimatedEntries: 0,
|
|
11274
|
+
measuredSpend: 0,
|
|
11275
|
+
estimatedSpend: 0
|
|
11276
|
+
};
|
|
11277
|
+
prior.usage = provenance.usage;
|
|
11278
|
+
prior.pricing = provenance.pricing;
|
|
11279
|
+
if (measured) {
|
|
11280
|
+
prior.measuredEntries++;
|
|
11281
|
+
prior.measuredSpend += cost;
|
|
11282
|
+
} else {
|
|
11283
|
+
prior.estimatedEntries++;
|
|
11284
|
+
prior.estimatedSpend += cost;
|
|
11285
|
+
}
|
|
11286
|
+
this.costProvenance.set(model, prior);
|
|
11287
|
+
}
|
|
11288
|
+
safeAccountTaskCost(agent, task) {
|
|
11289
|
+
return this.accountTaskCost(agent, task).catch(() => ({
|
|
11290
|
+
cost: 0,
|
|
11291
|
+
tokensUsed: 0,
|
|
11292
|
+
provenance: { usage: "estimated", pricing: "unknown-model" }
|
|
11293
|
+
}));
|
|
11294
|
+
}
|
|
11295
|
+
async accountTaskCost(agent, task) {
|
|
11296
|
+
const model = `${agent.model.provider}/${agent.model.model}`;
|
|
11297
|
+
const read = agent.sessionID ? await this.readSessionTokens(agent.sessionID) : { read: false };
|
|
11298
|
+
if (read.read) {
|
|
11299
|
+
const measured = this.forecaster.measureCost(read.usage, model, agent.model.provider);
|
|
11300
|
+
return {
|
|
11301
|
+
cost: measured.cost,
|
|
11302
|
+
tokensUsed: measured.tokens,
|
|
11303
|
+
provenance: { usage: "measured", pricing: measured.pricingSource }
|
|
11304
|
+
};
|
|
11305
|
+
}
|
|
11306
|
+
const predicted = this.forecaster.forecastTask(task, task.requiredRole, model, task.complexity);
|
|
11307
|
+
return {
|
|
11308
|
+
cost: predicted.estimatedCost,
|
|
11309
|
+
tokensUsed: predicted.estimatedInputTokens + predicted.estimatedOutputTokens,
|
|
11310
|
+
provenance: { usage: "estimated", pricing: predicted.pricingSource }
|
|
11311
|
+
};
|
|
11312
|
+
}
|
|
11313
|
+
async readSessionTokens(sessionID) {
|
|
11314
|
+
const finite = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
11315
|
+
try {
|
|
11316
|
+
const session = await this.ctx?.session.get({ sessionID });
|
|
11317
|
+
const tokens = session?.tokens;
|
|
11318
|
+
if (!tokens)
|
|
11319
|
+
return { read: false };
|
|
11320
|
+
return {
|
|
11321
|
+
read: true,
|
|
11322
|
+
usage: {
|
|
11323
|
+
input: finite(tokens.input),
|
|
11324
|
+
output: finite(tokens.output),
|
|
11325
|
+
reasoning: finite(tokens.reasoning),
|
|
11326
|
+
cache: { read: finite(tokens.cache?.read), write: finite(tokens.cache?.write) }
|
|
11327
|
+
}
|
|
11328
|
+
};
|
|
11329
|
+
} catch {
|
|
11330
|
+
return { read: false };
|
|
11331
|
+
}
|
|
11332
|
+
}
|
|
11187
11333
|
checkBudget() {
|
|
11188
11334
|
const remaining = this.budget.maxTotalCost - this.totalSpent;
|
|
11189
11335
|
const remainingPercent = remaining / this.budget.maxTotalCost;
|
|
@@ -11274,7 +11420,11 @@ Please continue from where the previous agent left off.`;
|
|
|
11274
11420
|
totalSpent: state.totalSpent,
|
|
11275
11421
|
budgetRemaining: state.budgetRemaining,
|
|
11276
11422
|
byAgent: Object.fromEntries(this.costByAgent),
|
|
11277
|
-
byModel: Object.fromEntries(this.costByModel)
|
|
11423
|
+
byModel: Object.fromEntries(this.costByModel),
|
|
11424
|
+
tokensByModel: Object.fromEntries(this.tokensByModel),
|
|
11425
|
+
provenance: Object.fromEntries(this.costProvenance),
|
|
11426
|
+
measuredEntries: sumBy(this.costProvenance, (p) => p.measuredEntries),
|
|
11427
|
+
estimatedEntries: sumBy(this.costProvenance, (p) => p.estimatedEntries)
|
|
11278
11428
|
}, null, 2);
|
|
11279
11429
|
}
|
|
11280
11430
|
pause() {
|
package/dist/tui.js
CHANGED
|
@@ -454,6 +454,78 @@ var PRESETS = {
|
|
|
454
454
|
|
|
455
455
|
// src/tui.tsx
|
|
456
456
|
import { jsxDEV, Fragment } from "@opentui/solid/jsx-dev-runtime";
|
|
457
|
+
var NEXUS_AGENT_PREFIX = "nexus-";
|
|
458
|
+
function metadataString(metadata, key) {
|
|
459
|
+
const value = metadata?.[key];
|
|
460
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
461
|
+
}
|
|
462
|
+
function roleFromAgent(agent) {
|
|
463
|
+
if (!agent)
|
|
464
|
+
return;
|
|
465
|
+
const role = agent.startsWith(NEXUS_AGENT_PREFIX) ? agent.slice(NEXUS_AGENT_PREFIX.length) : agent;
|
|
466
|
+
return role.length > 0 ? role : undefined;
|
|
467
|
+
}
|
|
468
|
+
function modelLabel(model) {
|
|
469
|
+
if (!model?.providerID || !model.id)
|
|
470
|
+
return;
|
|
471
|
+
return `${model.providerID}/${model.id}`;
|
|
472
|
+
}
|
|
473
|
+
function titleCase(value) {
|
|
474
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
475
|
+
}
|
|
476
|
+
function sidebarAgentFor(session, status, now) {
|
|
477
|
+
const role = metadataString(session.metadata, "nexusRole") ?? roleFromAgent(session.agent);
|
|
478
|
+
return {
|
|
479
|
+
id: session.id,
|
|
480
|
+
name: role ? titleCase(role) : session.title || session.id.slice(0, 12),
|
|
481
|
+
role: role ?? "agent",
|
|
482
|
+
status,
|
|
483
|
+
model: metadataString(session.metadata, "nexusModel") ?? modelLabel(session.model) ?? "",
|
|
484
|
+
sessionID: session.id,
|
|
485
|
+
spawnedAt: now,
|
|
486
|
+
tasksCompleted: 0,
|
|
487
|
+
tasksFailed: 0
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function sidebarChildSessionIDs(family, currentSessionID) {
|
|
491
|
+
if (!Array.isArray(family))
|
|
492
|
+
return [];
|
|
493
|
+
return family.filter((id) => id !== currentSessionID);
|
|
494
|
+
}
|
|
495
|
+
function collectSidebarAgents(source, currentSessionID, statusOf, now) {
|
|
496
|
+
const childIDs = sidebarChildSessionIDs(source.family(currentSessionID), currentSessionID);
|
|
497
|
+
if (childIDs.length === 0)
|
|
498
|
+
return [];
|
|
499
|
+
const agents = [];
|
|
500
|
+
for (const id of childIDs) {
|
|
501
|
+
try {
|
|
502
|
+
const session = source.get(id);
|
|
503
|
+
if (!session)
|
|
504
|
+
continue;
|
|
505
|
+
let status = "completed";
|
|
506
|
+
try {
|
|
507
|
+
status = statusOf(id);
|
|
508
|
+
} catch {}
|
|
509
|
+
agents.push(sidebarAgentFor(session, status, now));
|
|
510
|
+
} catch {}
|
|
511
|
+
}
|
|
512
|
+
return agents;
|
|
513
|
+
}
|
|
514
|
+
function mergeSidebarAgents(agents, polled, childIDs) {
|
|
515
|
+
const next = agents.map((agent) => ({ ...agent }));
|
|
516
|
+
for (const agent of polled) {
|
|
517
|
+
const existing = next.find((a) => a.sessionID === agent.sessionID);
|
|
518
|
+
if (existing) {
|
|
519
|
+
existing.status = agent.status;
|
|
520
|
+
existing.name = agent.name;
|
|
521
|
+
existing.role = agent.role;
|
|
522
|
+
existing.model = agent.model;
|
|
523
|
+
} else {
|
|
524
|
+
next.push(agent);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return next.filter((a) => a.sessionID && childIDs.includes(a.sessionID) || a.status === "completed" || a.status === "failed");
|
|
528
|
+
}
|
|
457
529
|
var tui_default = define({
|
|
458
530
|
id: "nexus.cli",
|
|
459
531
|
setup(context) {
|
|
@@ -848,69 +920,20 @@ var tui_default = define({
|
|
|
848
920
|
const currentSessionID = currentRoute.sessionID;
|
|
849
921
|
if (!currentSessionID)
|
|
850
922
|
return;
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
return;
|
|
854
|
-
const childIDs = family.filter((id) => id !== currentSessionID);
|
|
855
|
-
try {
|
|
856
|
-
const allSessions = context.data.session.list();
|
|
857
|
-
if (allSessions && Array.isArray(allSessions)) {
|
|
858
|
-
for (const s of allSessions) {
|
|
859
|
-
if (!s || !s.id)
|
|
860
|
-
continue;
|
|
861
|
-
const meta = s.metadata;
|
|
862
|
-
if (meta?.nexusRole && !childIDs.includes(s.id) && s.id !== currentSessionID) {
|
|
863
|
-
childIDs.push(s.id);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
} catch {}
|
|
923
|
+
const source = context.data.session;
|
|
924
|
+
const childIDs = sidebarChildSessionIDs(source.family(currentSessionID), currentSessionID);
|
|
868
925
|
if (childIDs.length === 0) {
|
|
869
926
|
setSidebarState((draft) => {
|
|
870
|
-
draft.agents = draft.agents
|
|
927
|
+
draft.agents = mergeSidebarAgents(draft.agents, [], []);
|
|
871
928
|
});
|
|
872
929
|
return;
|
|
873
930
|
}
|
|
874
|
-
const newAgents =
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
let status = "completed";
|
|
881
|
-
try {
|
|
882
|
-
status = context.data.session.status(id) === "running" ? "working" : "completed";
|
|
883
|
-
} catch {}
|
|
884
|
-
return {
|
|
885
|
-
id,
|
|
886
|
-
name: meta.nexusRole ? `${meta.nexusRole.charAt(0).toUpperCase() + meta.nexusRole.slice(1)}` : session?.title || id.slice(0, 12),
|
|
887
|
-
role: meta.nexusRole || "agent",
|
|
888
|
-
status,
|
|
889
|
-
model: meta.nexusModel || "",
|
|
890
|
-
sessionID: id,
|
|
891
|
-
spawnedAt: new Date().toISOString(),
|
|
892
|
-
tasksCompleted: 0,
|
|
893
|
-
tasksFailed: 0
|
|
894
|
-
};
|
|
895
|
-
} catch {
|
|
896
|
-
return null;
|
|
897
|
-
}
|
|
898
|
-
}).filter(Boolean);
|
|
899
|
-
if (newAgents.length > 0) {
|
|
900
|
-
setSidebarState((draft) => {
|
|
901
|
-
for (const agent of newAgents) {
|
|
902
|
-
const existing = draft.agents.find((a) => a.sessionID === agent.sessionID);
|
|
903
|
-
if (existing) {
|
|
904
|
-
existing.status = agent.status;
|
|
905
|
-
existing.name = agent.name;
|
|
906
|
-
existing.model = agent.model;
|
|
907
|
-
} else {
|
|
908
|
-
draft.agents.push(agent);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
draft.agents = draft.agents.filter((a) => a.sessionID && childIDs.includes(a.sessionID) || a.status === "completed" || a.status === "failed");
|
|
912
|
-
});
|
|
913
|
-
}
|
|
931
|
+
const newAgents = collectSidebarAgents(source, currentSessionID, (id) => context.data.session.status(id) === "running" ? "working" : "completed", new Date().toISOString());
|
|
932
|
+
if (newAgents.length === 0)
|
|
933
|
+
return;
|
|
934
|
+
setSidebarState((draft) => {
|
|
935
|
+
draft.agents = mergeSidebarAgents(draft.agents, newAgents, childIDs);
|
|
936
|
+
});
|
|
914
937
|
} catch {}
|
|
915
938
|
};
|
|
916
939
|
const unsubSessionSucceeded = context.data.on("session.execution.succeeded", (event) => {
|
|
@@ -1039,5 +1062,9 @@ var tui_default = define({
|
|
|
1039
1062
|
}
|
|
1040
1063
|
});
|
|
1041
1064
|
export {
|
|
1042
|
-
|
|
1065
|
+
collectSidebarAgents,
|
|
1066
|
+
tui_default as default,
|
|
1067
|
+
mergeSidebarAgents,
|
|
1068
|
+
sidebarAgentFor,
|
|
1069
|
+
sidebarChildSessionIDs
|
|
1043
1070
|
};
|