@serkanalgur/opencode-nexus 2.3.4 → 2.4.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 +276 -118
- package/dist/tui.js +19 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8486,7 +8486,11 @@ class NexusConfigManager {
|
|
|
8486
8486
|
}
|
|
8487
8487
|
getModelForRole(role) {
|
|
8488
8488
|
const config = this.getConfig();
|
|
8489
|
-
|
|
8489
|
+
const model = config.models[role] || config.models.coder || DEFAULT_CONFIG.models.coder;
|
|
8490
|
+
if (!model.includes("/")) {
|
|
8491
|
+
console.warn(`[nexus] Model "${model}" for role "${role}" is missing provider prefix. Expected "providerID/modelID" format.`);
|
|
8492
|
+
}
|
|
8493
|
+
return model;
|
|
8490
8494
|
}
|
|
8491
8495
|
updateStorageConfig(update) {
|
|
8492
8496
|
this.storageConfig = {
|
|
@@ -8570,10 +8574,18 @@ class NexusConfigManager {
|
|
|
8570
8574
|
}
|
|
8571
8575
|
initProjectConfig(basePath) {
|
|
8572
8576
|
const projectPath = join(basePath, ".opencode", "nexus.jsonc");
|
|
8577
|
+
try {
|
|
8578
|
+
readFileSync(projectPath, "utf-8");
|
|
8579
|
+
return;
|
|
8580
|
+
} catch {}
|
|
8573
8581
|
this.writeJsoncFile(projectPath, { ...DEFAULT_CONFIG });
|
|
8574
8582
|
}
|
|
8575
8583
|
initGlobalConfig() {
|
|
8576
8584
|
const globalPath = join(homedir(), ".config", "opencode", "nexus.jsonc");
|
|
8585
|
+
try {
|
|
8586
|
+
readFileSync(globalPath, "utf-8");
|
|
8587
|
+
return;
|
|
8588
|
+
} catch {}
|
|
8577
8589
|
this.writeJsoncFile(globalPath, { ...DEFAULT_CONFIG });
|
|
8578
8590
|
}
|
|
8579
8591
|
saveConfig(level, basePath) {
|
|
@@ -10073,6 +10085,21 @@ ${lines.join(`
|
|
|
10073
10085
|
}
|
|
10074
10086
|
|
|
10075
10087
|
// src/orchestrator.ts
|
|
10088
|
+
function assistantMessageText(message) {
|
|
10089
|
+
if (message.type !== "assistant")
|
|
10090
|
+
return "";
|
|
10091
|
+
return message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
10092
|
+
}
|
|
10093
|
+
function lastAssistantText(messages) {
|
|
10094
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
10095
|
+
if (messages[i].type !== "assistant")
|
|
10096
|
+
continue;
|
|
10097
|
+
const text = assistantMessageText(messages[i]);
|
|
10098
|
+
if (text)
|
|
10099
|
+
return text;
|
|
10100
|
+
}
|
|
10101
|
+
return "";
|
|
10102
|
+
}
|
|
10076
10103
|
var DEFAULT_ESCALATION = {
|
|
10077
10104
|
maxRetries: 3,
|
|
10078
10105
|
retryDelay: 1000,
|
|
@@ -10124,7 +10151,7 @@ class NexusOrchestrator {
|
|
|
10124
10151
|
}
|
|
10125
10152
|
notifications = null;
|
|
10126
10153
|
modelCosts = new Map;
|
|
10127
|
-
|
|
10154
|
+
lastDegradedSpawn = null;
|
|
10128
10155
|
constructor(config, messageStoreConfig, memoryStoreConfig) {
|
|
10129
10156
|
this.config = this.mergeConfig(config);
|
|
10130
10157
|
this.budget = this.config.budget;
|
|
@@ -10149,7 +10176,7 @@ class NexusOrchestrator {
|
|
|
10149
10176
|
async initialize(ctx, onStateChange) {
|
|
10150
10177
|
this.ctx = ctx;
|
|
10151
10178
|
this.onStateChange = onStateChange ?? null;
|
|
10152
|
-
const projectDir = ctx.location
|
|
10179
|
+
const projectDir = ctx.location.directory;
|
|
10153
10180
|
this.configManager.loadFromPath(projectDir);
|
|
10154
10181
|
await this.loadModelCosts();
|
|
10155
10182
|
this.cleanupInterval = setInterval(() => this.cleanupStaleData(), 300000);
|
|
@@ -10169,41 +10196,22 @@ class NexusOrchestrator {
|
|
|
10169
10196
|
}
|
|
10170
10197
|
async loadModelCosts() {
|
|
10171
10198
|
try {
|
|
10172
|
-
if (!this.ctx)
|
|
10199
|
+
if (!this.ctx?.model)
|
|
10173
10200
|
return;
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
const models = result?.data?.data ?? result?.data ?? [];
|
|
10177
|
-
if (Array.isArray(models)) {
|
|
10178
|
-
for (const model of models) {
|
|
10179
|
-
if (model.cost && Array.isArray(model.cost) && model.cost.length > 0) {
|
|
10180
|
-
const baseCost = model.cost[0];
|
|
10181
|
-
this.modelCosts.set(model.id, {
|
|
10182
|
-
input: baseCost.input || 0,
|
|
10183
|
-
output: baseCost.output || 0,
|
|
10184
|
-
cacheRead: baseCost.cache?.read || 0,
|
|
10185
|
-
cacheWrite: baseCost.cache?.write || 0
|
|
10186
|
-
});
|
|
10187
|
-
}
|
|
10188
|
-
}
|
|
10189
|
-
}
|
|
10201
|
+
const { data } = await this.ctx.model.list();
|
|
10202
|
+
if (!Array.isArray(data) || data.length === 0)
|
|
10190
10203
|
return;
|
|
10191
|
-
|
|
10192
|
-
const
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
const
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
cacheRead: baseCost.cache?.read || 0,
|
|
10203
|
-
cacheWrite: baseCost.cache?.write || 0
|
|
10204
|
-
});
|
|
10205
|
-
}
|
|
10206
|
-
}
|
|
10204
|
+
const per1k = (v) => (v || 0) / 1000;
|
|
10205
|
+
for (const model of data) {
|
|
10206
|
+
if (!model?.cost || !Array.isArray(model.cost) || model.cost.length === 0)
|
|
10207
|
+
continue;
|
|
10208
|
+
const baseCost = model.cost[0];
|
|
10209
|
+
this.modelCosts.set(`${model.providerID}/${model.id}`, {
|
|
10210
|
+
input: per1k(baseCost.input),
|
|
10211
|
+
output: per1k(baseCost.output),
|
|
10212
|
+
cacheRead: per1k(baseCost.cache?.read),
|
|
10213
|
+
cacheWrite: per1k(baseCost.cache?.write)
|
|
10214
|
+
});
|
|
10207
10215
|
}
|
|
10208
10216
|
} catch {}
|
|
10209
10217
|
}
|
|
@@ -10552,15 +10560,14 @@ Please continue from where the previous agent left off.`;
|
|
|
10552
10560
|
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Task timed out")), timeout));
|
|
10553
10561
|
await Promise.race([waitPromise, timeoutPromise]);
|
|
10554
10562
|
const messages = await this.ctx.session.context({ sessionID: agent.sessionID });
|
|
10555
|
-
const
|
|
10556
|
-
const output = lastAssistantMsg?.content || "Task completed";
|
|
10563
|
+
const output = lastAssistantText(messages) || "Task completed (no output captured)";
|
|
10557
10564
|
const duration = Date.now() - startTime;
|
|
10558
10565
|
const result = {
|
|
10559
10566
|
success: true,
|
|
10560
|
-
output
|
|
10567
|
+
output,
|
|
10561
10568
|
duration,
|
|
10562
10569
|
tokensUsed: 0,
|
|
10563
|
-
cost: this.estimateModelCost(agent.model.model)
|
|
10570
|
+
cost: this.estimateModelCost(agent.model.model, agent.model.provider)
|
|
10564
10571
|
};
|
|
10565
10572
|
this.dag.markComplete(node.id, result);
|
|
10566
10573
|
this.todoEnforcer.completeTask(agent.id);
|
|
@@ -10596,7 +10603,7 @@ Please continue from where the previous agent left off.`;
|
|
|
10596
10603
|
if (priorPattern.length > 0) {
|
|
10597
10604
|
this.learning.recordSuccess(priorPattern[0].entry.id);
|
|
10598
10605
|
}
|
|
10599
|
-
if (
|
|
10606
|
+
if (output.length > 0) {
|
|
10600
10607
|
const securityIssues = this.securityScanner.scanContent(output, node.task.name);
|
|
10601
10608
|
if (securityIssues.length > 0) {
|
|
10602
10609
|
this.emit("security:issues-found", {
|
|
@@ -10757,7 +10764,57 @@ Please continue from where the previous agent left off.`;
|
|
|
10757
10764
|
};
|
|
10758
10765
|
this.notifyStateChange();
|
|
10759
10766
|
}
|
|
10760
|
-
async
|
|
10767
|
+
async createChildSession(params) {
|
|
10768
|
+
const { tool, agent, description, prompt, model, parent, callID } = params;
|
|
10769
|
+
if (!parent.sessionID) {
|
|
10770
|
+
throw new Error("Cannot spawn agent without a parent session ID");
|
|
10771
|
+
}
|
|
10772
|
+
if (!parent.agent) {
|
|
10773
|
+
throw new Error("Cannot spawn agent without the calling agent id (tool context has no 'agent')");
|
|
10774
|
+
}
|
|
10775
|
+
let childSessionID;
|
|
10776
|
+
let resolveChildSession = null;
|
|
10777
|
+
let watchdog;
|
|
10778
|
+
const childSessionReady = new Promise((resolve, reject) => {
|
|
10779
|
+
resolveChildSession = resolve;
|
|
10780
|
+
watchdog = setTimeout(() => reject(new Error(`subagent tool did not report a child session for ${agent}`)), 30000);
|
|
10781
|
+
});
|
|
10782
|
+
childSessionReady.catch(() => {});
|
|
10783
|
+
const reportProgress = (p) => {
|
|
10784
|
+
if (p?.sessionID && !childSessionID) {
|
|
10785
|
+
childSessionID = p.sessionID;
|
|
10786
|
+
resolveChildSession?.(p.sessionID);
|
|
10787
|
+
}
|
|
10788
|
+
return Promise.resolve();
|
|
10789
|
+
};
|
|
10790
|
+
const toolContext = {
|
|
10791
|
+
sessionID: parent.sessionID,
|
|
10792
|
+
agent: parent.agent,
|
|
10793
|
+
messageID: parent.messageID || `msg_${callID}`,
|
|
10794
|
+
id: `call_${callID}`,
|
|
10795
|
+
progress: reportProgress,
|
|
10796
|
+
signal: parent.signal ?? new AbortController().signal
|
|
10797
|
+
};
|
|
10798
|
+
const subagentCall = tool.execute({ agent, description, prompt, model, background: true }, toolContext);
|
|
10799
|
+
let childSessionIDResolved;
|
|
10800
|
+
try {
|
|
10801
|
+
childSessionIDResolved = await Promise.race([
|
|
10802
|
+
childSessionReady,
|
|
10803
|
+
subagentCall.then(() => {
|
|
10804
|
+
throw new Error(`subagent tool finished without reporting a child session for ${agent}`);
|
|
10805
|
+
}, (err) => {
|
|
10806
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10807
|
+
throw new Error(`subagent tool failed for ${agent}: ${message}`);
|
|
10808
|
+
})
|
|
10809
|
+
]);
|
|
10810
|
+
} finally {
|
|
10811
|
+
if (watchdog)
|
|
10812
|
+
clearTimeout(watchdog);
|
|
10813
|
+
}
|
|
10814
|
+
subagentCall.catch(() => {});
|
|
10815
|
+
return childSessionIDResolved;
|
|
10816
|
+
}
|
|
10817
|
+
async spawnAgent(config, options) {
|
|
10761
10818
|
if (!this.ctx) {
|
|
10762
10819
|
throw new Error("Orchestrator not initialized");
|
|
10763
10820
|
}
|
|
@@ -10765,33 +10822,74 @@ Please continue from where the previous agent left off.`;
|
|
|
10765
10822
|
throw new Error("Budget exceeded — cannot spawn new agents");
|
|
10766
10823
|
}
|
|
10767
10824
|
const agentId = `agent-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
10768
|
-
|
|
10825
|
+
let modelConfig = config.model || this.configManager.getModelForRole(config.role);
|
|
10826
|
+
if (!modelConfig.includes("/")) {
|
|
10827
|
+
const allModels = this.configManager.getConfig().models;
|
|
10828
|
+
const match = Object.values(allModels).find((m) => m?.split("/")[1] === modelConfig);
|
|
10829
|
+
if (match) {
|
|
10830
|
+
modelConfig = match;
|
|
10831
|
+
} else {
|
|
10832
|
+
throw new Error(`Invalid model "${config.model}". Use "providerID/modelID" format (e.g. "opencode-go/mimo-v2.5")`);
|
|
10833
|
+
}
|
|
10834
|
+
}
|
|
10769
10835
|
const slashIndex = modelConfig.indexOf("/");
|
|
10770
|
-
const provider =
|
|
10771
|
-
const modelName =
|
|
10836
|
+
const provider = modelConfig.slice(0, slashIndex);
|
|
10837
|
+
const modelName = modelConfig.slice(slashIndex + 1);
|
|
10772
10838
|
const roleEmoji = this.configManager.getRoleEmoji(config.role);
|
|
10773
10839
|
const title = `${roleEmoji} ${this.configManager.getRoleDisplayName(config.role)} — ${modelConfig}`;
|
|
10774
10840
|
const agentTypeMap = {
|
|
10775
|
-
architect: "architect",
|
|
10776
|
-
coder: "
|
|
10777
|
-
reviewer: "
|
|
10778
|
-
tester: "
|
|
10779
|
-
explorer: "
|
|
10780
|
-
documenter: "
|
|
10841
|
+
architect: "nexus-architect",
|
|
10842
|
+
coder: "nexus-coder",
|
|
10843
|
+
reviewer: "nexus-reviewer",
|
|
10844
|
+
tester: "nexus-tester",
|
|
10845
|
+
explorer: "nexus-explorer",
|
|
10846
|
+
documenter: "nexus-documenter"
|
|
10781
10847
|
};
|
|
10782
|
-
const agentType = agentTypeMap[config.role] || "
|
|
10783
|
-
const
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10848
|
+
const agentType = agentTypeMap[config.role] || "nexus-coder";
|
|
10849
|
+
const parent = options?.toolContext?.sessionID ? options.toolContext : undefined;
|
|
10850
|
+
const toolList = parent && typeof this.ctx.tool?.list === "function" ? await this.ctx.tool.list() : undefined;
|
|
10851
|
+
const subagentTool = Array.isArray(toolList) ? toolList.find((t) => t?.id === "subagent" && typeof t?.execute === "function") : undefined;
|
|
10852
|
+
const taskText = options?.task || config.task?.description || config.task?.name || "";
|
|
10853
|
+
let spawnPath = "session-create";
|
|
10854
|
+
let childSessionID;
|
|
10855
|
+
if (parent && subagentTool) {
|
|
10856
|
+
if (!taskText) {
|
|
10857
|
+
throw new Error("spawnAgent requires task text when using the subagent tool path");
|
|
10858
|
+
}
|
|
10859
|
+
spawnPath = "subagent-tool";
|
|
10860
|
+
childSessionID = await this.createChildSession({
|
|
10861
|
+
tool: subagentTool,
|
|
10862
|
+
agent: agentType,
|
|
10863
|
+
description: title,
|
|
10864
|
+
prompt: taskText,
|
|
10865
|
+
model: modelConfig,
|
|
10866
|
+
parent,
|
|
10867
|
+
callID: agentId
|
|
10868
|
+
});
|
|
10869
|
+
} else {
|
|
10870
|
+
const created = await this.ctx.session.create({
|
|
10871
|
+
title,
|
|
10872
|
+
agent: agentType,
|
|
10873
|
+
model: modelName ? { providerID: provider, id: modelName } : undefined,
|
|
10874
|
+
metadata: {
|
|
10875
|
+
nexusRole: config.role,
|
|
10876
|
+
nexusTask: config.task?.name || "direct-spawn",
|
|
10877
|
+
nexusAgentId: agentId,
|
|
10878
|
+
nexusModel: modelConfig
|
|
10879
|
+
}
|
|
10880
|
+
});
|
|
10881
|
+
childSessionID = created.id;
|
|
10882
|
+
}
|
|
10883
|
+
if (spawnPath === "session-create") {
|
|
10884
|
+
this.lastDegradedSpawn = {
|
|
10885
|
+
agentId,
|
|
10886
|
+
role: config.role,
|
|
10887
|
+
reason: parent ? "subagent-tool-unavailable" : "no-parent-context"
|
|
10888
|
+
};
|
|
10889
|
+
console.warn(`[nexus] spawn degraded for ${config.role}: child session ${childSessionID} was created via ` + `ctx.session.create (${this.lastDegradedSpawn.reason}) and is not linked to a parent session.`);
|
|
10890
|
+
} else {
|
|
10891
|
+
this.lastDegradedSpawn = null;
|
|
10892
|
+
}
|
|
10795
10893
|
const agent = {
|
|
10796
10894
|
id: agentId,
|
|
10797
10895
|
name: title,
|
|
@@ -10814,7 +10912,8 @@ Please continue from where the previous agent left off.`;
|
|
|
10814
10912
|
averageResponseTime: 0,
|
|
10815
10913
|
errorRate: 0
|
|
10816
10914
|
},
|
|
10817
|
-
sessionID:
|
|
10915
|
+
sessionID: childSessionID,
|
|
10916
|
+
spawnPath
|
|
10818
10917
|
};
|
|
10819
10918
|
this.agents.set(agentId, agent);
|
|
10820
10919
|
const taskDesc = config.task?.name || `Agent ${config.role} task`;
|
|
@@ -10869,10 +10968,29 @@ Please continue from where the previous agent left off.`;
|
|
|
10869
10968
|
selectModel(role, complexity) {
|
|
10870
10969
|
return this.selectBestModel(role, complexity);
|
|
10871
10970
|
}
|
|
10872
|
-
|
|
10873
|
-
const
|
|
10971
|
+
getModelCost(model, provider) {
|
|
10972
|
+
const exact = this.modelCosts.get(model);
|
|
10973
|
+
if (exact)
|
|
10974
|
+
return exact;
|
|
10975
|
+
if (provider) {
|
|
10976
|
+
const qualified = this.modelCosts.get(`${provider}/${model}`);
|
|
10977
|
+
if (qualified)
|
|
10978
|
+
return qualified;
|
|
10979
|
+
}
|
|
10980
|
+
const bare = model.slice(model.indexOf("/") + 1);
|
|
10981
|
+
let best;
|
|
10982
|
+
for (const [key, cost] of this.modelCosts) {
|
|
10983
|
+
if (key.slice(key.indexOf("/") + 1) !== bare)
|
|
10984
|
+
continue;
|
|
10985
|
+
if (!best || cost.input < best.input)
|
|
10986
|
+
best = cost;
|
|
10987
|
+
}
|
|
10988
|
+
return best;
|
|
10989
|
+
}
|
|
10990
|
+
estimateModelCost(model, provider) {
|
|
10991
|
+
const realCost = this.getModelCost(model, provider);
|
|
10874
10992
|
if (realCost) {
|
|
10875
|
-
return (realCost.input
|
|
10993
|
+
return (realCost.input + realCost.output) / 2;
|
|
10876
10994
|
}
|
|
10877
10995
|
const costs = {
|
|
10878
10996
|
"claude-sonnet-4-6": 0.15,
|
|
@@ -10912,7 +11030,7 @@ Please continue from where the previous agent left off.`;
|
|
|
10912
11030
|
scoreModel(modelId, role, complexity) {
|
|
10913
11031
|
const [provider, ...parts] = modelId.split("/");
|
|
10914
11032
|
const model = parts.join("/");
|
|
10915
|
-
const cost = this.estimateModelCost(model);
|
|
11033
|
+
const cost = this.estimateModelCost(model, provider);
|
|
10916
11034
|
const quality = this.estimateModelQuality(model);
|
|
10917
11035
|
const maxCost = 15;
|
|
10918
11036
|
const costScore = 1 - cost / maxCost;
|
|
@@ -10944,14 +11062,14 @@ Please continue from where the previous agent left off.`;
|
|
|
10944
11062
|
const scored = unique.map((m) => this.scoreModel(m, role, complexity));
|
|
10945
11063
|
const budgetRemaining = this.budget.maxTotalCost - this.totalSpent;
|
|
10946
11064
|
const affordable = scored.filter((s) => {
|
|
10947
|
-
const cost = this.estimateModelCost(s.model);
|
|
11065
|
+
const cost = this.estimateModelCost(s.model, s.provider);
|
|
10948
11066
|
return cost <= budgetRemaining || cost === 0;
|
|
10949
11067
|
});
|
|
10950
11068
|
const best = (affordable.length > 0 ? affordable : scored).sort((a, b) => b.overallScore - a.overallScore)[0];
|
|
10951
11069
|
return {
|
|
10952
11070
|
provider: best.provider,
|
|
10953
11071
|
model: best.model,
|
|
10954
|
-
estimatedCost: this.estimateModelCost(best.model),
|
|
11072
|
+
estimatedCost: this.estimateModelCost(best.model, best.provider),
|
|
10955
11073
|
estimatedQuality: best.qualityScore,
|
|
10956
11074
|
reasoning: best.reasoning
|
|
10957
11075
|
};
|
|
@@ -11445,6 +11563,28 @@ import { homedir as homedir2 } from "node:os";
|
|
|
11445
11563
|
var NEXUS_AGENT_CONTENT = `---
|
|
11446
11564
|
description: Nexus multi-agent orchestrator — decomposes tasks and delegates to specialized sub-agents
|
|
11447
11565
|
mode: primary
|
|
11566
|
+
permissions:
|
|
11567
|
+
- action: subagent
|
|
11568
|
+
resource: "nexus-*"
|
|
11569
|
+
effect: allow
|
|
11570
|
+
- action: subagent
|
|
11571
|
+
resource: "nexus-architect"
|
|
11572
|
+
effect: allow
|
|
11573
|
+
- action: subagent
|
|
11574
|
+
resource: "nexus-coder"
|
|
11575
|
+
effect: allow
|
|
11576
|
+
- action: subagent
|
|
11577
|
+
resource: "nexus-reviewer"
|
|
11578
|
+
effect: allow
|
|
11579
|
+
- action: subagent
|
|
11580
|
+
resource: "nexus-tester"
|
|
11581
|
+
effect: allow
|
|
11582
|
+
- action: subagent
|
|
11583
|
+
resource: "nexus-explorer"
|
|
11584
|
+
effect: allow
|
|
11585
|
+
- action: subagent
|
|
11586
|
+
resource: "nexus-documenter"
|
|
11587
|
+
effect: allow
|
|
11448
11588
|
---
|
|
11449
11589
|
|
|
11450
11590
|
# Nexus Orchestrator
|
|
@@ -11608,8 +11748,10 @@ mode: subagent
|
|
|
11608
11748
|
permissions:
|
|
11609
11749
|
- action: edit
|
|
11610
11750
|
resource: "*"
|
|
11751
|
+
effect: allow
|
|
11611
11752
|
- action: shell
|
|
11612
11753
|
resource: "*"
|
|
11754
|
+
effect: allow
|
|
11613
11755
|
---
|
|
11614
11756
|
|
|
11615
11757
|
# Nexus Architect Agent
|
|
@@ -11650,8 +11792,10 @@ mode: subagent
|
|
|
11650
11792
|
permissions:
|
|
11651
11793
|
- action: edit
|
|
11652
11794
|
resource: "*"
|
|
11795
|
+
effect: allow
|
|
11653
11796
|
- action: shell
|
|
11654
11797
|
resource: "*"
|
|
11798
|
+
effect: allow
|
|
11655
11799
|
---
|
|
11656
11800
|
|
|
11657
11801
|
# Nexus Coder Agent
|
|
@@ -11737,8 +11881,10 @@ mode: subagent
|
|
|
11737
11881
|
permissions:
|
|
11738
11882
|
- action: edit
|
|
11739
11883
|
resource: "*"
|
|
11884
|
+
effect: allow
|
|
11740
11885
|
- action: shell
|
|
11741
11886
|
resource: "*"
|
|
11887
|
+
effect: allow
|
|
11742
11888
|
---
|
|
11743
11889
|
|
|
11744
11890
|
# Nexus Tester Agent
|
|
@@ -11836,8 +11982,10 @@ mode: subagent
|
|
|
11836
11982
|
permissions:
|
|
11837
11983
|
- action: edit
|
|
11838
11984
|
resource: "*"
|
|
11985
|
+
effect: allow
|
|
11839
11986
|
- action: shell
|
|
11840
11987
|
resource: "*"
|
|
11988
|
+
effect: allow
|
|
11841
11989
|
---
|
|
11842
11990
|
|
|
11843
11991
|
# Nexus Documenter Agent
|
|
@@ -11915,6 +12063,25 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
11915
12063
|
totalCost: initialState.totalSpent,
|
|
11916
12064
|
budgetRemaining: initialState.budgetRemaining
|
|
11917
12065
|
})));
|
|
12066
|
+
const spawnAndDeliver = async (opts, toolCtx) => {
|
|
12067
|
+
const agent = await orchestrator.spawnAgent({ role: opts.role, model: opts.model }, {
|
|
12068
|
+
toolContext: {
|
|
12069
|
+
sessionID: toolCtx?.sessionID || "",
|
|
12070
|
+
agent: toolCtx?.agent,
|
|
12071
|
+
messageID: toolCtx?.messageID,
|
|
12072
|
+
callID: toolCtx?.id,
|
|
12073
|
+
signal: toolCtx?.signal
|
|
12074
|
+
},
|
|
12075
|
+
task: opts.task
|
|
12076
|
+
});
|
|
12077
|
+
if (agent.spawnPath !== "subagent-tool") {
|
|
12078
|
+
await ctx.session.prompt({
|
|
12079
|
+
sessionID: agent.sessionID,
|
|
12080
|
+
text: opts.task
|
|
12081
|
+
});
|
|
12082
|
+
}
|
|
12083
|
+
return agent;
|
|
12084
|
+
};
|
|
11918
12085
|
await ctx.tool.transform((editor) => {
|
|
11919
12086
|
editor.namespace({
|
|
11920
12087
|
name: "nexus",
|
|
@@ -12101,35 +12268,36 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12101
12268
|
});
|
|
12102
12269
|
editor.add({
|
|
12103
12270
|
name: "model.costs",
|
|
12104
|
-
description: "Show real model pricing from OpenCode or set custom costs",
|
|
12271
|
+
description: "Show real model pricing from OpenCode, or set custom costs. All prices are USD per 1K tokens.",
|
|
12105
12272
|
input: {
|
|
12106
12273
|
type: "object",
|
|
12107
12274
|
properties: {
|
|
12108
|
-
model: { type: "string", description: "Model
|
|
12109
|
-
setInput: { type: "number", description: "Set input cost per
|
|
12110
|
-
setOutput: { type: "number", description: "Set output cost per
|
|
12275
|
+
model: { type: "string", description: "Model to show cost for, as 'provider/id' or a bare 'id' (optional, shows all if omitted)" },
|
|
12276
|
+
setInput: { type: "number", description: "Set input cost in USD per 1K tokens for a model (e.g. 0.003 for $3 per million tokens)" },
|
|
12277
|
+
setOutput: { type: "number", description: "Set output cost in USD per 1K tokens for a model (e.g. 0.015 for $15 per million tokens)" }
|
|
12111
12278
|
},
|
|
12112
12279
|
additionalProperties: false
|
|
12113
12280
|
},
|
|
12114
12281
|
options: { codemode: true },
|
|
12115
12282
|
execute: async (input) => {
|
|
12116
12283
|
const { model, setInput, setOutput } = input;
|
|
12284
|
+
const per1k = (v) => `$${v}/1K tokens`;
|
|
12117
12285
|
if (model && setInput !== undefined && setOutput !== undefined) {
|
|
12118
12286
|
orchestrator.setModelCosts({ [model]: { input: setInput, output: setOutput } });
|
|
12119
|
-
return { content: `Set ${model}: input
|
|
12287
|
+
return { content: `Set ${model}: input=${per1k(setInput)}, output=${per1k(setOutput)}` };
|
|
12120
12288
|
}
|
|
12121
12289
|
if (model) {
|
|
12122
|
-
const cost = orchestrator.
|
|
12290
|
+
const cost = orchestrator.getModelCost(model);
|
|
12123
12291
|
if (cost) {
|
|
12124
|
-
return { content: `${model}: input
|
|
12292
|
+
return { content: `${model}: input=${per1k(cost.input)}, output=${per1k(cost.output)}, cache_read=${per1k(cost.cacheRead)}, cache_write=${per1k(cost.cacheWrite)}` };
|
|
12125
12293
|
}
|
|
12126
12294
|
const estimate = orchestrator["estimateModelCost"](model);
|
|
12127
12295
|
return { content: `${model}: no real pricing data (estimated $${estimate}/1K tokens)` };
|
|
12128
12296
|
}
|
|
12129
12297
|
if (orchestrator.modelCosts.size > 0) {
|
|
12130
|
-
const lines = ["\uD83D\uDCCA Model Pricing (from OpenCode):"];
|
|
12298
|
+
const lines = ["\uD83D\uDCCA Model Pricing (from OpenCode, per 1K tokens):"];
|
|
12131
12299
|
for (const [id, cost] of orchestrator.modelCosts) {
|
|
12132
|
-
lines.push(` ${id}:
|
|
12300
|
+
lines.push(` ${id}: ${per1k(cost.input)} in, ${per1k(cost.output)} out`);
|
|
12133
12301
|
}
|
|
12134
12302
|
return { content: lines.join(`
|
|
12135
12303
|
`) };
|
|
@@ -12153,27 +12321,15 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12153
12321
|
additionalProperties: false
|
|
12154
12322
|
},
|
|
12155
12323
|
options: { codemode: true },
|
|
12156
|
-
execute: async (input) => {
|
|
12324
|
+
execute: async (input, toolCtx) => {
|
|
12157
12325
|
const { role, task, model, wait, timeout } = input;
|
|
12158
12326
|
try {
|
|
12159
|
-
|
|
12160
|
-
try {
|
|
12161
|
-
const currentSession = ctx.session?.current?.();
|
|
12162
|
-
if (currentSession?.id) {
|
|
12163
|
-
orchestrator.parentSessionID = currentSession.id;
|
|
12164
|
-
}
|
|
12165
|
-
} catch {}
|
|
12166
|
-
}
|
|
12167
|
-
const agent = await orchestrator.spawnAgent({ role, model });
|
|
12168
|
-
await orchestrator.ctx.session.prompt({
|
|
12169
|
-
sessionID: agent.sessionID,
|
|
12170
|
-
text: task
|
|
12171
|
-
});
|
|
12327
|
+
const agent = await spawnAndDeliver({ role, task, model }, toolCtx);
|
|
12172
12328
|
agent.status = "working";
|
|
12173
12329
|
orchestrator.notifyStateChange();
|
|
12174
12330
|
if (wait) {
|
|
12175
12331
|
const waitTimeout = timeout || 120000;
|
|
12176
|
-
const waitPromise =
|
|
12332
|
+
const waitPromise = ctx.session.wait({ sessionID: agent.sessionID });
|
|
12177
12333
|
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Timed out after ${waitTimeout}ms`)), waitTimeout));
|
|
12178
12334
|
try {
|
|
12179
12335
|
await Promise.race([waitPromise, timeoutPromise]);
|
|
@@ -12181,9 +12337,9 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12181
12337
|
agent.status = "working";
|
|
12182
12338
|
orchestrator.notifyStateChange();
|
|
12183
12339
|
try {
|
|
12184
|
-
const messages = await
|
|
12185
|
-
const
|
|
12186
|
-
if (
|
|
12340
|
+
const messages = await ctx.session.context({ sessionID: agent.sessionID });
|
|
12341
|
+
const partialText = lastAssistantText(messages);
|
|
12342
|
+
if (partialText) {
|
|
12187
12343
|
agent.status = "completed";
|
|
12188
12344
|
await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
|
|
12189
12345
|
const taskPreview = task.length > 80 ? task.substring(0, 77) + "..." : task;
|
|
@@ -12195,7 +12351,7 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12195
12351
|
`\uD83D\uDCCE Session: ${agent.sessionID}`,
|
|
12196
12352
|
`
|
|
12197
12353
|
--- Partial Result ---`,
|
|
12198
|
-
|
|
12354
|
+
partialText
|
|
12199
12355
|
].join(`
|
|
12200
12356
|
`)
|
|
12201
12357
|
};
|
|
@@ -12213,9 +12369,8 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12213
12369
|
};
|
|
12214
12370
|
}
|
|
12215
12371
|
try {
|
|
12216
|
-
const messages = await
|
|
12217
|
-
const
|
|
12218
|
-
const result = lastMsg?.content || "Task completed (no output captured)";
|
|
12372
|
+
const messages = await ctx.session.context({ sessionID: agent.sessionID });
|
|
12373
|
+
const result = lastAssistantText(messages) || "Task completed (no output captured)";
|
|
12219
12374
|
agent.status = "completed";
|
|
12220
12375
|
await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
|
|
12221
12376
|
const taskPreview = task.length > 80 ? task.substring(0, 77) + "..." : task;
|
|
@@ -12290,24 +12445,19 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12290
12445
|
additionalProperties: false
|
|
12291
12446
|
},
|
|
12292
12447
|
options: { codemode: true },
|
|
12293
|
-
execute: async (input) => {
|
|
12448
|
+
execute: async (input, toolCtx) => {
|
|
12294
12449
|
const { role, task, model, timeout } = input;
|
|
12295
12450
|
try {
|
|
12296
|
-
const agent = await
|
|
12297
|
-
await orchestrator.ctx.session.prompt({
|
|
12298
|
-
sessionID: agent.sessionID,
|
|
12299
|
-
text: task
|
|
12300
|
-
});
|
|
12451
|
+
const agent = await spawnAndDeliver({ role, task, model }, toolCtx);
|
|
12301
12452
|
agent.status = "working";
|
|
12302
12453
|
orchestrator.notifyStateChange();
|
|
12303
12454
|
const waitTimeout = timeout || 120000;
|
|
12304
|
-
const waitPromise =
|
|
12455
|
+
const waitPromise = ctx.session.wait({ sessionID: agent.sessionID });
|
|
12305
12456
|
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve("timeout"), waitTimeout));
|
|
12306
12457
|
const outcome = await Promise.race([waitPromise.then(() => "completed"), timeoutPromise]);
|
|
12307
12458
|
try {
|
|
12308
|
-
const messages = await
|
|
12309
|
-
const
|
|
12310
|
-
const resultContent = lastMsg?.content || (outcome === "timeout" ? "Timed out — agent may still be running" : "Completed with no output");
|
|
12459
|
+
const messages = await ctx.session.context({ sessionID: agent.sessionID });
|
|
12460
|
+
const resultContent = lastAssistantText(messages) || (outcome === "timeout" ? "Timed out — agent may still be running" : "Completed with no output");
|
|
12311
12461
|
agent.status = outcome === "timeout" ? "working" : "completed";
|
|
12312
12462
|
orchestrator.notifyStateChange();
|
|
12313
12463
|
await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
|
|
@@ -12609,12 +12759,20 @@ ${lines.join(`
|
|
|
12609
12759
|
const agents = orchestrator.getState().agents.filter((a) => a.status === "working" || a.status === "idle");
|
|
12610
12760
|
if (agents.length === 0)
|
|
12611
12761
|
return { content: "No running agents to move to background." };
|
|
12762
|
+
const background = ctx.session.background;
|
|
12763
|
+
if (!background) {
|
|
12764
|
+
return { content: "This OpenCode version does not expose session.background on the plugin context — nothing was detached." };
|
|
12765
|
+
}
|
|
12766
|
+
let detached = 0;
|
|
12612
12767
|
for (const agent of agents) {
|
|
12768
|
+
if (!agent.sessionID)
|
|
12769
|
+
continue;
|
|
12613
12770
|
try {
|
|
12614
|
-
await
|
|
12771
|
+
await background.call(ctx.session, { sessionID: agent.sessionID });
|
|
12772
|
+
detached++;
|
|
12615
12773
|
} catch {}
|
|
12616
12774
|
}
|
|
12617
|
-
return { content: `${
|
|
12775
|
+
return { content: `${detached} agent(s) moved to background. You can continue working while they run.` };
|
|
12618
12776
|
}
|
|
12619
12777
|
});
|
|
12620
12778
|
editor.add({
|
|
@@ -12631,11 +12789,11 @@ ${lines.join(`
|
|
|
12631
12789
|
execute: async (input) => {
|
|
12632
12790
|
const { sessionID } = input;
|
|
12633
12791
|
try {
|
|
12634
|
-
const messages = await
|
|
12635
|
-
const
|
|
12636
|
-
if (
|
|
12792
|
+
const messages = await ctx.session.context({ sessionID });
|
|
12793
|
+
const text = lastAssistantText(messages);
|
|
12794
|
+
if (text) {
|
|
12637
12795
|
return { content: `Session ${sessionID} result:
|
|
12638
|
-
${
|
|
12796
|
+
${text}` };
|
|
12639
12797
|
}
|
|
12640
12798
|
return { content: `Session ${sessionID} has no assistant messages yet.` };
|
|
12641
12799
|
} catch (error) {
|
package/dist/tui.js
CHANGED
|
@@ -129,7 +129,11 @@ class NexusConfigManager {
|
|
|
129
129
|
}
|
|
130
130
|
getModelForRole(role) {
|
|
131
131
|
const config = this.getConfig();
|
|
132
|
-
|
|
132
|
+
const model = config.models[role] || config.models.coder || DEFAULT_CONFIG.models.coder;
|
|
133
|
+
if (!model.includes("/")) {
|
|
134
|
+
console.warn(`[nexus] Model "${model}" for role "${role}" is missing provider prefix. Expected "providerID/modelID" format.`);
|
|
135
|
+
}
|
|
136
|
+
return model;
|
|
133
137
|
}
|
|
134
138
|
updateStorageConfig(update) {
|
|
135
139
|
this.storageConfig = {
|
|
@@ -213,10 +217,18 @@ class NexusConfigManager {
|
|
|
213
217
|
}
|
|
214
218
|
initProjectConfig(basePath) {
|
|
215
219
|
const projectPath = join(basePath, ".opencode", "nexus.jsonc");
|
|
220
|
+
try {
|
|
221
|
+
readFileSync(projectPath, "utf-8");
|
|
222
|
+
return;
|
|
223
|
+
} catch {}
|
|
216
224
|
this.writeJsoncFile(projectPath, { ...DEFAULT_CONFIG });
|
|
217
225
|
}
|
|
218
226
|
initGlobalConfig() {
|
|
219
227
|
const globalPath = join(homedir(), ".config", "opencode", "nexus.jsonc");
|
|
228
|
+
try {
|
|
229
|
+
readFileSync(globalPath, "utf-8");
|
|
230
|
+
return;
|
|
231
|
+
} catch {}
|
|
220
232
|
this.writeJsoncFile(globalPath, { ...DEFAULT_CONFIG });
|
|
221
233
|
}
|
|
222
234
|
saveConfig(level, basePath) {
|
|
@@ -382,6 +394,12 @@ var tui_default = define({
|
|
|
382
394
|
id: "nexus.cli",
|
|
383
395
|
setup(context) {
|
|
384
396
|
const configManager = new NexusConfigManager;
|
|
397
|
+
try {
|
|
398
|
+
const loc = context.location ?? context.data.location.default();
|
|
399
|
+
if (loc?.directory) {
|
|
400
|
+
configManager.loadFromPath(loc.directory);
|
|
401
|
+
}
|
|
402
|
+
} catch {}
|
|
385
403
|
let configSaveScope = "global";
|
|
386
404
|
const handleModelSelect = async (role) => {
|
|
387
405
|
if (!configManager.getRoles().includes(role)) {
|