@serkanalgur/opencode-nexus 2.3.5 → 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.
Files changed (2) hide show
  1. package/dist/index.js +251 -114
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10085,6 +10085,21 @@ ${lines.join(`
10085
10085
  }
10086
10086
 
10087
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
+ }
10088
10103
  var DEFAULT_ESCALATION = {
10089
10104
  maxRetries: 3,
10090
10105
  retryDelay: 1000,
@@ -10136,7 +10151,7 @@ class NexusOrchestrator {
10136
10151
  }
10137
10152
  notifications = null;
10138
10153
  modelCosts = new Map;
10139
- parentSessionID = null;
10154
+ lastDegradedSpawn = null;
10140
10155
  constructor(config, messageStoreConfig, memoryStoreConfig) {
10141
10156
  this.config = this.mergeConfig(config);
10142
10157
  this.budget = this.config.budget;
@@ -10161,7 +10176,7 @@ class NexusOrchestrator {
10161
10176
  async initialize(ctx, onStateChange) {
10162
10177
  this.ctx = ctx;
10163
10178
  this.onStateChange = onStateChange ?? null;
10164
- const projectDir = ctx.location?.directory || process.cwd();
10179
+ const projectDir = ctx.location.directory;
10165
10180
  this.configManager.loadFromPath(projectDir);
10166
10181
  await this.loadModelCosts();
10167
10182
  this.cleanupInterval = setInterval(() => this.cleanupStaleData(), 300000);
@@ -10181,41 +10196,22 @@ class NexusOrchestrator {
10181
10196
  }
10182
10197
  async loadModelCosts() {
10183
10198
  try {
10184
- if (!this.ctx)
10199
+ if (!this.ctx?.model)
10185
10200
  return;
10186
- if (this.ctx.client?.model?.list) {
10187
- const result = await this.ctx.client.model.list();
10188
- const models = result?.data?.data ?? result?.data ?? [];
10189
- if (Array.isArray(models)) {
10190
- for (const model of models) {
10191
- if (model.cost && Array.isArray(model.cost) && model.cost.length > 0) {
10192
- const baseCost = model.cost[0];
10193
- this.modelCosts.set(model.id, {
10194
- input: baseCost.input || 0,
10195
- output: baseCost.output || 0,
10196
- cacheRead: baseCost.cache?.read || 0,
10197
- cacheWrite: baseCost.cache?.write || 0
10198
- });
10199
- }
10200
- }
10201
- }
10201
+ const { data } = await this.ctx.model.list();
10202
+ if (!Array.isArray(data) || data.length === 0)
10202
10203
  return;
10203
- }
10204
- const location = this.ctx.location ?? this.ctx.data?.location?.default();
10205
- if (location && this.ctx.data?.location?.model) {
10206
- await this.ctx.data.location.model.sync(location);
10207
- const models = this.ctx.data.location.model.list(location) ?? [];
10208
- for (const model of models) {
10209
- if (model.cost && Array.isArray(model.cost) && model.cost.length > 0) {
10210
- const baseCost = model.cost[0];
10211
- this.modelCosts.set(model.id, {
10212
- input: baseCost.input || 0,
10213
- output: baseCost.output || 0,
10214
- cacheRead: baseCost.cache?.read || 0,
10215
- cacheWrite: baseCost.cache?.write || 0
10216
- });
10217
- }
10218
- }
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
+ });
10219
10215
  }
10220
10216
  } catch {}
10221
10217
  }
@@ -10564,15 +10560,14 @@ Please continue from where the previous agent left off.`;
10564
10560
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Task timed out")), timeout));
10565
10561
  await Promise.race([waitPromise, timeoutPromise]);
10566
10562
  const messages = await this.ctx.session.context({ sessionID: agent.sessionID });
10567
- const lastAssistantMsg = messages.filter((m) => m.role === "assistant").pop();
10568
- const output = lastAssistantMsg?.content || "Task completed";
10563
+ const output = lastAssistantText(messages) || "Task completed (no output captured)";
10569
10564
  const duration = Date.now() - startTime;
10570
10565
  const result = {
10571
10566
  success: true,
10572
- output: typeof output === "string" ? output : JSON.stringify(output),
10567
+ output,
10573
10568
  duration,
10574
10569
  tokensUsed: 0,
10575
- cost: this.estimateModelCost(agent.model.model)
10570
+ cost: this.estimateModelCost(agent.model.model, agent.model.provider)
10576
10571
  };
10577
10572
  this.dag.markComplete(node.id, result);
10578
10573
  this.todoEnforcer.completeTask(agent.id);
@@ -10608,7 +10603,7 @@ Please continue from where the previous agent left off.`;
10608
10603
  if (priorPattern.length > 0) {
10609
10604
  this.learning.recordSuccess(priorPattern[0].entry.id);
10610
10605
  }
10611
- if (typeof output === "string" && output.length > 0) {
10606
+ if (output.length > 0) {
10612
10607
  const securityIssues = this.securityScanner.scanContent(output, node.task.name);
10613
10608
  if (securityIssues.length > 0) {
10614
10609
  this.emit("security:issues-found", {
@@ -10769,7 +10764,57 @@ Please continue from where the previous agent left off.`;
10769
10764
  };
10770
10765
  this.notifyStateChange();
10771
10766
  }
