@elizaos/plugin-agent-orchestrator 0.3.9 → 0.3.10

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 CHANGED
@@ -99,7 +99,35 @@ var init_ansi_utils = __esm(() => {
99
99
  });
100
100
 
101
101
  // src/services/swarm-coordinator-prompts.ts
102
- function buildCoordinationPrompt(taskCtx, promptText, recentOutput, decisionHistory) {
102
+ function buildSiblingSection(siblings) {
103
+ if (!siblings || siblings.length === 0)
104
+ return "";
105
+ return `
106
+ Other agents in this swarm:
107
+ ` + siblings.map((s) => ` - [${s.status}] "${s.label}" (${s.agentType}): ${s.originalTask}`).join(`
108
+ `) + `
109
+ Use this context when the agent asks creative or architectural questions — ` + `your answer should be consistent with what sibling agents are working on.
110
+ `;
111
+ }
112
+ function buildSharedDecisionsSection(decisions) {
113
+ if (!decisions || decisions.length === 0)
114
+ return "";
115
+ return `
116
+ Key decisions made by other agents in this swarm:
117
+ ` + decisions.slice(-10).map((d) => ` - [${d.agentLabel}] ${d.summary}`).join(`
118
+ `) + `
119
+ Align with these decisions for consistency — don't contradict them unless the task requires it.
120
+ `;
121
+ }
122
+ function buildSwarmContextSection(swarmContext) {
123
+ if (!swarmContext)
124
+ return "";
125
+ return `
126
+ Project context (from planning phase):
127
+ ${swarmContext}
128
+ `;
129
+ }
130
+ function buildCoordinationPrompt(taskCtx, promptText, recentOutput, decisionHistory, siblingTasks, sharedDecisions, swarmContext) {
103
131
  const historySection = decisionHistory.length > 0 ? `
104
132
  Previous decisions for this session:
105
133
  ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] prompt="${d.promptText}" → ${d.action}${d.response ? ` ("${d.response}")` : ""} — ${d.reasoning}`).join(`
@@ -110,7 +138,7 @@ ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] prompt="${d.
110
138
  ` + `Original task: "${taskCtx.originalTask}"
111
139
  ` + `Working directory: ${taskCtx.workdir}
112
140
  ` + `Repository: ${taskCtx.repo ?? "none (scratch directory)"}
113
- ` + historySection + `
141
+ ` + buildSwarmContextSection(swarmContext) + buildSiblingSection(siblingTasks) + buildSharedDecisionsSection(sharedDecisions) + historySection + `
114
142
  Recent terminal output (last 50 lines):
115
143
  ` + `---
116
144
  ${recentOutput.slice(-3000)}
@@ -139,11 +167,12 @@ ${recentOutput.slice(-3000)}
139
167
  ` + `- Only use "complete" if the agent confirmed it verified ALL test plan items after creating the PR.
140
168
  ` + `- If the agent is asking for information that was NOT provided in the original task ` + `(e.g. which repository to use, project requirements, credentials), ESCALATE. ` + `The coordinator does not have this information — the human must provide it.
141
169
  ` + `- When in doubt, escalate — it's better to ask the human than to make a wrong choice.
170
+ ` + `- If the agent's output reveals a significant decision that sibling agents should know about ` + `(e.g. chose a library, designed an API shape, picked a UI pattern, established a writing style, ` + `narrowed a research scope, made any choice that affects the shared project), ` + `include "keyDecision" with a brief one-line summary. Skip this for routine tool approvals.
142
171
 
143
172
  ` + `Respond with ONLY a JSON object:
144
- ` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "..."}`;
173
+ ` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "...", "keyDecision": "..."}`;
145
174
  }
146
- function buildIdleCheckPrompt(taskCtx, recentOutput, idleMinutes, idleCheckNumber, maxIdleChecks, decisionHistory) {
175
+ function buildIdleCheckPrompt(taskCtx, recentOutput, idleMinutes, idleCheckNumber, maxIdleChecks, decisionHistory, siblingTasks, sharedDecisions, swarmContext) {
147
176
  const historySection = decisionHistory.length > 0 ? `
148
177
  Previous decisions for this session:
149
178
  ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] prompt="${d.promptText}" → ${d.action}${d.response ? ` ("${d.response}")` : ""} — ${d.reasoning}`).join(`
@@ -155,7 +184,7 @@ ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] prompt="${d.
155
184
  ` + `Working directory: ${taskCtx.workdir}
156
185
  ` + `Repository: ${taskCtx.repo ?? "none (scratch directory)"}
157
186
  ` + `Idle check: ${idleCheckNumber} of ${maxIdleChecks} (session will be force-escalated after ${maxIdleChecks})
158
- ` + historySection + `
187
+ ` + buildSwarmContextSection(swarmContext) + buildSiblingSection(siblingTasks) + buildSharedDecisionsSection(sharedDecisions) + historySection + `
159
188
  Recent terminal output (last 50 lines):
160
189
  ` + `---
161
190
  ${recentOutput.slice(-3000)}
@@ -179,11 +208,12 @@ ${recentOutput.slice(-3000)}
179
208
  ` + `- If the output shows an error or the agent seems stuck in a loop, escalate.
180
209
  ` + `- If the agent is clearly mid-operation (build output, test runner, git operations), use "ignore".
181
210
  ` + `- On check ${idleCheckNumber} of ${maxIdleChecks} — if unsure, lean toward "respond" with a nudge rather than "complete".
211
+ ` + `- If the agent's output reveals a significant creative or architectural decision, ` + `include "keyDecision" with a brief one-line summary.
182
212
 
183
213
  ` + `Respond with ONLY a JSON object:
184
- ` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "..."}`;
214
+ ` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "...", "keyDecision": "..."}`;
185
215
  }
