@elizaos/plugin-agent-orchestrator 0.3.8 → 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;
@@ -471,6 +508,18 @@ __export(exports_swarm_decision_loop, {
471
508
  });
472
509
  import * as path from "node:path";
473
510
  import { ModelType as ModelType3 } from "@elizaos/core";
511
+ function withTimeout(promise, ms, label) {
512
+ return new Promise((resolve2, reject) => {
513
+ const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
514
+ promise.then((val) => {
515
+ clearTimeout(timer);
516
+ resolve2(val);
517
+ }, (err) => {
518
+ clearTimeout(timer);
519
+ reject(err);
520
+ });
521
+ });
522
+ }
474
523
  function toContextSummary(taskCtx) {
475
524
  return {
476
525
  sessionId: taskCtx.sessionId,
@@ -490,6 +539,57 @@ function toDecisionHistory(taskCtx) {
490
539
  reasoning: d.reasoning
491
540
  }));
492
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
+ }
493
593
  async function drainPendingTurnComplete(ctx, sessionId) {
494
594
  if (!ctx.pendingTurnComplete.has(sessionId))
495
595
  return;
@@ -530,8 +630,16 @@ function checkAllTasksComplete(ctx) {
530
630
  return;
531
631
  const terminalStates = new Set(["completed", "stopped", "error"]);
532
632
  const allDone = tasks.every((t) => terminalStates.has(t.status));
533
- 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}`);
534
636
  return;
637
+ }
638
+ if (ctx.swarmCompleteNotified) {
639
+ ctx.log("checkAllTasksComplete: already notified — skipping");
640
+ return;
641
+ }
642
+ ctx.swarmCompleteNotified = true;
535
643
  const completed = tasks.filter((t) => t.status === "completed");
536
644
  const stopped = tasks.filter((t) => t.status === "stopped");
537
645
  const errored = tasks.filter((t) => t.status === "error");
@@ -545,7 +653,7 @@ function checkAllTasksComplete(ctx) {
545
653
  if (errored.length > 0) {
546
654
  parts.push(`${errored.length} errored`);
547
655
  }
548
- 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`);
549
657
  ctx.broadcast({
550
658
  type: "swarm_complete",
551
659
  sessionId: "",
@@ -557,6 +665,34 @@ function checkAllTasksComplete(ctx) {
557
665
  errored: errored.length
558
666
  }
559
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
+ }
560
696
  }
561
697
  async function fetchRecentOutput(ctx, sessionId, lines = 50) {
562
698
  if (!ctx.ptyService)
@@ -568,7 +704,7 @@ async function fetchRecentOutput(ctx, sessionId, lines = 50) {
568
704
  }
569
705
  }