10772
- async spawnAgent(config) {
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) {
10773
10818
  if (!this.ctx) {
10774
10819
  throw new Error("Orchestrator not initialized");
10775
10820
  }
@@ -10793,26 +10838,58 @@ Please continue from where the previous agent left off.`;
10793
10838
  const roleEmoji = this.configManager.getRoleEmoji(config.role);
10794
10839
  const title = `${roleEmoji} ${this.configManager.getRoleDisplayName(config.role)} — ${modelConfig}`;
10795
10840
  const agentTypeMap = {
10796
- architect: "architect",
10797
- coder: "build-orchestrator",
10798
- reviewer: "code-reviewer",
10799
- tester: "build-orchestrator",
10800
- explorer: "explore",
10801
- documenter: "doc-writer"
10841
+ architect: "nexus-architect",
10842
+ coder: "nexus-coder",
10843
+ reviewer: "nexus-reviewer",
10844
+ tester: "nexus-tester",
10845
+ explorer: "nexus-explorer",
10846
+ documenter: "nexus-documenter"
10802
10847
  };
10803
- const agentType = agentTypeMap[config.role] || "build";
10804
- const session = await this.ctx.session.create({
10805
- title,
10806
- agent: agentType,
10807
- model: modelName ? { providerID: provider, id: modelName } : undefined,
10808
- parentID: this.parentSessionID || undefined,
10809
- metadata: {
10810
- nexusRole: config.role,
10811
- nexusTask: config.task?.name || "direct-spawn",
10812
- nexusAgentId: agentId,
10813
- nexusModel: modelConfig
10814
- }
10815
- });
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
+ }
10816
10893
  const agent = {
10817
10894
  id: agentId,
10818
10895
  name: title,
@@ -10835,7 +10912,8 @@ Please continue from where the previous agent left off.`;
10835
10912
  averageResponseTime: 0,
10836
10913
  errorRate: 0
10837
10914
  },
10838
- sessionID: session.id
10915
+ sessionID: childSessionID,
10916
+ spawnPath
10839
10917
  };
10840
10918
  this.agents.set(agentId, agent);
10841
10919
  const taskDesc = config.task?.name || `Agent ${config.role} task`;