186
- function buildTurnCompletePrompt(taskCtx, turnOutput, decisionHistory) {
216
+ function buildTurnCompletePrompt(taskCtx, turnOutput, decisionHistory, siblingTasks, sharedDecisions, swarmContext) {
187
217
  const historySection = decisionHistory.length > 0 ? `
188
218
  Previous decisions for this session:
189
219
  ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] prompt="${d.promptText}" → ${d.action}${d.response ? ` ("${d.response}")` : ""} — ${d.reasoning}`).join(`
@@ -194,7 +224,7 @@ ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] prompt="${d.
194
224
  ` + `Original task: "${taskCtx.originalTask}"
195
225
  ` + `Working directory: ${taskCtx.workdir}
196
226
  ` + `Repository: ${taskCtx.repo ?? "none (scratch directory)"}
197
- ` + historySection + `
227
+ ` + buildSwarmContextSection(swarmContext) + buildSiblingSection(siblingTasks) + buildSharedDecisionsSection(sharedDecisions) + historySection + `
198
228
  Output from this turn:
199
229
  ` + `---
200
230
  ${turnOutput.slice(-3000)}
@@ -227,11 +257,12 @@ ${turnOutput.slice(-3000)}
227
257
  ` + `- Keep follow-up instructions concise and specific.
228
258
  ` + `- When asking agents to verify work, prefer CLI tools (gh, curl, cat, git diff, etc.) over ` + `browser automation. Browser tools may not be available in headless environments and can cause delays.
229
259
  ` + `- Default to "respond" — only use "complete" when you're certain ALL work is done.
260
+ ` + `- If the agent's output reveals a significant decision that sibling agents should know about ` + `(e.g. chose a library, designed an API shape, picked a UI pattern, established a writing style, ` + `narrowed a research scope, made any choice that affects the shared project), ` + `include "keyDecision" with a brief one-line summary. Skip this for routine tool approvals.
230
261
 
231
262
  ` + `Respond with ONLY a JSON object:
232
- ` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "..."}`;
263
+ ` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "...", "keyDecision": "..."}`;
233
264
  }
234
- function buildBlockedEventMessage(taskCtx, promptText, recentOutput, decisionHistory) {
265
+ function buildBlockedEventMessage(taskCtx, promptText, recentOutput, decisionHistory, siblingTasks, sharedDecisions, swarmContext) {
235
266
  const historySection = decisionHistory.length > 0 ? `
236
267
  Previous decisions:
237
268
  ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] "${d.promptText}" → ${d.action}${d.response ? ` ("${d.response}")` : ""} — ${d.reasoning}`).join(`
@@ -242,7 +273,7 @@ ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] "${d.promptT
242
273
  ` + `Task: "${taskCtx.originalTask}"
243
274
  ` + `Workdir: ${taskCtx.workdir}
244
275
  ` + `Repo: ${taskCtx.repo ?? "none (scratch directory)"}
245
- ` + historySection + `
276
+ ` + buildSwarmContextSection(swarmContext) + buildSiblingSection(siblingTasks) + buildSharedDecisionsSection(sharedDecisions) + historySection + `
246
277
  Recent terminal output:
247
278
  ---
248
279
  ${recentOutput.slice(-3000)}
@@ -263,11 +294,13 @@ ${recentOutput.slice(-3000)}
263
294
  ` + `- If a PR was just created, respond to review & verify test plan items before completing.
264
295
  ` + `- When in doubt, escalate.
265
296
 
297
+ ` + `If the agent's output reveals a significant decision that sibling agents should know about, include "keyDecision" with a brief summary.
298
+
266
299
  ` + `Include a JSON action block at the end of your response:
267
- ` + "```json\n" + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "..."}
300
+ ` + "```json\n" + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "...", "keyDecision": "..."}
268
301
  ` + "```";
269
302
  }
270
- function buildTurnCompleteEventMessage(taskCtx, turnOutput, decisionHistory) {
303
+ function buildTurnCompleteEventMessage(taskCtx, turnOutput, decisionHistory, siblingTasks, sharedDecisions, swarmContext) {
271
304
  const historySection = decisionHistory.length > 0 ? `
272
305
  Previous decisions:
273
306
  ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] "${d.promptText}" → ${d.action}${d.response ? ` ("${d.response}")` : ""} — ${d.reasoning}`).join(`
@@ -278,7 +311,7 @@ ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] "${d.promptT
278
311
  ` + `Task: "${taskCtx.originalTask}"
279
312
  ` + `Workdir: ${taskCtx.workdir}
280
313
  ` + `Repo: ${taskCtx.repo ?? "none (scratch directory)"}
281
- ` + historySection + `
314
+ ` + buildSwarmContextSection(swarmContext) + buildSiblingSection(siblingTasks) + buildSharedDecisionsSection(sharedDecisions) + historySection + `
282
315
  Turn output:
283
316
  ---
284
317
  ${turnOutput.slice(-3000)}
@@ -298,9 +331,10 @@ ${turnOutput.slice(-3000)}
298
331
  ` + `- If a PR was just created, respond to review & verify test plan items.
299
332
  ` + `- When asking agents to verify work, prefer CLI tools (gh, curl, cat, etc.) over browser automation.
300
333
  ` + `- Default to "respond" — only "complete" when certain ALL work is done.
334
+ ` + `- If the agent's output reveals a significant creative or architectural decision, include "keyDecision" with a brief summary.
301
335
 
302
336
  ` + `Include a JSON action block at the end of your response:
303
- ` + "```json\n" + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "..."}
337
+ ` + "```json\n" + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "...", "keyDecision": "..."}
304
338
  ` + "```";
305
339
  }
306
340
  function parseCoordinationResponse(llmOutput) {
@@ -326,6 +360,9 @@ function parseCoordinationResponse(llmOutput) {
326
360
  return null;
327
361
  }
328
362
  }