570
706
  async function makeCoordinationDecision(ctx, taskCtx, promptText, recentOutput) {
571
- 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());
572
708
  try {
573
709
  const result = await ctx.runtime.useModel(ModelType3.TEXT_SMALL, {
574
710
  prompt
@@ -587,7 +723,11 @@ async function executeDecision(ctx, sessionId, decision) {
587
723
  if (decision.useKeys && decision.keys) {
588
724
  await ctx.ptyService.sendKeysToSession(sessionId, decision.keys);
589
725
  } else if (decision.response !== undefined) {
590
- 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
+ }
591
731
  }
592
732
  break;
593
733
  case "complete": {
@@ -606,6 +746,9 @@ async function executeDecision(ctx, sessionId, decision) {
606
746
  const rawOutput = await ctx.ptyService.getSessionOutput(sessionId, 50);
607
747
  summary = extractCompletionSummary(rawOutput);
608
748
  } catch {}
749
+ if (taskCtx) {
750
+ taskCtx.completionSummary = summary || decision.reasoning || "";
751
+ }
609
752
  ctx.sendChatMessage(summary ? `Finished "${taskCtx?.label ?? sessionId}".
610
753
 
611
754
  ${summary}` : `Finished "${taskCtx?.label ?? sessionId}".`, "coding-agent");
@@ -679,7 +822,7 @@ async function handleBlocked(ctx, sessionId, taskCtx, data) {
679
822
  const count = taskCtx.autoResolvedCount;
680
823
  if (count <= 2 || count % 5 === 0) {
681
824
  const excerpt = promptText.length > 120 ? `${promptText.slice(0, 120)}...` : promptText;
682
- ctx.sendChatMessage(`[${taskCtx.label}] Approved: ${excerpt}`, "coding-agent");
825
+ ctx.log(`[${taskCtx.label}] Approved: ${excerpt}`);
683
826
  }
684
827
  return;
685
828
  }
@@ -745,48 +888,16 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
745
888
  const raw = await fetchRecentOutput(ctx, sessionId);
746
889
  turnOutput = cleanForChat(raw);
747
890
  }
748
- const agentDecisionCb = ctx.getAgentDecisionCallback();
749
891
  let decision = null;
750
- let decisionFromPipeline = false;
751
- const triageCtx = {
752
- eventType: "turn_complete",
753
- promptText: "",
754
- recentOutput: turnOutput,
755
- originalTask: taskCtx.originalTask
756
- };
757
- const tier = agentDecisionCb ? await classifyEventTier(ctx.runtime, triageCtx, ctx.log) : "routine";
758
- if (tier === "routine") {
759
- const prompt = buildTurnCompletePrompt(toContextSummary(taskCtx), turnOutput, toDecisionHistory(taskCtx));
760
- try {
761
- const result = await ctx.runtime.useModel(ModelType3.TEXT_SMALL, {
762
- prompt
763
- });
764
- decision = parseCoordinationResponse(result);
765
- } catch (err) {
766
- ctx.log(`Turn-complete LLM call failed: ${err}`);
767
- }
768
- } else {
769
- if (agentDecisionCb) {
770
- const eventMessage = buildTurnCompleteEventMessage(toContextSummary(taskCtx), turnOutput, toDecisionHistory(taskCtx));
771
- try {
772
- decision = await agentDecisionCb(eventMessage, sessionId, taskCtx);
773
- if (decision)
774
- decisionFromPipeline = true;
775
- } catch (err) {
776
- ctx.log(`Agent decision callback failed for turn-complete: ${err} — falling back to small LLM`);
777
- }
778
- }
779
- if (!decision) {
780
- const prompt = buildTurnCompletePrompt(toContextSummary(taskCtx), turnOutput, toDecisionHistory(taskCtx));
781
- try {
782
- const result = await ctx.runtime.useModel(ModelType3.TEXT_SMALL, {
783
- prompt
784
- });
785
- decision = parseCoordinationResponse(result);
786
- } catch (err) {
787
- ctx.log(`Turn-complete LLM fallback call failed: ${err}`);
788
- }
789
- }
892
+ const decisionFromPipeline = false;
893
+ const prompt = buildTurnCompletePrompt(toContextSummary(taskCtx), turnOutput, toDecisionHistory(taskCtx), collectSiblings(ctx, sessionId), ctx.sharedDecisions, ctx.getSwarmContext());
894
+ try {
895
+ const result = await ctx.runtime.useModel(ModelType3.TEXT_SMALL, {
896
+ prompt
897
+ });
898
+ decision = parseCoordinationResponse(result);
899
+ } catch (err) {
900
+ ctx.log(`Turn-complete LLM call failed: ${err}`);
790
901
  }
791
902
  if (!decision) {
792
903
  ctx.log(`Turn-complete for "${taskCtx.label}": all decision paths failed — escalating`);
@@ -804,6 +915,7 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
804
915
  response: formatDecisionResponse(decision),
805
916
  reasoning: decision.reasoning
806
917
  });
918
+ recordKeyDecision(ctx, taskCtx.label, decision);
807
919
  ctx.broadcast({
808
920
  type: "turn_assessment",
809
921
  sessionId,
@@ -817,7 +929,7 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
817
929
  if (decision.action === "respond") {
818
930
  const instruction = decision.response ?? "";
819
931
  const preview = instruction.length > 120 ? `${instruction.slice(0, 120)}...` : instruction;
820
- ctx.sendChatMessage(`[${taskCtx.label}] Turn done, continuing: ${preview}`, "coding-agent");
932
+ ctx.log(`[${taskCtx.label}] Turn done, continuing: ${preview}`);
821
933
  } else if (decision.action === "escalate") {
822
934
  ctx.sendChatMessage(`[${taskCtx.label}] Turn finished — needs your attention: ${decision.reasoning}`, "coding-agent");
823
935
  }
@@ -854,9 +966,9 @@ async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, rec
854
966
  decision = await makeCoordinationDecision(ctx, taskCtx, promptText, output);
855
967
  } else {
856
968
  if (agentDecisionCb) {
857
- 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());
858
970
  try {
859
- decision = await agentDecisionCb(eventMessage, sessionId, taskCtx);
971
+ decision = await withTimeout(agentDecisionCb(eventMessage, sessionId, taskCtx), DECISION_CB_TIMEOUT_MS, "agentDecisionCb");
860
972
  if (decision)
861
973
  decisionFromPipeline = true;
862
974
  } catch (err) {
@@ -902,6 +1014,7 @@ async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, rec
902
1014
  response: formatDecisionResponse(decision),
903
1015
  reasoning: decision.reasoning
904
1016
  });
1017
+ recordKeyDecision(ctx, taskCtx.label, decision);
905
1018
  taskCtx.autoResolvedCount = 0;
906
1019
  ctx.broadcast({
907
1020
  type: "coordination_decision",
@@ -919,7 +1032,7 @@ async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, rec
919
1032
  if (decision.action === "respond") {
920
1033
  const actionDesc = decision.useKeys ? `Sent keys: ${decision.keys?.join(", ")}` : decision.response ? `Responded: ${decision.response.length > 100 ? `${decision.response.slice(0, 100)}...` : decision.response}` : "Responded";
921
1034
  const reasonExcerpt = decision.reasoning.length > 150 ? `${decision.reasoning.slice(0, 150)}...` : decision.reasoning;
922
- ctx.sendChatMessage(`[${taskCtx.label}] ${actionDesc} — ${reasonExcerpt}`, "coding-agent");
1035
+ ctx.log(`[${taskCtx.label}] ${actionDesc} — ${reasonExcerpt}`);
923
1036
  } else if (decision.action === "escalate") {
924
1037
  ctx.sendChatMessage(`[${taskCtx.label}] Needs your attention: ${decision.reasoning}`, "coding-agent");
925
1038
  }
@@ -954,9 +1067,9 @@ async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recent
954
1067
  decision = await makeCoordinationDecision(ctx, taskCtx, promptText, output);
955
1068
  } else {
956
1069
  if (agentDecisionCb) {
957
- 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());
958
1071
  try {
959
- decision = await agentDecisionCb(eventMessage, sessionId, taskCtx);
1072
+ decision = await withTimeout(agentDecisionCb(eventMessage, sessionId, taskCtx), DECISION_CB_TIMEOUT_MS, "agentDecisionCb");
960
1073
  if (decision)
961
1074
  decisionFromPipeline = true;
962
1075
  } catch (err) {
@@ -1006,7 +1119,7 @@ async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recent
1006
1119
  await drainPendingTurnComplete(ctx, sessionId);
1007
1120
  }
1008
1121
  }
1009
- var MAX_AUTO_RESPONSES = 10;
1122
+ var DECISION_CB_TIMEOUT_MS = 30000, MAX_AUTO_RESPONSES = 10;
1010
1123
  var init_swarm_decision_loop = __esm(() => {
1011
1124
  init_ansi_utils();
1012
1125
  init_swarm_event_triage();
@@ -2033,8 +2146,8 @@ import {
2033
2146
  } from "@elizaos/core";
2034
2147
 
2035
2148
  // src/services/pty-service.ts
2036
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2037
- import { dirname, join } from "node:path";
2149
+ import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2150
+ import { dirname, join as join2 } from "node:path";
2038
2151
  import { logger as logger2 } from "@elizaos/core";
2039
2152
  import {
2040
2153
  checkAdapters,
@@ -2349,6 +2462,8 @@ async function initializePTYManager(ctx) {
2349
2462
  }
2350
2463
 
2351
2464
  // src/services/pty-session-io.ts
2465
+ import { readFile, writeFile } from "node:fs/promises";
2466
+ import { join } from "node:path";
2352
2467
  async function sendToSession(ctx, sessionId, input) {
2353
2468
  const session = ctx.manager.get(sessionId);
2354
2469
  if (!session) {
@@ -2403,6 +2518,12 @@ async function stopSession(ctx, sessionId, sessionMetadata, sessionWorkdirs, log
2403
2518
  }
2404
2519
  } catch {}
2405
2520
  ctx.outputUnsubscribers.delete(sessionId);
2521
+ const workdir = sessionWorkdirs.get(sessionId);
2522
+ if (workdir) {
2523
+ try {
2524
+ await cleanupAgentHooks(workdir, log);
2525
+ } catch {}
2526
+ }
2406
2527
  sessionMetadata.delete(sessionId);
2407
2528
  sessionWorkdirs.delete(sessionId);
2408
2529
  ctx.sessionOutputBuffers.delete(sessionId);
@@ -2410,6 +2531,28 @@ async function stopSession(ctx, sessionId, sessionMetadata, sessionWorkdirs, log
2410
2531
  log(`Stopped session ${sessionId}`);
2411
2532
  }
2412
2533
  }
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
+ }
2555
+ }
2413
2556
  function subscribeToOutput(ctx, sessionId, callback) {
2414
2557
  if (ctx.usingBunWorker) {
2415
2558
  const unsubscribe2 = ctx.manager.onSessionData(sessionId, callback);
@@ -2521,7 +2664,7 @@ function setupDeferredTaskDelivery(ctx, session, task, agentType) {
2521
2664
  taskSent = true;
2522
2665
  if (readyTimeout)
2523
2666
  clearTimeout(readyTimeout);
2524
- ctx.markTaskDelivered?.(sid);
2667
+ ctx.markTaskDelivered(sid);
2525
2668
  setTimeout(() => sendTaskWithRetry(0), settleMs);
2526
2669
  if (ctx.usingBunWorker) {
2527
2670
  ctx.manager.removeListener("session_ready", onReady);
@@ -2570,7 +2713,12 @@ function buildSpawnConfig(sessionId, options, workdir) {
2570
2713
  type: options.agentType,
2571
2714
  workdir,
2572
2715
  inheritProcessEnv: false,
2573
- env: { ...buildSanitizedBaseEnv(), ...options.env, ...modelEnv },
2716
+ env: {
2717
+ ...buildSanitizedBaseEnv(),
2718
+ ...options.env,
2719
+ ...modelEnv,
2720
+ PARALLAX_SESSION_ID: sessionId
2721
+ },
2574
2722
  ...options.skipAdapterAutoResponse ? { skipAdapterAutoResponse: true } : {},
2575
2723
  adapterConfig: {
2576
2724
  ...options.credentials,
@@ -3026,7 +3174,18 @@ async function handleIdleCheck(ctx, taskCtx, idleMinutes) {
3026
3174
  response: d.response,
3027
3175
  reasoning: d.reasoning
3028
3176
  }));
3029
- 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());
3030
3189
  let decision = null;
3031
3190
  try {
3032
3191
  const result = await ctx.runtime.useModel(ModelType4.TEXT_SMALL, {
@@ -3062,7 +3221,7 @@ async function handleIdleCheck(ctx, taskCtx, idleMinutes) {
3062
3221
  });
3063
3222
  if (decision.action === "complete") {} else if (decision.action === "respond") {
3064
3223
  const actionDesc = decision.useKeys ? `Sent keys: ${decision.keys?.join(", ")}` : `Nudged: ${decision.response ?? ""}`;
3065
- ctx.sendChatMessage(`[${taskCtx.label}] Idle for ${idleMinutes}m — ${actionDesc}`, "coding-agent");
3224
+ ctx.log(`[${taskCtx.label}] Idle for ${idleMinutes}m — ${actionDesc}`);
3066
3225
  } else if (decision.action === "escalate") {
3067
3226
  ctx.sendChatMessage(`[${taskCtx.label}] Idle for ${idleMinutes}m — needs your attention: ${decision.reasoning}`, "coding-agent");
3068
3227
  } else if (decision.action === "ignore") {
@@ -3093,11 +3252,15 @@ class SwarmCoordinator {
3093
3252
  chatCallback = null;
3094
3253
  wsBroadcast = null;
3095
3254
  agentDecisionCb = null;
3255
+ swarmCompleteCb = null;
3096
3256
  unregisteredBuffer = new Map;
3097
3257
  idleWatchdogTimer = null;
3098
3258
  lastSeenOutput = new Map;
3099
3259
  lastToolNotification = new Map;
3100
3260
  _paused = false;
3261
+ sharedDecisions = [];
3262
+ _swarmContext = "";
3263
+ swarmCompleteNotified = false;
3101
3264
  pauseBuffer = [];
3102
3265
  pauseTimeout = null;
3103
3266
  constructor(runtime) {
@@ -3111,6 +3274,20 @@ class SwarmCoordinator {
3111
3274
  this.wsBroadcast = cb;
3112
3275
  this.log("WS broadcast callback wired");
3113
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
+ }
3114
3291
  setAgentDecisionCallback(cb) {
3115
3292
  this.agentDecisionCb = cb;
3116
3293
  this.log("Agent decision callback wired — events will route through Milaidy");
@@ -3162,6 +3339,9 @@ class SwarmCoordinator {
3162
3339
  this.lastSeenOutput.clear();
3163
3340
  this.lastToolNotification.clear();
3164
3341
  this.agentDecisionCb = null;
3342
+ this.sharedDecisions.length = 0;
3343
+ this._swarmContext = "";
3344
+ this.swarmCompleteNotified = false;
3165
3345
  this._paused = false;
3166
3346
  if (this.pauseTimeout) {
3167
3347
  clearTimeout(this.pauseTimeout);
@@ -3205,6 +3385,16 @@ class SwarmCoordinator {
3205
3385
  }
3206
3386
  }
3207
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
+ }
3208
3398
  this.tasks.set(sessionId, {
3209
3399
  sessionId,
3210
3400
  agentType: context.agentType,
@@ -3218,7 +3408,8 @@ class SwarmCoordinator {
3218
3408
  registeredAt: Date.now(),
3219
3409
  lastActivityAt: Date.now(),
3220
3410
  idleCheckCount: 0,
3221
- taskDelivered: false
3411
+ taskDelivered: false,
3412
+ lastSeenDecisionIndex: 0
3222
3413
  });
3223
3414
  this.broadcast({
3224
3415
  type: "task_registered",
@@ -3373,7 +3564,9 @@ class SwarmCoordinator {
3373
3564
  break;
3374
3565
  }
3375
3566
  case "stopped":
3376
- taskCtx.status = "stopped";
3567
+ if (taskCtx.status !== "completed" && taskCtx.status !== "error") {
3568
+ taskCtx.status = "stopped";
3569
+ }
3377
3570
  this.inFlightDecisions.delete(sessionId);
3378
3571
  this.broadcast({
3379
3572
  type: "stopped",
@@ -3401,6 +3594,9 @@ class SwarmCoordinator {
3401
3594
  data
3402
3595
  });
3403
3596
  const toolData = data;
3597
+ if (toolData.source === "hook") {
3598
+ break;
3599
+ }
3404
3600
  const now = Date.now();
3405
3601
  const STARTUP_GRACE_MS = 1e4;
3406
3602
  if (now - taskCtx.registeredAt < STARTUP_GRACE_MS) {
@@ -3420,7 +3616,7 @@ class SwarmCoordinator {
3420
3616
  }
3421
3617
  } catch {}
3422
3618
  }
3423
- this.sendChatMessage(`[${taskCtx.label}] Running ${toolDesc}.${urlSuffix} The agent is working outside the terminal — I'll let it finish.`, "coding-agent");
3619
+ this.log(`[${taskCtx.label}] Running ${toolDesc}.${urlSuffix} The agent is working outside the terminal.`);
3424
3620
  }
3425
3621
  break;
3426
3622
  }
@@ -3703,21 +3899,55 @@ class PTYService {
3703
3899
  this.log(`Failed to write approval config: ${err}`);
3704
3900
  }
3705
3901
  }
3902
+ const hookUrl = `http://localhost:${this.runtime.getSetting("SERVER_PORT") ?? "2138"}/api/coding-agents/hooks`;
3706
3903
  if (resolvedAgentType === "claude") {
3707
3904
  try {
3708
- const settingsPath = join(workdir, ".claude", "settings.json");
3905
+ const settingsPath = join2(workdir, ".claude", "settings.json");
3709
3906
  let settings = {};
3710
3907
  try {
3711
- settings = JSON.parse(await readFile(settingsPath, "utf-8"));
3908
+ settings = JSON.parse(await readFile2(settingsPath, "utf-8"));
3712
3909
  } catch {}
3713
3910
  const permissions = settings.permissions ?? {};
3714
3911
  permissions.allowedDirectories = [workdir];
3715
3912
  settings.permissions = permissions;
3913
+ const adapter = this.getAdapter("claude");
3914
+ const hookProtocol = adapter.getHookTelemetryProtocol({
3915
+ httpUrl: hookUrl,
3916
+ sessionId
3917
+ });
3918
+ if (hookProtocol) {
3919
+ const existingHooks = settings.hooks ?? {};
3920
+ settings.hooks = { ...existingHooks, ...hookProtocol.settingsHooks };
3921
+ this.log(`Injecting HTTP hooks for session ${sessionId}`);
3922
+ }
3716
3923
  await mkdir(dirname(settingsPath), { recursive: true });
3717
- await writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
3924
+ await writeFile2(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
3718
3925
  this.log(`Wrote allowedDirectories [${workdir}] to ${settingsPath}`);
3719
3926
  } catch (err) {
3720
- this.log(`Failed to write allowedDirectories: ${err}`);
3927
+ this.log(`Failed to write Claude settings: ${err}`);
3928
+ }
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}`);
3721
3951
  }
3722
3952
  }
3723
3953
  const spawnConfig = buildSpawnConfig(sessionId, {
@@ -3861,6 +4091,42 @@ class PTYService {
3861
4091
  const session = this.getSession(sessionId);
3862
4092
  return session?.status === "authenticating";
3863
4093
  }
4094
+ findSessionIdByCwd(cwd) {
4095
+ for (const [sessionId, workdir] of this.sessionWorkdirs) {
4096
+ if (workdir === cwd)
4097
+ return sessionId;
4098
+ }
4099
+ return;
4100
+ }
4101
+ handleHookEvent(sessionId, event, data) {
4102
+ const summary = event === "tool_running" ? `tool=${data.toolName ?? "?"}` : event === "permission_approved" ? `tool=${data.tool ?? "?"}` : JSON.stringify(data);
4103
+ if (event === "tool_running" || event === "permission_approved") {
4104
+ logger2.debug(`[PTYService] Hook event for ${sessionId}: ${event} ${summary}`);
4105
+ } else {
4106
+ this.log(`Hook event for ${sessionId}: ${event} ${summary}`);
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
+ }
4111
+ switch (event) {
4112
+ case "tool_running":
4113
+ this.emitEvent(sessionId, "tool_running", data);
4114
+ break;
4115
+ case "task_complete":
4116
+ this.emitEvent(sessionId, "task_complete", data);
4117
+ break;
4118
+ case "permission_approved":
4119
+ break;
4120
+ case "notification":
4121
+ this.emitEvent(sessionId, "message", data);
4122
+ break;
4123
+ case "session_end":
4124
+ this.emitEvent(sessionId, "stopped", { ...data, reason: "session_end" });
4125
+ break;
4126
+ default:
4127
+ break;
4128
+ }
4129
+ }
3864
4130
  async checkAvailableAgents(types) {
3865
4131
  const agentTypes = types ?? ["claude", "gemini", "codex", "aider"];
3866
4132
  return checkAdapters(agentTypes);
@@ -3988,9 +4254,7 @@ class PTYService {
3988
4254
  return this.metricsTracker.getAll();
3989
4255
  }
3990
4256
  log(message) {
3991
- if (this.serviceConfig.debug) {
3992
- logger2.debug(`[PTYService] ${message}`);
3993
- }
4257
+ logger2.debug(`[PTYService] ${message}`);
3994
4258
  }
3995
4259
  }
3996
4260
 
@@ -4249,7 +4513,8 @@ var spawnAgentAction = {
4249
4513
 
4250
4514
  // src/actions/coding-task-handlers.ts
4251
4515
  import {
4252
- logger as logger5
4516
+ logger as logger5,
4517
+ ModelType as ModelType5
4253
4518
  } from "@elizaos/core";
4254
4519
 
4255
4520
  // src/actions/coding-task-helpers.ts
@@ -4325,6 +4590,65 @@ ${preview}` : `Agent "${label}" completed the task.`
4325
4590
 
4326
4591
  // src/actions/coding-task-handlers.ts
4327
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
+ }
4328
4652
  async function handleMultiAgent(ctx, agentsParam) {
4329
4653
  const {
4330
4654
  runtime,
@@ -4372,6 +4696,13 @@ async function handleMultiAgent(ctx, agentsParam) {
4372
4696
  text: `Launching ${agentSpecs.length} agents${repo ? ` on ${repo}` : ""}...`
4373
4697
  });
4374
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
+ }
4375
4706
  const results = [];
4376
4707
  for (const [i, spec] of agentSpecs.entries()) {
4377
4708
  let specAgentType = defaultAgentType;
@@ -4381,51 +4712,14 @@ async function handleMultiAgent(ctx, agentsParam) {
4381
4712
  const colonIdx = spec.indexOf(":");
4382
4713
  if (ctx.agentSelectionStrategy !== "fixed" && colonIdx > 0 && colonIdx < 20) {
4383
4714
  const prefix = spec.slice(0, colonIdx).trim().toLowerCase();
4384
- const knownTypes = [
4385
- "claude",
4386
- "claude-code",
4387
- "claudecode",
4388
- "codex",
4389
- "openai",
4390
- "gemini",
4391
- "google",
4392
- "aider",
4393
- "pi",
4394
- "pi-ai",
4395
- "piai",
4396
- "pi-coding-agent",
4397
- "picodingagent",
4398
- "shell",
4399
- "bash"
4400
- ];
4401
- if (knownTypes.includes(prefix)) {
4715
+ if (KNOWN_AGENT_PREFIXES.includes(prefix)) {
4402
4716
  specRequestedType = prefix;
4403
4717
  specPiRequested = isPiAgentType(prefix);
4404
4718
  specAgentType = normalizeAgentType(prefix);
4405
4719
  specTask = spec.slice(colonIdx + 1).trim();
4406
4720
  }
4407
4721
  } else if (ctx.agentSelectionStrategy === "fixed" && colonIdx > 0 && colonIdx < 20) {
4408
- const prefix = spec.slice(0, colonIdx).trim().toLowerCase();
4409
- const knownTypes = [
4410
- "claude",
4411
- "claude-code",
4412
- "claudecode",
4413
- "codex",
4414
- "openai",
4415
- "gemini",
4416
- "google",
4417
- "aider",
4418
- "pi",
4419
- "pi-ai",
4420
- "piai",
4421
- "pi-coding-agent",
4422
- "picodingagent",
4423
- "shell",
4424
- "bash"
4425
- ];
4426
- if (knownTypes.includes(prefix)) {
4427
- specTask = spec.slice(colonIdx + 1).trim();
4428
- }
4722
+ specTask = stripAgentPrefix(spec);
4429
4723
  }
4430
4724
  const specLabel = explicitLabel ? `${explicitLabel}-${i + 1}` : generateLabel(repo, specTask);
4431
4725
  try {
@@ -4458,7 +4752,12 @@ async function handleMultiAgent(ctx, agentsParam) {
4458
4752
  }
4459
4753
  }
4460
4754
  const coordinator = getCoordinator(runtime);
4461
- 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;
4462
4761
  const displayType = specPiRequested ? "pi" : specAgentType;
4463
4762
  const session = await ptyService.spawnSession({
4464
4763
  name: `coding-${Date.now()}-${i}`,
@@ -6247,6 +6546,132 @@ async function handleCoordinatorRoutes(req, res, pathname, ctx) {
6247
6546
  return false;
6248
6547
  }
6249
6548
 
6549
+ // src/api/hook-routes.ts
6550
+ async function handleHookRoutes(req, res, pathname, ctx) {
6551
+ if (pathname !== "/api/coding-agents/hooks")
6552
+ return false;
6553
+ const method = req.method?.toUpperCase();
6554
+ if (method !== "POST") {
6555
+ sendError(res, "Method not allowed", 405);
6556
+ return true;
6557
+ }
6558
+ if (!ctx.ptyService) {
6559
+ sendError(res, "PTY Service not available", 503);
6560
+ return true;
6561
+ }
6562
+ let body;
6563
+ try {
6564
+ body = await parseBody(req);
6565
+ } catch (err) {
6566
+ sendError(res, err instanceof Error ? err.message : "Failed to parse request body", 400);
6567
+ return true;
6568
+ }
6569
+ const payload = body;
6570
+ const eventName = payload.hook_event_name;
6571
+ if (!eventName) {
6572
+ sendError(res, "Missing hook_event_name", 400);
6573
+ return true;
6574
+ }
6575
+ const toolName = payload.tool_name ?? payload.toolName;
6576
+ const notificationType = payload.notification_type ?? payload.notificationType;
6577
+ const headerSessionId = req.headers["x-parallax-session-id"];
6578
+ const sessionId = headerSessionId ? headerSessionId : payload.cwd ? ctx.ptyService.findSessionIdByCwd(payload.cwd) : undefined;
6579
+ if (!sessionId) {
6580
+ sendJson(res, { status: "ignored", reason: "session_not_found" });
6581
+ return true;
6582
+ }
6583
+ switch (eventName) {
6584
+ case "PermissionRequest": {
6585
+ sendJson(res, {
6586
+ hookSpecificOutput: {
6587
+ hookEventName: "PermissionRequest",
6588
+ decision: { behavior: "allow" }
6589
+ }
6590
+ });
6591
+ ctx.ptyService.handleHookEvent(sessionId, "permission_approved", {
6592
+ tool: toolName
6593
+ });
6594
+ return true;
6595
+ }
6596
+ case "PreToolUse": {
6597
+ ctx.ptyService.handleHookEvent(sessionId, "tool_running", {
6598
+ toolName,
6599
+ source: "hook"
6600
+ });
6601
+ sendJson(res, {
6602
+ hookSpecificOutput: {
6603
+ hookEventName: "PreToolUse",
6604
+ permissionDecision: "allow"
6605
+ }
6606
+ });
6607
+ return true;
6608
+ }
6609
+ case "Stop": {
6610
+ ctx.ptyService.handleHookEvent(sessionId, "task_complete", {
6611
+ source: "hook"
6612
+ });
6613
+ sendJson(res, {});
6614
+ return true;
6615
+ }
6616
+ case "TaskCompleted": {
6617
+ ctx.ptyService.handleHookEvent(sessionId, "task_complete", {
6618
+ source: "hook_task_completed"
6619
+ });
6620
+ sendJson(res, {});
6621
+ return true;
6622
+ }
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": {
6640
+ ctx.ptyService.handleHookEvent(sessionId, "task_complete", {
6641
+ source: "gemini_hook"
6642
+ });
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 });
6666
+ return true;
6667
+ }
6668
+ default: {
6669
+ sendJson(res, { status: "ignored", reason: "unknown_event" });
6670
+ return true;
6671
+ }
6672
+ }
6673
+ }
6674
+
6250
6675
  // src/api/issue-routes.ts
6251
6676
  async function handleIssueRoutes(req, res, pathname, ctx) {
6252
6677
  const method = req.method?.toUpperCase();
@@ -6509,6 +6934,9 @@ function sendError(res, message, status = 400) {
6509
6934
  sendJson(res, { error: message }, status);
6510
6935
  }
6511
6936
  async function handleCodingAgentRoutes(req, res, pathname, ctx) {
6937
+ if (await handleHookRoutes(req, res, pathname, ctx)) {
6938
+ return true;
6939
+ }
6512
6940
  if (await handleCoordinatorRoutes(req, res, pathname, ctx)) {
6513
6941
  return true;
6514
6942
  }
@@ -6578,5 +7006,5 @@ export {
6578
7006
  CodingWorkspaceService
6579
7007
  };
6580
7008
 
6581
- //# debugId=87DC30F17636D1DA64756E2164756E21
7009
+ //# debugId=D706621831E2790E64756E2164756E21
6582
7010
  //# sourceMappingURL=index.js.map