@@ -10890,10 +10968,29 @@ Please continue from where the previous agent left off.`;
10890
10968
  selectModel(role, complexity) {
10891
10969
  return this.selectBestModel(role, complexity);
10892
10970
  }
10893
- estimateModelCost(model) {
10894
- const realCost = this.modelCosts.get(model);
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);
10895
10992
  if (realCost) {
10896
- return (realCost.input * 1000 + realCost.output * 1000) / 2;
10993
+ return (realCost.input + realCost.output) / 2;
10897
10994
  }
10898
10995
  const costs = {
10899
10996
  "claude-sonnet-4-6": 0.15,
@@ -10933,7 +11030,7 @@ Please continue from where the previous agent left off.`;
10933
11030
  scoreModel(modelId, role, complexity) {
10934
11031
  const [provider, ...parts] = modelId.split("/");
10935
11032
  const model = parts.join("/");
10936
- const cost = this.estimateModelCost(model);
11033
+ const cost = this.estimateModelCost(model, provider);
10937
11034
  const quality = this.estimateModelQuality(model);
10938
11035
  const maxCost = 15;
10939
11036
  const costScore = 1 - cost / maxCost;
@@ -10965,14 +11062,14 @@ Please continue from where the previous agent left off.`;
10965
11062
  const scored = unique.map((m) => this.scoreModel(m, role, complexity));
10966
11063
  const budgetRemaining = this.budget.maxTotalCost - this.totalSpent;
10967
11064
  const affordable = scored.filter((s) => {
10968
- const cost = this.estimateModelCost(s.model);
11065
+ const cost = this.estimateModelCost(s.model, s.provider);
10969
11066
  return cost <= budgetRemaining || cost === 0;
10970
11067
  });
10971
11068
  const best = (affordable.length > 0 ? affordable : scored).sort((a, b) => b.overallScore - a.overallScore)[0];
10972
11069
  return {
10973
11070
  provider: best.provider,
10974
11071
  model: best.model,
10975
- estimatedCost: this.estimateModelCost(best.model),
11072
+ estimatedCost: this.estimateModelCost(best.model, best.provider),
10976
11073
  estimatedQuality: best.qualityScore,
10977
11074
  reasoning: best.reasoning
10978
11075
  };
@@ -11466,6 +11563,28 @@ import { homedir as homedir2 } from "node:os";
11466
11563
  var NEXUS_AGENT_CONTENT = `---
11467
11564
  description: Nexus multi-agent orchestrator — decomposes tasks and delegates to specialized sub-agents
11468
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
11469
11588
  ---
11470
11589
 
11471
11590
  # Nexus Orchestrator
@@ -11629,8 +11748,10 @@ mode: subagent
11629
11748
  permissions:
11630
11749
  - action: edit
11631
11750
  resource: "*"
11751
+ effect: allow
11632
11752
  - action: shell
11633
11753
  resource: "*"
11754
+ effect: allow
11634
11755
  ---
11635
11756
 
11636
11757
  # Nexus Architect Agent
@@ -11671,8 +11792,10 @@ mode: subagent
11671
11792
  permissions:
11672
11793
  - action: edit
11673
11794
  resource: "*"
11795
+ effect: allow
11674
11796
  - action: shell
11675
11797
  resource: "*"
11798
+ effect: allow
11676
11799
  ---
11677
11800
 
11678
11801
  # Nexus Coder Agent
@@ -11758,8 +11881,10 @@ mode: subagent
11758
11881
  permissions:
11759
11882
  - action: edit
11760
11883
  resource: "*"
11884
+ effect: allow
11761
11885
  - action: shell
11762
11886
  resource: "*"
11887
+ effect: allow
11763
11888
  ---
11764
11889
 
11765
11890
  # Nexus Tester Agent
@@ -11857,8 +11982,10 @@ mode: subagent
11857
11982
  permissions:
11858
11983
  - action: edit
11859
11984
  resource: "*"
11985
+ effect: allow
11860
11986
  - action: shell
11861
11987
  resource: "*"
11988
+ effect: allow
11862
11989
  ---
11863
11990
 
11864
11991
  # Nexus Documenter Agent
@@ -11936,6 +12063,25 @@ You are a technical writer who creates documentation that developers actually wa
11936
12063
  totalCost: initialState.totalSpent,
11937
12064
  budgetRemaining: initialState.budgetRemaining
11938
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
+ };
11939
12085
  await ctx.tool.transform((editor) => {
11940
12086
  editor.namespace({
11941
12087
  name: "nexus",
@@ -12122,35 +12268,36 @@ You are a technical writer who creates documentation that developers actually wa
12122
12268
  });
12123
12269
  editor.add({
12124
12270
  name: "model.costs",
12125
- 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.",
12126
12272
  input: {
12127
12273
  type: "object",
12128
12274
  properties: {
12129
- model: { type: "string", description: "Model ID to show cost for (optional, shows all if omitted)" },
12130
- setInput: { type: "number", description: "Set input cost per token for a model" },
12131
- setOutput: { type: "number", description: "Set output cost per token for a model" }
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)" }
12132
12278
  },
12133
12279
  additionalProperties: false
12134
12280
  },
12135
12281
  options: { codemode: true },
12136
12282
  execute: async (input) => {
12137
12283
  const { model, setInput, setOutput } = input;
12284
+ const per1k = (v) => `$${v}/1K tokens`;
12138
12285
  if (model && setInput !== undefined && setOutput !== undefined) {
12139
12286
  orchestrator.setModelCosts({ [model]: { input: setInput, output: setOutput } });
12140
- return { content: `Set ${model}: input=$${setInput}/token, output=$${setOutput}/token` };
12287
+ return { content: `Set ${model}: input=${per1k(setInput)}, output=${per1k(setOutput)}` };
12141
12288
  }
12142
12289
  if (model) {
12143
- const cost = orchestrator.modelCosts.get(model);
12290
+ const cost = orchestrator.getModelCost(model);
12144
12291
  if (cost) {
12145
- return { content: `${model}: input=$${cost.input}/token, output=$${cost.output}/token, cache_read=$${cost.cacheRead}/token, cache_write=$${cost.cacheWrite}/token` };
12292
+ return { content: `${model}: input=${per1k(cost.input)}, output=${per1k(cost.output)}, cache_read=${per1k(cost.cacheRead)}, cache_write=${per1k(cost.cacheWrite)}` };
12146
12293
  }
12147
12294
  const estimate = orchestrator["estimateModelCost"](model);
12148
12295
  return { content: `${model}: no real pricing data (estimated $${estimate}/1K tokens)` };
12149
12296
  }
12150
12297
  if (orchestrator.modelCosts.size > 0) {
12151
- const lines = ["\uD83D\uDCCA Model Pricing (from OpenCode):"];
12298
+ const lines = ["\uD83D\uDCCA Model Pricing (from OpenCode, per 1K tokens):"];
12152
12299
  for (const [id, cost] of orchestrator.modelCosts) {
12153
- lines.push(` ${id}: $${cost.input}/token in, $${cost.output}/token out`);
12300
+ lines.push(` ${id}: ${per1k(cost.input)} in, ${per1k(cost.output)} out`);
12154
12301
  }
12155
12302
  return { content: lines.join(`
12156
12303
  `) };
@@ -12174,27 +12321,15 @@ You are a technical writer who creates documentation that developers actually wa
12174
12321
  additionalProperties: false
12175
12322
  },
12176
12323
  options: { codemode: true },
12177
- execute: async (input) => {
12324
+ execute: async (input, toolCtx) => {
12178
12325
  const { role, task, model, wait, timeout } = input;
12179
12326
  try {
12180
- if (!orchestrator.parentSessionID) {
12181
- try {
12182
- const currentSession = ctx.session?.current?.();
12183
- if (currentSession?.id) {
12184
- orchestrator.parentSessionID = currentSession.id;
12185
- }
12186
- } catch {}
12187
- }
12188
- const agent = await orchestrator.spawnAgent({ role, model });
12189
- await orchestrator.ctx.session.prompt({
12190
- sessionID: agent.sessionID,
12191
- text: task
12192
- });
12327
+ const agent = await spawnAndDeliver({ role, task, model }, toolCtx);
12193
12328
  agent.status = "working";
12194
12329
  orchestrator.notifyStateChange();
12195
12330
  if (wait) {
12196
12331
  const waitTimeout = timeout || 120000;
12197
- const waitPromise = orchestrator.ctx.session.wait({ sessionID: agent.sessionID });
12332
+ const waitPromise = ctx.session.wait({ sessionID: agent.sessionID });
12198
12333
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Timed out after ${waitTimeout}ms`)), waitTimeout));
12199
12334
  try {
12200
12335
  await Promise.race([waitPromise, timeoutPromise]);
@@ -12202,9 +12337,9 @@ You are a technical writer who creates documentation that developers actually wa
12202
12337
  agent.status = "working";
12203
12338
  orchestrator.notifyStateChange();
12204
12339
  try {
12205
- const messages = await orchestrator.ctx.session.context({ sessionID: agent.sessionID });
12206
- const lastMsg = messages.filter((m) => m.role === "assistant").pop();
12207
- if (lastMsg) {
12340
+ const messages = await ctx.session.context({ sessionID: agent.sessionID });
12341
+ const partialText = lastAssistantText(messages);
12342
+ if (partialText) {
12208
12343
  agent.status = "completed";
12209
12344
  await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
12210
12345
  const taskPreview = task.length > 80 ? task.substring(0, 77) + "..." : task;
@@ -12216,7 +12351,7 @@ You are a technical writer who creates documentation that developers actually wa
12216
12351
  `\uD83D\uDCCE Session: ${agent.sessionID}`,
12217
12352
  `
12218
12353
  --- Partial Result ---`,
12219
- typeof lastMsg.content === "string" ? lastMsg.content : JSON.stringify(lastMsg.content)
12354
+ partialText
12220
12355
  ].join(`
12221
12356
  `)
12222
12357
  };
@@ -12234,9 +12369,8 @@ You are a technical writer who creates documentation that developers actually wa
12234
12369
  };
12235
12370
  }
12236
12371
  try {
12237
- const messages = await orchestrator.ctx.session.context({ sessionID: agent.sessionID });
12238
- const lastMsg = messages.filter((m) => m.role === "assistant").pop();
12239
- 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)";
12240
12374
  agent.status = "completed";
12241
12375
  await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
12242
12376
  const taskPreview = task.length > 80 ? task.substring(0, 77) + "..." : task;
@@ -12311,24 +12445,19 @@ You are a technical writer who creates documentation that developers actually wa
12311
12445
  additionalProperties: false
12312
12446
  },
12313
12447
  options: { codemode: true },
12314
- execute: async (input) => {
12448
+ execute: async (input, toolCtx) => {
12315
12449
  const { role, task, model, timeout } = input;
12316
12450
  try {
12317
- const agent = await orchestrator.spawnAgent({ role, model });
12318
- await orchestrator.ctx.session.prompt({
12319
- sessionID: agent.sessionID,
12320
- text: task
12321
- });
12451
+ const agent = await spawnAndDeliver({ role, task, model }, toolCtx);
12322
12452
  agent.status = "working";
12323
12453
  orchestrator.notifyStateChange();
12324
12454
  const waitTimeout = timeout || 120000;
12325
- const waitPromise = orchestrator.ctx.session.wait({ sessionID: agent.sessionID });
12455
+ const waitPromise = ctx.session.wait({ sessionID: agent.sessionID });
12326
12456
  const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve("timeout"), waitTimeout));
12327
12457
  const outcome = await Promise.race([waitPromise.then(() => "completed"), timeoutPromise]);
12328
12458
  try {
12329
- const messages = await orchestrator.ctx.session.context({ sessionID: agent.sessionID });
12330
- const lastMsg = messages.filter((m) => m.role === "assistant").pop();
12331
- 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");
12332
12461
  agent.status = outcome === "timeout" ? "working" : "completed";
12333
12462
  orchestrator.notifyStateChange();
12334
12463
  await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
@@ -12630,12 +12759,20 @@ ${lines.join(`
12630
12759
  const agents = orchestrator.getState().agents.filter((a) => a.status === "working" || a.status === "idle");
12631
12760
  if (agents.length === 0)
12632
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;
12633
12767
  for (const agent of agents) {
12768
+ if (!agent.sessionID)
12769
+ continue;
12634
12770
  try {
12635
- await orchestrator.ctx.session.background({ sessionID: agent.sessionID });
12771
+ await background.call(ctx.session, { sessionID: agent.sessionID });
12772
+ detached++;
12636
12773
  } catch {}
12637
12774
  }
12638
- return { content: `${agents.length} agent(s) moved to background. You can continue working while they run.` };
12775
+ return { content: `${detached} agent(s) moved to background. You can continue working while they run.` };
12639
12776
  }
12640
12777
  });
12641
12778
  editor.add({
@@ -12652,11 +12789,11 @@ ${lines.join(`
12652
12789
  execute: async (input) => {
12653
12790
  const { sessionID } = input;
12654
12791
  try {
12655
- const messages = await orchestrator.ctx.session.context({ sessionID });
12656
- const lastMsg = messages.filter((m) => m.role === "assistant").pop();
12657
- if (lastMsg) {
12792
+ const messages = await ctx.session.context({ sessionID });
12793
+ const text = lastAssistantText(messages);
12794
+ if (text) {
12658
12795
  return { content: `Session ${sessionID} result:
12659
- ${lastMsg.content}` };
12796
+ ${text}` };
12660
12797
  }
12661
12798
  return { content: `Session ${sessionID} has no assistant messages yet.` };
12662
12799
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serkanalgur/opencode-nexus",
3
- "version": "2.3.5",
3
+ "version": "2.4.0",
4
4
  "description": "Adaptive Multi-Agent Orchestration with Cost Intelligence for OpenCode V2",
5
5
  "keywords": [
6
6
  "opencode",