363
+ if (typeof parsed.keyDecision === "string" && parsed.keyDecision.trim()) {
364
+ result.keyDecision = parsed.keyDecision.trim().slice(0, 240);
365
+ }
329
366
  return result;
330
367
  } catch {
331
368
  return null;
@@ -502,6 +539,57 @@ function toDecisionHistory(taskCtx) {
502
539
  reasoning: d.reasoning
503
540
  }));
504
541
  }
542
+ function collectSiblings(ctx, currentSessionId) {
543
+ const siblings = [];
544
+ for (const [sid, task] of ctx.tasks) {
545
+ if (sid === currentSessionId)
546
+ continue;
547
+ siblings.push({
548
+ label: task.label,
549
+ agentType: task.agentType,
550
+ originalTask: task.originalTask,
551
+ status: task.status
552
+ });
553
+ }
554
+ return siblings;
555
+ }
556
+ function enrichWithSharedDecisions(ctx, sessionId, response) {
557
+ const taskCtx = ctx.tasks.get(sessionId);
558
+ if (!taskCtx)
559
+ return { response };
560
+ const allDecisions = ctx.sharedDecisions;
561
+ const lastSeen = taskCtx.lastSeenDecisionIndex;
562
+ const snapshotEnd = allDecisions.length;
563
+ if (lastSeen >= snapshotEnd)
564
+ return { response };
565
+ if (response.length < 20) {
566
+ return { response };
567
+ }
568
+ const unseen = allDecisions.slice(lastSeen, snapshotEnd);
569
+ const contextBlock = unseen.map((d) => `[${d.agentLabel}] ${d.summary}`).join("; ");
570
+ return {
571
+ response: `${response}
572
+
573
+ (Context from other agents: ${contextBlock})`,
574
+ snapshotIndex: snapshotEnd
575
+ };
576
+ }
577
+ function commitSharedDecisionIndex(ctx, sessionId, snapshotIndex) {
578
+ const taskCtx = ctx.tasks.get(sessionId);
579
+ if (taskCtx) {
580
+ taskCtx.lastSeenDecisionIndex = snapshotIndex;
581
+ }
582
+ }
583
+ function recordKeyDecision(ctx, agentLabel, decision) {
584
+ if (!decision.keyDecision)
585
+ return;
586
+ ctx.sharedDecisions.push({
587
+ agentLabel,
588
+ summary: decision.keyDecision,
589
+ timestamp: Date.now()
590
+ });
591
+ ctx.log(`Shared decision from "${agentLabel}": ${decision.keyDecision}`);
592
+ }
505
593
  async function drainPendingTurnComplete(ctx, sessionId) {
506
594
  if (!ctx.pendingTurnComplete.has(sessionId))
507
595
  return;
@@ -542,8 +630,16 @@ function checkAllTasksComplete(ctx) {
542
630
  return;
543
631
  const terminalStates = new Set(["completed", "stopped", "error"]);
544
632
  const allDone = tasks.every((t) => terminalStates.has(t.status));
545
- if (!allDone)
633
+ if (!allDone) {
634
+ const statuses = tasks.map((t) => `${t.label}=${t.status}`).join(", ");
635
+ ctx.log(`checkAllTasksComplete: not all done yet — ${statuses}`);
636
+ return;
637
+ }
638
+ if (ctx.swarmCompleteNotified) {
639
+ ctx.log("checkAllTasksComplete: already notified — skipping");
546
640
  return;
641
+ }
642
+ ctx.swarmCompleteNotified = true;
547
643
  const completed = tasks.filter((t) => t.status === "completed");
548
644
  const stopped = tasks.filter((t) => t.status === "stopped");
549
645
  const errored = tasks.filter((t) => t.status === "error");
@@ -557,7 +653,7 @@ function checkAllTasksComplete(ctx) {
557
653
  if (errored.length > 0) {
558
654
  parts.push(`${errored.length} errored`);
559
655
  }
560
- ctx.sendChatMessage(`All ${tasks.length} coding agents finished (${parts.join(", ")}). Review their work when you're ready.`, "coding-agent");
656
+ ctx.log(`checkAllTasksComplete: all ${tasks.length} tasks terminal (${parts.join(", ")}) firing swarm_complete`);
561
657
  ctx.broadcast({
562
658
  type: "swarm_complete",
563
659
  sessionId: "",
@@ -569,6 +665,34 @@ function checkAllTasksComplete(ctx) {
569
665
  errored: errored.length
570
666
  }
571
667
  });
668
+ const swarmCompleteCb = ctx.getSwarmCompleteCallback();
669
+ const sendFallbackSummary = () => {
670
+ ctx.sendChatMessage(`All ${tasks.length} coding agents finished (${parts.join(", ")}). Review their work when you're ready.`, "coding-agent");
671
+ };
672
+ if (swarmCompleteCb) {
673
+ ctx.log("checkAllTasksComplete: swarm complete callback is wired — calling synthesis");
674
+ const taskSummaries = tasks.map((t) => ({
675
+ sessionId: t.sessionId,
676
+ label: t.label,
677
+ agentType: t.agentType,
678
+ originalTask: t.originalTask,
679
+ status: t.status,
680
+ completionSummary: t.completionSummary ?? ""
681
+ }));
682
+ withTimeout(Promise.resolve().then(() => swarmCompleteCb({
683
+ tasks: taskSummaries,
684
+ total: tasks.length,
685
+ completed: completed.length,
686
+ stopped: stopped.length,
687
+ errored: errored.length
688
+ })), DECISION_CB_TIMEOUT_MS, "swarmCompleteCb").catch((err) => {
689
+ ctx.log(`Swarm complete callback failed: ${err} — falling back to generic summary`);
690
+ sendFallbackSummary();
691
+ });
692
+ } else {
693
+ ctx.log("checkAllTasksComplete: no synthesis callback — sending generic message");
694
+ sendFallbackSummary();
695
+ }
572
696
  }
573
697
  async function fetchRecentOutput(ctx, sessionId, lines = 50) {
574
698
  if (!ctx.ptyService)
@@ -580,7 +704,7 @@ async function fetchRecentOutput(ctx, sessionId, lines = 50) {
580
704
  }
581
705
  }
582
706
  async function makeCoordinationDecision(ctx, taskCtx, promptText, recentOutput) {
583
- const prompt = buildCoordinationPrompt(toContextSummary(taskCtx), promptText, recentOutput, toDecisionHistory(taskCtx));
707
+ const prompt = buildCoordinationPrompt(toContextSummary(taskCtx), promptText, recentOutput, toDecisionHistory(taskCtx), collectSiblings(ctx, taskCtx.sessionId), ctx.sharedDecisions, ctx.getSwarmContext());
584
708
  try {
585
709
  const result = await ctx.runtime.useModel(ModelType3.TEXT_SMALL, {
586
710
  prompt
@@ -599,7 +723,11 @@ async function executeDecision(ctx, sessionId, decision) {
599
723
  if (decision.useKeys && decision.keys) {
600
724
  await ctx.ptyService.sendKeysToSession(sessionId, decision.keys);
601
725
  } else if (decision.response !== undefined) {
602
- await ctx.ptyService.sendToSession(sessionId, decision.response);
726
+ const { response: enriched, snapshotIndex } = enrichWithSharedDecisions(ctx, sessionId, decision.response);
727
+ await ctx.ptyService.sendToSession(sessionId, enriched);
728
+ if (snapshotIndex !== undefined) {
729
+ commitSharedDecisionIndex(ctx, sessionId, snapshotIndex);
730
+ }
603
731
  }
604
732
  break;
605
733
  case "complete": {
@@ -618,6 +746,9 @@ async function executeDecision(ctx, sessionId, decision) {
618
746
  const rawOutput = await ctx.ptyService.getSessionOutput(sessionId, 50);
619
747
  summary = extractCompletionSummary(rawOutput);
620
748
  } catch {}
749
+ if (taskCtx) {
750
+ taskCtx.completionSummary = summary || decision.reasoning || "";
751
+ }
621
752
  ctx.sendChatMessage(summary ? `Finished "${taskCtx?.label ?? sessionId}".
622
753
 
623
754
  ${summary}` : `Finished "${taskCtx?.label ?? sessionId}".`, "coding-agent");
@@ -759,7 +890,7 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
759
890
  }
760
891
  let decision = null;
761
892
  const decisionFromPipeline = false;
762
- const prompt = buildTurnCompletePrompt(toContextSummary(taskCtx), turnOutput, toDecisionHistory(taskCtx));
893
+ const prompt = buildTurnCompletePrompt(toContextSummary(taskCtx), turnOutput, toDecisionHistory(taskCtx), collectSiblings(ctx, sessionId), ctx.sharedDecisions, ctx.getSwarmContext());
763
894
  try {
764
895
  const result = await ctx.runtime.useModel(ModelType3.TEXT_SMALL, {
765
896
  prompt
@@ -784,6 +915,7 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
784
915
  response: formatDecisionResponse(decision),
785
916
  reasoning: decision.reasoning
786
917
  });
918
+ recordKeyDecision(ctx, taskCtx.label, decision);
787
919
  ctx.broadcast({
788
920
  type: "turn_assessment",
789
921
  sessionId,
@@ -834,7 +966,7 @@ async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, rec
834
966
  decision = await makeCoordinationDecision(ctx, taskCtx, promptText, output);
835
967
  } else {
836
968
  if (agentDecisionCb) {
837
- const eventMessage = buildBlockedEventMessage(toContextSummary(taskCtx), promptText, output, toDecisionHistory(taskCtx));
969
+ const eventMessage = buildBlockedEventMessage(toContextSummary(taskCtx), promptText, output, toDecisionHistory(taskCtx), collectSiblings(ctx, sessionId), ctx.sharedDecisions, ctx.getSwarmContext());
838
970
  try {
839
971
  decision = await withTimeout(agentDecisionCb(eventMessage, sessionId, taskCtx), DECISION_CB_TIMEOUT_MS, "agentDecisionCb");
840
972
  if (decision)
@@ -882,6 +1014,7 @@ async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, rec
882
1014
  response: formatDecisionResponse(decision),
883
1015
  reasoning: decision.reasoning
884
1016
  });
1017
+ recordKeyDecision(ctx, taskCtx.label, decision);
885
1018
  taskCtx.autoResolvedCount = 0;
886
1019
  ctx.broadcast({
887
1020
  type: "coordination_decision",
@@ -934,7 +1067,7 @@ async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recent
934
1067
  decision = await makeCoordinationDecision(ctx, taskCtx, promptText, output);
935
1068
  } else {
936
1069
  if (agentDecisionCb) {
937
- const eventMessage = buildBlockedEventMessage(toContextSummary(taskCtx), promptText, output, toDecisionHistory(taskCtx));
1070
+ const eventMessage = buildBlockedEventMessage(toContextSummary(taskCtx), promptText, output, toDecisionHistory(taskCtx), collectSiblings(ctx, sessionId), ctx.sharedDecisions, ctx.getSwarmContext());
938
1071
  try {
939
1072
  decision = await withTimeout(agentDecisionCb(eventMessage, sessionId, taskCtx), DECISION_CB_TIMEOUT_MS, "agentDecisionCb");
940
1073
  if (decision)
@@ -2388,7 +2521,7 @@ async function stopSession(ctx, sessionId, sessionMetadata, sessionWorkdirs, log
2388
2521
  const workdir = sessionWorkdirs.get(sessionId);
2389
2522
  if (workdir) {
2390
2523
  try {
2391
- await cleanupClaudeHooks(workdir, log);
2524
+ await cleanupAgentHooks(workdir, log);
2392
2525
  } catch {}
2393
2526
  }
2394
2527
  sessionMetadata.delete(sessionId);
@@ -2398,17 +2531,27 @@ async function stopSession(ctx, sessionId, sessionMetadata, sessionWorkdirs, log
2398
2531
  log(`Stopped session ${sessionId}`);
2399
2532
  }
2400
2533
  }
2401
- async function cleanupClaudeHooks(workdir, log) {
2402
- const settingsPath = join(workdir, ".claude", "settings.json");
2403
- try {
2404
- const raw = await readFile(settingsPath, "utf-8");
2405
- const settings = JSON.parse(raw);
2406
- if (!settings.hooks)
2407
- return;
2408
- delete settings.hooks;
2409
- await writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
2410
- log(`Cleaned up hooks from ${settingsPath}`);
2411
- } catch {}
2534
+ async function cleanupAgentHooks(workdir, log) {
2535
+ const settingsPaths = [
2536
+ join(workdir, ".claude", "settings.json"),
2537
+ join(workdir, ".gemini", "settings.json")
2538
+ ];
2539
+ for (const settingsPath of settingsPaths) {
2540
+ try {
2541
+ const raw = await readFile(settingsPath, "utf-8");
2542
+ const settings = JSON.parse(raw);
2543
+ if (!settings.hooks)
2544
+ continue;
2545
+ delete settings.hooks;
2546
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
2547
+ log(`Cleaned up hooks from ${settingsPath}`);
2548
+ } catch (err) {
2549
+ const code = err.code;
2550
+ if (code !== "ENOENT") {
2551
+ log(`Failed to clean up hooks from ${settingsPath}: ${err}`);
2552
+ }
2553
+ }
2554
+ }
2412
2555
  }
2413
2556
  function subscribeToOutput(ctx, sessionId, callback) {
2414
2557
  if (ctx.usingBunWorker) {
@@ -3031,7 +3174,18 @@ async function handleIdleCheck(ctx, taskCtx, idleMinutes) {
3031
3174
  response: d.response,
3032
3175
  reasoning: d.reasoning
3033
3176
  }));
3034
- const prompt = buildIdleCheckPrompt(contextSummary, recentOutput, idleMinutes, taskCtx.idleCheckCount, MAX_IDLE_CHECKS, decisionHistory);
3177
+ const siblings = [];
3178
+ for (const [sid, task] of ctx.tasks) {
3179
+ if (sid === sessionId)
3180
+ continue;
3181
+ siblings.push({
3182
+ label: task.label,
3183
+ agentType: task.agentType,
3184
+ originalTask: task.originalTask,
3185
+ status: task.status
3186
+ });
3187
+ }
3188
+ const prompt = buildIdleCheckPrompt(contextSummary, recentOutput, idleMinutes, taskCtx.idleCheckCount, MAX_IDLE_CHECKS, decisionHistory, siblings, ctx.sharedDecisions, ctx.getSwarmContext());
3035
3189
  let decision = null;
3036
3190
  try {
3037
3191
  const result = await ctx.runtime.useModel(ModelType4.TEXT_SMALL, {
@@ -3098,11 +3252,15 @@ class SwarmCoordinator {
3098
3252
  chatCallback = null;
3099
3253
  wsBroadcast = null;
3100
3254
  agentDecisionCb = null;
3255
+ swarmCompleteCb = null;
3101
3256
  unregisteredBuffer = new Map;
3102
3257
  idleWatchdogTimer = null;
3103
3258
  lastSeenOutput = new Map;
3104
3259
  lastToolNotification = new Map;
3105
3260
  _paused = false;
3261
+ sharedDecisions = [];
3262
+ _swarmContext = "";
3263
+ swarmCompleteNotified = false;
3106
3264
  pauseBuffer = [];
3107
3265
  pauseTimeout = null;
3108
3266
  constructor(runtime) {
@@ -3116,6 +3274,20 @@ class SwarmCoordinator {
3116
3274
  this.wsBroadcast = cb;
3117
3275
  this.log("WS broadcast callback wired");
3118
3276
  }
3277
+ setSwarmCompleteCallback(cb) {
3278
+ this.swarmCompleteCb = cb;
3279
+ this.log("Swarm complete callback wired");
3280
+ }
3281
+ getSwarmCompleteCallback() {
3282
+ return this.swarmCompleteCb;
3283
+ }
3284
+ setSwarmContext(context) {
3285
+ this._swarmContext = context;
3286
+ this.log(`Swarm context set (${context.length} chars)`);
3287
+ }
3288
+ getSwarmContext() {
3289
+ return this._swarmContext;
3290
+ }
3119
3291
  setAgentDecisionCallback(cb) {
3120
3292
  this.agentDecisionCb = cb;
3121
3293
  this.log("Agent decision callback wired — events will route through Milaidy");
@@ -3167,6 +3339,9 @@ class SwarmCoordinator {
3167
3339
  this.lastSeenOutput.clear();
3168
3340
  this.lastToolNotification.clear();
3169
3341
  this.agentDecisionCb = null;
3342
+ this.sharedDecisions.length = 0;
3343
+ this._swarmContext = "";
3344
+ this.swarmCompleteNotified = false;
3170
3345
  this._paused = false;
3171
3346
  if (this.pauseTimeout) {
3172
3347
  clearTimeout(this.pauseTimeout);
@@ -3210,6 +3385,16 @@ class SwarmCoordinator {
3210
3385
  }
3211
3386
  }
3212
3387
  registerTask(sessionId, context) {
3388
+ const allPreviousTerminal = this.tasks.size === 0 || Array.from(this.tasks.values()).every((t) => t.status === "completed" || t.status === "stopped" || t.status === "error");
3389
+ if (allPreviousTerminal) {
3390
+ this.swarmCompleteNotified = false;
3391
+ if (this.tasks.size > 0) {
3392
+ this.tasks.clear();
3393
+ this.sharedDecisions.length = 0;
3394
+ this._swarmContext = "";
3395
+ this.log("Cleared stale swarm state for new swarm");
3396
+ }
3397
+ }
3213
3398
  this.tasks.set(sessionId, {
3214
3399
  sessionId,
3215
3400
  agentType: context.agentType,
@@ -3223,7 +3408,8 @@ class SwarmCoordinator {
3223
3408
  registeredAt: Date.now(),
3224
3409
  lastActivityAt: Date.now(),
3225
3410
  idleCheckCount: 0,
3226
- taskDelivered: false
3411
+ taskDelivered: false,
3412
+ lastSeenDecisionIndex: 0
3227
3413
  });
3228
3414
  this.broadcast({
3229
3415
  type: "task_registered",
@@ -3378,7 +3564,9 @@ class SwarmCoordinator {
3378
3564
  break;
3379
3565
  }
3380
3566
  case "stopped":
3381
- taskCtx.status = "stopped";
3567
+ if (taskCtx.status !== "completed" && taskCtx.status !== "error") {
3568
+ taskCtx.status = "stopped";
3569
+ }
3382
3570
  this.inFlightDecisions.delete(sessionId);
3383
3571
  this.broadcast({
3384
3572
  type: "stopped",
@@ -3711,6 +3899,7 @@ class PTYService {
3711
3899
  this.log(`Failed to write approval config: ${err}`);
3712
3900
  }
3713
3901
  }
3902
+ const hookUrl = `http://localhost:${this.runtime.getSetting("SERVER_PORT") ?? "2138"}/api/coding-agents/hooks`;
3714
3903
  if (resolvedAgentType === "claude") {
3715
3904
  try {
3716
3905
  const settingsPath = join2(workdir, ".claude", "settings.json");
@@ -3721,14 +3910,14 @@ class PTYService {
3721
3910
  const permissions = settings.permissions ?? {};
3722
3911
  permissions.allowedDirectories = [workdir];
3723
3912
  settings.permissions = permissions;
3724
- const serverPort = this.runtime.getSetting("SERVER_PORT") ?? "2138";
3725
3913
  const adapter = this.getAdapter("claude");
3726
3914
  const hookProtocol = adapter.getHookTelemetryProtocol({
3727
- httpUrl: `http://localhost:${serverPort}/api/coding-agents/hooks`,
3915
+ httpUrl: hookUrl,
3728
3916
  sessionId
3729
3917
  });
3730
3918
  if (hookProtocol) {
3731
- settings.hooks = hookProtocol.settingsHooks;
3919
+ const existingHooks = settings.hooks ?? {};
3920
+ settings.hooks = { ...existingHooks, ...hookProtocol.settingsHooks };
3732
3921
  this.log(`Injecting HTTP hooks for session ${sessionId}`);
3733
3922
  }
3734
3923
  await mkdir(dirname(settingsPath), { recursive: true });
@@ -3738,6 +3927,29 @@ class PTYService {
3738
3927
  this.log(`Failed to write Claude settings: ${err}`);
3739
3928
  }
3740
3929
  }
3930
+ if (resolvedAgentType === "gemini") {
3931
+ try {
3932
+ const settingsPath = join2(workdir, ".gemini", "settings.json");
3933
+ let settings = {};
3934
+ try {
3935
+ settings = JSON.parse(await readFile2(settingsPath, "utf-8"));
3936
+ } catch {}
3937
+ const adapter = this.getAdapter("gemini");
3938
+ const hookProtocol = adapter.getHookTelemetryProtocol({
3939
+ httpUrl: hookUrl,
3940
+ sessionId
3941
+ });
3942
+ if (hookProtocol) {
3943
+ const existingHooks = settings.hooks ?? {};
3944
+ settings.hooks = { ...existingHooks, ...hookProtocol.settingsHooks };
3945
+ this.log(`Injecting Gemini CLI hooks for session ${sessionId}`);
3946
+ }
3947
+ await mkdir(dirname(settingsPath), { recursive: true });
3948
+ await writeFile2(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
3949
+ } catch (err) {
3950
+ this.log(`Failed to write Gemini settings: ${err}`);
3951
+ }
3952
+ }
3741
3953
  const spawnConfig = buildSpawnConfig(sessionId, {
3742
3954
  ...options,
3743
3955
  agentType: resolvedAgentType,
@@ -3893,6 +4105,9 @@ class PTYService {
3893
4105
  } else {
3894
4106
  this.log(`Hook event for ${sessionId}: ${event} ${summary}`);
3895
4107
  }
4108
+ if (this.manager && this.usingBunWorker) {
4109
+ this.manager.notifyHookEvent(sessionId, event).catch((err) => logger2.debug(`[PTYService] Failed to forward hook event to session: ${err}`));
4110
+ }
3896
4111
  switch (event) {
3897
4112
  case "tool_running":
3898
4113
  this.emitEvent(sessionId, "tool_running", data);
@@ -3905,6 +4120,9 @@ class PTYService {
3905
4120
  case "notification":
3906
4121
  this.emitEvent(sessionId, "message", data);
3907
4122
  break;
4123
+ case "session_end":
4124
+ this.emitEvent(sessionId, "stopped", { ...data, reason: "session_end" });
4125
+ break;
3908
4126
  default:
3909
4127
  break;
3910
4128
  }
@@ -4295,7 +4513,8 @@ var spawnAgentAction = {
4295
4513
 
4296
4514
  // src/actions/coding-task-handlers.ts
4297
4515
  import {
4298
- logger as logger5
4516
+ logger as logger5,
4517
+ ModelType as ModelType5
4299
4518
  } from "@elizaos/core";
4300
4519
 
4301
4520
  // src/actions/coding-task-helpers.ts
@@ -4371,6 +4590,65 @@ ${preview}` : `Agent "${label}" completed the task.`
4371
4590
 
4372
4591
  // src/actions/coding-task-handlers.ts
4373
4592
  var MAX_CONCURRENT_AGENTS = 8;
4593
+ var KNOWN_AGENT_PREFIXES = [
4594
+ "claude",
4595
+ "claude-code",
4596
+ "claudecode",
4597
+ "codex",
4598
+ "openai",
4599
+ "gemini",
4600
+ "google",
4601
+ "aider",
4602
+ "pi",
4603
+ "pi-ai",
4604
+ "piai",
4605
+ "pi-coding-agent",
4606
+ "picodingagent",
4607
+ "shell",
4608
+ "bash"
4609
+ ];
4610
+ function stripAgentPrefix(spec) {
4611
+ const colonIdx = spec.indexOf(":");
4612
+ if (colonIdx <= 0 || colonIdx >= 20)
4613
+ return spec;
4614
+ const prefix = spec.slice(0, colonIdx).trim().toLowerCase();
4615
+ if (KNOWN_AGENT_PREFIXES.includes(prefix)) {
4616
+ return spec.slice(colonIdx + 1).trim();
4617
+ }
4618
+ return spec;
4619
+ }
4620
+ async function generateSwarmContext(runtime, subtasks, userRequest) {
4621
+ const taskList = subtasks.map((t, i) => ` ${i + 1}. ${t}`).join(`
4622
+ `);
4623
+ const prompt = `You are an AI orchestrator about to launch ${subtasks.length} parallel agents. ` + `Before they start, produce a brief shared context document so all agents stay aligned.
4624
+
4625
+ ` + `User's request: "${userRequest}"
4626
+
4627
+ ` + `Subtasks being assigned:
4628
+ ${taskList}
4629
+
4630
+ ` + `Generate a concise shared context brief (3-8 bullet points) covering:
4631
+ ` + `- Project intent and overall goal
4632
+ ` + `- Key constraints or preferences from the user's request
4633
+ ` + `- Conventions all agents should follow (naming, style, patterns, tone)
4634
+ ` + `- How subtasks relate to each other (dependencies, shared interfaces, etc.)
4635
+ ` + `- Any decisions that should be consistent across all agents
4636
+
4637
+ ` + `Only include what's relevant — skip categories that don't apply. ` + `Be specific and actionable, not generic. Keep it under 200 words.
4638
+
4639
+ ` + `Output ONLY the bullet points, no preamble.`;
4640
+ try {
4641
+ const result = await runtime.useModel(ModelType5.TEXT_SMALL, {
4642
+ prompt,
4643
+ maxTokens: 400,
4644
+ temperature: 0.3
4645
+ });
4646
+ return result?.trim() || "";
4647
+ } catch (err) {
4648
+ logger5.warn(`Swarm context generation failed: ${err}`);
4649
+ return "";
4650
+ }
4651
+ }
4374
4652
  async function handleMultiAgent(ctx, agentsParam) {
4375
4653
  const {
4376
4654
  runtime,
@@ -4418,6 +4696,13 @@ async function handleMultiAgent(ctx, agentsParam) {
4418
4696
  text: `Launching ${agentSpecs.length} agents${repo ? ` on ${repo}` : ""}...`
4419
4697
  });
4420
4698
  }
4699
+ const cleanSubtasks = agentSpecs.map(stripAgentPrefix);
4700
+ const userRequest = message.content?.text ?? agentsParam;
4701
+ const swarmContext = agentSpecs.length > 1 ? await generateSwarmContext(runtime, cleanSubtasks, userRequest) : "";
4702
+ if (swarmContext) {
4703
+ const coordinator = getCoordinator(runtime);
4704
+ coordinator?.setSwarmContext(swarmContext);
4705
+ }
4421
4706
  const results = [];
4422
4707
  for (const [i, spec] of agentSpecs.entries()) {
4423
4708
  let specAgentType = defaultAgentType;
@@ -4427,51 +4712,14 @@ async function handleMultiAgent(ctx, agentsParam) {
4427
4712
  const colonIdx = spec.indexOf(":");
4428
4713
  if (ctx.agentSelectionStrategy !== "fixed" && colonIdx > 0 && colonIdx < 20) {
4429
4714
  const prefix = spec.slice(0, colonIdx).trim().toLowerCase();
4430
- const knownTypes = [
4431
- "claude",
4432
- "claude-code",
4433
- "claudecode",
4434
- "codex",
4435
- "openai",
4436
- "gemini",
4437
- "google",
4438
- "aider",
4439
- "pi",
4440
- "pi-ai",
4441
- "piai",
4442
- "pi-coding-agent",
4443
- "picodingagent",
4444
- "shell",
4445
- "bash"
4446
- ];
4447
- if (knownTypes.includes(prefix)) {
4715
+ if (KNOWN_AGENT_PREFIXES.includes(prefix)) {
4448
4716
  specRequestedType = prefix;
4449
4717
  specPiRequested = isPiAgentType(prefix);
4450
4718
  specAgentType = normalizeAgentType(prefix);
4451
4719
  specTask = spec.slice(colonIdx + 1).trim();
4452
4720
  }
4453
4721
  } else if (ctx.agentSelectionStrategy === "fixed" && colonIdx > 0 && colonIdx < 20) {
4454
- const prefix = spec.slice(0, colonIdx).trim().toLowerCase();
4455
- const knownTypes = [
4456
- "claude",
4457
- "claude-code",
4458
- "claudecode",
4459
- "codex",
4460
- "openai",
4461
- "gemini",
4462
- "google",
4463
- "aider",
4464
- "pi",
4465
- "pi-ai",
4466
- "piai",
4467
- "pi-coding-agent",
4468
- "picodingagent",
4469
- "shell",
4470
- "bash"
4471
- ];
4472
- if (knownTypes.includes(prefix)) {
4473
- specTask = spec.slice(colonIdx + 1).trim();
4474
- }
4722
+ specTask = stripAgentPrefix(spec);
4475
4723
  }
4476
4724
  const specLabel = explicitLabel ? `${explicitLabel}-${i + 1}` : generateLabel(repo, specTask);
4477
4725
  try {
@@ -4504,7 +4752,12 @@ async function handleMultiAgent(ctx, agentsParam) {
4504
4752
  }
4505
4753
  }
4506
4754
  const coordinator = getCoordinator(runtime);
4507
- const initialTask = specPiRequested ? toPiCommand(specTask) : specTask;
4755
+ const taskWithContext = swarmContext ? `${specTask}
4756
+
4757
+ --- Shared Context (from project planning) ---
4758
+ ${swarmContext}
4759
+ --- End Shared Context ---` : specTask;
4760
+ const initialTask = specPiRequested ? toPiCommand(taskWithContext) : taskWithContext;
4508
4761
  const displayType = specPiRequested ? "pi" : specAgentType;
4509
4762
  const session = await ptyService.spawnSession({
4510
4763
  name: `coding-${Date.now()}-${i}`,
@@ -6319,6 +6572,8 @@ async function handleHookRoutes(req, res, pathname, ctx) {
6319
6572
  sendError(res, "Missing hook_event_name", 400);
6320
6573
  return true;
6321
6574
  }
6575
+ const toolName = payload.tool_name ?? payload.toolName;
6576
+ const notificationType = payload.notification_type ?? payload.notificationType;
6322
6577
  const headerSessionId = req.headers["x-parallax-session-id"];
6323
6578
  const sessionId = headerSessionId ? headerSessionId : payload.cwd ? ctx.ptyService.findSessionIdByCwd(payload.cwd) : undefined;
6324
6579
  if (!sessionId) {
@@ -6334,13 +6589,13 @@ async function handleHookRoutes(req, res, pathname, ctx) {
6334
6589
  }
6335
6590
  });
6336
6591
  ctx.ptyService.handleHookEvent(sessionId, "permission_approved", {
6337
- tool: payload.tool_name
6592
+ tool: toolName
6338
6593
  });
6339
6594
  return true;
6340
6595
  }
6341
6596
  case "PreToolUse": {
6342
6597
  ctx.ptyService.handleHookEvent(sessionId, "tool_running", {
6343
- toolName: payload.tool_name,
6598
+ toolName,
6344
6599
  source: "hook"
6345
6600
  });
6346
6601
  sendJson(res, {
@@ -6358,19 +6613,56 @@ async function handleHookRoutes(req, res, pathname, ctx) {
6358
6613
  sendJson(res, {});
6359
6614
  return true;
6360
6615
  }
6361
- case "Notification": {
6362
- ctx.ptyService.handleHookEvent(sessionId, "notification", {
6363
- type: payload.notification_type,
6364
- message: payload.message
6616
+ case "TaskCompleted": {
6617
+ ctx.ptyService.handleHookEvent(sessionId, "task_complete", {
6618
+ source: "hook_task_completed"
6365
6619
  });
6366
6620
  sendJson(res, {});
6367
6621
  return true;
6368
6622
  }
6369
- case "TaskCompleted": {
6623
+ case "BeforeTool": {
6624
+ ctx.ptyService.handleHookEvent(sessionId, "tool_running", {
6625
+ toolName,
6626
+ source: "gemini_hook"
6627
+ });
6628
+ sendJson(res, { decision: "allow", continue: true });
6629
+ return true;
6630
+ }
6631
+ case "AfterTool": {
6632
+ ctx.ptyService.handleHookEvent(sessionId, "notification", {
6633
+ type: "tool_complete",
6634
+ message: `Tool ${toolName ?? "unknown"} finished`
6635
+ });
6636
+ sendJson(res, { continue: true });
6637
+ return true;
6638
+ }
6639
+ case "AfterAgent": {
6370
6640
  ctx.ptyService.handleHookEvent(sessionId, "task_complete", {
6371
- source: "hook_task_completed"
6641
+ source: "gemini_hook"
6372
6642
  });
6373
- sendJson(res, {});
6643
+ sendJson(res, { continue: true });
6644
+ return true;
6645
+ }
6646
+ case "SessionEnd": {
6647
+ ctx.ptyService.handleHookEvent(sessionId, "session_end", {
6648
+ source: "hook"
6649
+ });
6650
+ sendJson(res, { continue: true });
6651
+ return true;
6652
+ }
6653
+ case "Notification": {
6654
+ if (notificationType === "ToolPermission") {
6655
+ ctx.ptyService.handleHookEvent(sessionId, "permission_approved", {
6656
+ tool: toolName
6657
+ });
6658
+ sendJson(res, { decision: "allow", continue: true });
6659
+ return true;
6660
+ }
6661
+ ctx.ptyService.handleHookEvent(sessionId, "notification", {
6662
+ type: notificationType,
6663
+ message: payload.message
6664
+ });
6665
+ sendJson(res, { continue: true });
6374
6666
  return true;
6375
6667
  }
6376
6668
  default: {
@@ -6714,5 +7006,5 @@ export {
6714
7006
  CodingWorkspaceService
6715
7007
  };
6716
7008
 
6717
- //# debugId=54C728A058C6B9C264756E2164756E21
7009
+ //# debugId=D706621831E2790E64756E2164756E21
6718
7010
  //# sourceMappingURL=index.js.map