@elizaos/plugin-agent-orchestrator 0.3.9 → 0.3.11

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
@@ -98,8 +98,64 @@ var init_ansi_utils = __esm(() => {
98
98
  STATUS_LINE = /^\s*(?:\d+[smh]\s+\d+s?\s*·|↓\s*[\d.]+k?\s*tokens|·\s*↓|esc\s+to\s+interrupt|[Uu]pdate available|ate available|Run:\s+brew|brew\s+upgrade|\d+\s+files?\s+\+\d+\s+-\d+|ctrl\+\w|\+\d+\s+lines|Wrote\s+\d+\s+lines\s+to|\?\s+for\s+shortcuts|Cooked for|Baked for|Cogitated for)/i;
99
99
  });
100
100
 
101
+ // src/services/trajectory-context.ts
102
+ function setTrajectoryContext(runtime, ctx) {
103
+ runtime[CTX_KEY] = ctx;
104
+ }
105
+ function clearTrajectoryContext(runtime) {
106
+ runtime[CTX_KEY] = undefined;
107
+ }
108
+ async function withTrajectoryContext(runtime, ctx, fn) {
109
+ setTrajectoryContext(runtime, ctx);
110
+ try {
111
+ return await fn();
112
+ } finally {
113
+ clearTrajectoryContext(runtime);
114
+ }
115
+ }
116
+ var CTX_KEY = "__orchestratorTrajectoryCtx";
117
+
101
118
  // src/services/swarm-coordinator-prompts.ts
102
- function buildCoordinationPrompt(taskCtx, promptText, recentOutput, decisionHistory) {
119
+ function buildSiblingSection(siblings) {
120
+ if (!siblings || siblings.length === 0)
121
+ return "";
122
+ const lines = siblings.map((s) => {
123
+ let line = ` - [${s.status}] "${s.label}" (${s.agentType}): ${s.originalTask}`;
124
+ if (s.completionSummary) {
125
+ line += `
126
+ Result: ${s.completionSummary}`;
127
+ } else if (s.lastKeyDecision) {
128
+ line += `
129
+ Latest: ${s.lastKeyDecision}`;
130
+ }
131
+ return line;
132
+ });
133
+ return `
134
+ Other agents in this swarm:
135
+ ` + lines.join(`
136
+ `) + `
137
+ Use this context when the agent asks creative or architectural questions — ` + `your answer should be consistent with what sibling agents are doing.
138
+ `;
139
+ }
140
+ function buildSharedDecisionsSection(decisions) {
141
+ if (!decisions || decisions.length === 0)
142
+ return "";
143
+ return `
144
+ Key decisions made by other agents in this swarm:
145
+ ` + decisions.slice(-10).map((d) => ` - [${d.agentLabel}] ${d.summary}`).join(`
146
+ `) + `
147
+ Align with these decisions for consistency — don't contradict them unless the task requires it.
148
+ `;
149
+ }
150
+ function buildSwarmContextSection(swarmContext) {
151
+ if (!swarmContext)
152
+ return "";
153
+ return `
154
+ Project context (from planning phase):
155
+ ${swarmContext}
156
+ `;
157
+ }
158
+ function buildCoordinationPrompt(taskCtx, promptText, recentOutput, decisionHistory, siblingTasks, sharedDecisions, swarmContext) {
103
159
  const historySection = decisionHistory.length > 0 ? `
104
160
  Previous decisions for this session:
105
161
  ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] prompt="${d.promptText}" → ${d.action}${d.response ? ` ("${d.response}")` : ""} — ${d.reasoning}`).join(`
@@ -110,7 +166,7 @@ ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] prompt="${d.
110
166
  ` + `Original task: "${taskCtx.originalTask}"
111
167
  ` + `Working directory: ${taskCtx.workdir}
112
168
  ` + `Repository: ${taskCtx.repo ?? "none (scratch directory)"}
113
- ` + historySection + `
169
+ ` + buildSwarmContextSection(swarmContext) + buildSiblingSection(siblingTasks) + buildSharedDecisionsSection(sharedDecisions) + historySection + `
114
170
  Recent terminal output (last 50 lines):
115
171
  ` + `---
116
172
  ${recentOutput.slice(-3000)}
@@ -139,11 +195,13 @@ ${recentOutput.slice(-3000)}
139
195
  ` + `- Only use "complete" if the agent confirmed it verified ALL test plan items after creating the PR.
140
196
  ` + `- 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
197
  ` + `- When in doubt, escalate — it's better to ask the human than to make a wrong choice.
198
+ ` + `- 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.
199
+ ` + `- Look for explicit "DECISION:" markers in the agent's output — these are the agent deliberately ` + `surfacing design choices. Always capture these as keyDecision.
142
200
 
143
201
  ` + `Respond with ONLY a JSON object:
144
- ` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "..."}`;
202
+ ` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "...", "keyDecision": "..."}`;
145
203
  }
146
- function buildIdleCheckPrompt(taskCtx, recentOutput, idleMinutes, idleCheckNumber, maxIdleChecks, decisionHistory) {
204
+ function buildIdleCheckPrompt(taskCtx, recentOutput, idleMinutes, idleCheckNumber, maxIdleChecks, decisionHistory, siblingTasks, sharedDecisions, swarmContext) {
147
205
  const historySection = decisionHistory.length > 0 ? `
148
206
  Previous decisions for this session:
149
207
  ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] prompt="${d.promptText}" → ${d.action}${d.response ? ` ("${d.response}")` : ""} — ${d.reasoning}`).join(`
@@ -155,7 +213,7 @@ ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] prompt="${d.
155
213
  ` + `Working directory: ${taskCtx.workdir}
156
214
  ` + `Repository: ${taskCtx.repo ?? "none (scratch directory)"}
157
215
  ` + `Idle check: ${idleCheckNumber} of ${maxIdleChecks} (session will be force-escalated after ${maxIdleChecks})
158
- ` + historySection + `
216
+ ` + buildSwarmContextSection(swarmContext) + buildSiblingSection(siblingTasks) + buildSharedDecisionsSection(sharedDecisions) + historySection + `
159
217
  Recent terminal output (last 50 lines):
160
218
  ` + `---
161
219
  ${recentOutput.slice(-3000)}
@@ -179,11 +237,13 @@ ${recentOutput.slice(-3000)}
179
237
  ` + `- If the output shows an error or the agent seems stuck in a loop, escalate.
180
238
  ` + `- If the agent is clearly mid-operation (build output, test runner, git operations), use "ignore".
181
239
  ` + `- On check ${idleCheckNumber} of ${maxIdleChecks} — if unsure, lean toward "respond" with a nudge rather than "complete".
240
+ ` + `- If the agent's output reveals a significant creative or architectural decision, ` + `include "keyDecision" with a brief one-line summary.
241
+ ` + `- Look for explicit "DECISION:" markers in the agent's output — always capture these as keyDecision.
182
242
 
183
243
  ` + `Respond with ONLY a JSON object:
184
- ` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "..."}`;
244
+ ` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "...", "keyDecision": "..."}`;
185
245
  }
186
- function buildTurnCompletePrompt(taskCtx, turnOutput, decisionHistory) {
246
+ function buildTurnCompletePrompt(taskCtx, turnOutput, decisionHistory, siblingTasks, sharedDecisions, swarmContext) {
187
247
  const historySection = decisionHistory.length > 0 ? `
188
248
  Previous decisions for this session:
189
249
  ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] prompt="${d.promptText}" → ${d.action}${d.response ? ` ("${d.response}")` : ""} — ${d.reasoning}`).join(`
@@ -194,7 +254,7 @@ ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] prompt="${d.
194
254
  ` + `Original task: "${taskCtx.originalTask}"
195
255
  ` + `Working directory: ${taskCtx.workdir}
196
256
  ` + `Repository: ${taskCtx.repo ?? "none (scratch directory)"}
197
- ` + historySection + `
257
+ ` + buildSwarmContextSection(swarmContext) + buildSiblingSection(siblingTasks) + buildSharedDecisionsSection(sharedDecisions) + historySection + `
198
258
  Output from this turn:
199
259
  ` + `---
200
260
  ${turnOutput.slice(-3000)}
@@ -227,11 +287,13 @@ ${turnOutput.slice(-3000)}
227
287
  ` + `- Keep follow-up instructions concise and specific.
228
288
  ` + `- 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
289
  ` + `- Default to "respond" — only use "complete" when you're certain ALL work is done.
290
+ ` + `- 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.
291
+ ` + `- Look for explicit "DECISION:" markers in the agent's output — these are the agent deliberately ` + `surfacing design choices. Always capture these as keyDecision.
230
292
 
231
293
  ` + `Respond with ONLY a JSON object:
232
- ` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "..."}`;
294
+ ` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "...", "keyDecision": "..."}`;
233
295
  }
234
- function buildBlockedEventMessage(taskCtx, promptText, recentOutput, decisionHistory) {
296
+ function buildBlockedEventMessage(taskCtx, promptText, recentOutput, decisionHistory, siblingTasks, sharedDecisions, swarmContext) {
235
297
  const historySection = decisionHistory.length > 0 ? `
236
298
  Previous decisions:
237
299
  ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] "${d.promptText}" → ${d.action}${d.response ? ` ("${d.response}")` : ""} — ${d.reasoning}`).join(`
@@ -242,7 +304,7 @@ ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] "${d.promptT
242
304
  ` + `Task: "${taskCtx.originalTask}"
243
305
  ` + `Workdir: ${taskCtx.workdir}
244
306
  ` + `Repo: ${taskCtx.repo ?? "none (scratch directory)"}
245
- ` + historySection + `
307
+ ` + buildSwarmContextSection(swarmContext) + buildSiblingSection(siblingTasks) + buildSharedDecisionsSection(sharedDecisions) + historySection + `
246
308
  Recent terminal output:
247
309
  ---
248
310
  ${recentOutput.slice(-3000)}
@@ -263,11 +325,14 @@ ${recentOutput.slice(-3000)}
263
325
  ` + `- If a PR was just created, respond to review & verify test plan items before completing.
264
326
  ` + `- When in doubt, escalate.
265
327
 
328
+ ` + `If the agent's output reveals a significant decision that sibling agents should know about, include "keyDecision" with a brief summary.
329
+ ` + `Look for explicit "DECISION:" markers in the agent's output — always capture these as keyDecision.
330
+
266
331
  ` + `Include a JSON action block at the end of your response:
267
- ` + "```json\n" + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "..."}
332
+ ` + "```json\n" + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "...", "keyDecision": "..."}
268
333
  ` + "```";
269
334
  }
270
- function buildTurnCompleteEventMessage(taskCtx, turnOutput, decisionHistory) {
335
+ function buildTurnCompleteEventMessage(taskCtx, turnOutput, decisionHistory, siblingTasks, sharedDecisions, swarmContext) {
271
336
  const historySection = decisionHistory.length > 0 ? `
272
337
  Previous decisions:
273
338
  ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] "${d.promptText}" → ${d.action}${d.response ? ` ("${d.response}")` : ""} — ${d.reasoning}`).join(`
@@ -278,7 +343,7 @@ ${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] "${d.promptT
278
343
  ` + `Task: "${taskCtx.originalTask}"
279
344
  ` + `Workdir: ${taskCtx.workdir}
280
345
  ` + `Repo: ${taskCtx.repo ?? "none (scratch directory)"}
281
- ` + historySection + `
346
+ ` + buildSwarmContextSection(swarmContext) + buildSiblingSection(siblingTasks) + buildSharedDecisionsSection(sharedDecisions) + historySection + `
282
347
  Turn output:
283
348
  ---
284
349
  ${turnOutput.slice(-3000)}
@@ -298,9 +363,11 @@ ${turnOutput.slice(-3000)}
298
363
  ` + `- If a PR was just created, respond to review & verify test plan items.
299
364
  ` + `- When asking agents to verify work, prefer CLI tools (gh, curl, cat, etc.) over browser automation.
300
365
  ` + `- Default to "respond" — only "complete" when certain ALL work is done.
366
+ ` + `- If the agent's output reveals a significant creative or architectural decision, include "keyDecision" with a brief summary.
367
+ ` + `- Look for explicit "DECISION:" markers in the agent's output — always capture these as keyDecision.
301
368
 
302
369
  ` + `Include a JSON action block at the end of your response:
303
- ` + "```json\n" + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "..."}
370
+ ` + "```json\n" + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "...", "keyDecision": "..."}
304
371
  ` + "```";
305
372
  }
306
373
  function parseCoordinationResponse(llmOutput) {
@@ -326,6 +393,9 @@ function parseCoordinationResponse(llmOutput) {
326
393
  return null;
327
394
  }
328
395
  }
396
+ if (typeof parsed.keyDecision === "string" && parsed.keyDecision.trim()) {
397
+ result.keyDecision = parsed.keyDecision.trim().slice(0, 240);
398
+ }
329
399
  return result;
330
400
  } catch {
331
401
  return null;
@@ -392,7 +462,7 @@ async function classifyEventTier(runtime, ctx, log) {
392
462
  }
393
463
  try {
394
464
  const prompt = buildTriagePrompt(ctx);
395
- const result = await runtime.useModel(ModelType2.TEXT_SMALL, { prompt });
465
+ const result = await withTrajectoryContext(runtime, { source: "orchestrator", decisionType: "event-triage" }, () => runtime.useModel(ModelType2.TEXT_SMALL, { prompt }));
396
466
  const tier = parseTriageResponse(result);
397
467
  if (tier) {
398
468
  log(`Triage: LLM → ${tier}`);
@@ -502,6 +572,74 @@ function toDecisionHistory(taskCtx) {
502
572
  reasoning: d.reasoning
503
573
  }));
504
574
  }
575
+ function collectSiblings(ctx, currentSessionId) {
576
+ const siblings = [];
577
+ for (const [sid, task] of ctx.tasks) {
578
+ if (sid === currentSessionId)
579
+ continue;
580
+ let lastKeyDecision;
581
+ for (let i = task.decisions.length - 1;i >= 0; i--) {
582
+ const d = task.decisions[i];
583
+ if (d.reasoning && d.decision !== "auto_resolved") {
584
+ lastKeyDecision = d.reasoning;
585
+ break;
586
+ }
587
+ }
588
+ for (let i = ctx.sharedDecisions.length - 1;i >= 0; i--) {
589
+ const sd = ctx.sharedDecisions[i];
590
+ if (sd.agentLabel === task.label) {
591
+ lastKeyDecision = sd.summary;
592
+ break;
593
+ }
594
+ }
595
+ siblings.push({
596
+ label: task.label,
597
+ agentType: task.agentType,
598
+ originalTask: task.originalTask,
599
+ status: task.status,
600
+ lastKeyDecision,
601
+ completionSummary: task.completionSummary
602
+ });
603
+ }
604
+ return siblings;
605
+ }
606
+ function enrichWithSharedDecisions(ctx, sessionId, response) {
607
+ const taskCtx = ctx.tasks.get(sessionId);
608
+ if (!taskCtx)
609
+ return { response };
610
+ const allDecisions = ctx.sharedDecisions;
611
+ const lastSeen = taskCtx.lastSeenDecisionIndex;
612
+ const snapshotEnd = allDecisions.length;
613
+ if (lastSeen >= snapshotEnd)
614
+ return { response };
615
+ if (response.length < 20) {
616
+ return { response };
617
+ }
618
+ const unseen = allDecisions.slice(lastSeen, snapshotEnd);
619
+ const contextBlock = unseen.map((d) => `[${d.agentLabel}] ${d.summary}`).join("; ");
620
+ return {
621
+ response: `${response}
622
+
623
+ (Context from other agents: ${contextBlock})`,
624
+ snapshotIndex: snapshotEnd
625
+ };
626
+ }
627
+ function commitSharedDecisionIndex(ctx, sessionId, snapshotIndex) {
628
+ const taskCtx = ctx.tasks.get(sessionId);
629
+ if (taskCtx) {
630
+ taskCtx.lastSeenDecisionIndex = snapshotIndex;
631
+ }
632
+ }
633
+ function recordKeyDecision(ctx, agentLabel, decision) {
634
+ if (!decision.keyDecision)
635
+ return;
636
+ ctx.sharedDecisions.push({
637
+ agentLabel,
638
+ summary: decision.keyDecision,
639
+ timestamp: Date.now()
640
+ });
641
+ ctx.log(`Shared decision from "${agentLabel}": ${decision.keyDecision}`);
642
+ }
505
643
  async function drainPendingTurnComplete(ctx, sessionId) {
506
644
  if (!ctx.pendingTurnComplete.has(sessionId))
507
645
  return;
@@ -513,6 +651,17 @@ async function drainPendingTurnComplete(ctx, sessionId) {
513
651
  ctx.log(`Draining buffered turn-complete for "${taskCtx.label}"`);
514
652
  await handleTurnComplete(ctx, sessionId, taskCtx, pendingData);
515
653
  }
654
+ async function drainPendingBlocked(ctx, sessionId) {
655
+ if (!ctx.pendingBlocked.has(sessionId))
656
+ return;
657
+ const pendingData = ctx.pendingBlocked.get(sessionId);
658
+ ctx.pendingBlocked.delete(sessionId);
659
+ const taskCtx = ctx.tasks.get(sessionId);
660
+ if (!taskCtx || taskCtx.status !== "active")
661
+ return;
662
+ ctx.log(`Draining buffered blocked event for "${taskCtx.label}"`);
663
+ await handleBlocked(ctx, sessionId, taskCtx, pendingData);
664
+ }
516
665
  function formatDecisionResponse(decision) {
517
666
  if (decision.action !== "respond")
518
667
  return;
@@ -542,8 +691,16 @@ function checkAllTasksComplete(ctx) {
542
691
  return;
543
692
  const terminalStates = new Set(["completed", "stopped", "error"]);
544
693
  const allDone = tasks.every((t) => terminalStates.has(t.status));
545
- if (!allDone)
694
+ if (!allDone) {
695
+ const statuses = tasks.map((t) => `${t.label}=${t.status}`).join(", ");
696
+ ctx.log(`checkAllTasksComplete: not all done yet — ${statuses}`);
697
+ return;
698
+ }
699
+ if (ctx.swarmCompleteNotified) {
700
+ ctx.log("checkAllTasksComplete: already notified — skipping");
546
701
  return;
702
+ }
703
+ ctx.swarmCompleteNotified = true;
547
704
  const completed = tasks.filter((t) => t.status === "completed");
548
705
  const stopped = tasks.filter((t) => t.status === "stopped");
549
706
  const errored = tasks.filter((t) => t.status === "error");
@@ -557,7 +714,7 @@ function checkAllTasksComplete(ctx) {
557
714
  if (errored.length > 0) {
558
715
  parts.push(`${errored.length} errored`);
559
716
  }
560
- ctx.sendChatMessage(`All ${tasks.length} coding agents finished (${parts.join(", ")}). Review their work when you're ready.`, "coding-agent");
717
+ ctx.log(`checkAllTasksComplete: all ${tasks.length} tasks terminal (${parts.join(", ")}) firing swarm_complete`);
561
718
  ctx.broadcast({
562
719
  type: "swarm_complete",
563
720
  sessionId: "",
@@ -569,6 +726,34 @@ function checkAllTasksComplete(ctx) {
569
726
  errored: errored.length
570
727
  }
571
728
  });
729
+ const swarmCompleteCb = ctx.getSwarmCompleteCallback();
730
+ const sendFallbackSummary = () => {
731
+ ctx.sendChatMessage(`All ${tasks.length} coding agents finished (${parts.join(", ")}). Review their work when you're ready.`, "coding-agent");
732
+ };
733
+ if (swarmCompleteCb) {
734
+ ctx.log("checkAllTasksComplete: swarm complete callback is wired — calling synthesis");
735
+ const taskSummaries = tasks.map((t) => ({
736
+ sessionId: t.sessionId,
737
+ label: t.label,
738
+ agentType: t.agentType,
739
+ originalTask: t.originalTask,
740
+ status: t.status,
741
+ completionSummary: t.completionSummary ?? ""
742
+ }));
743
+ withTimeout(Promise.resolve().then(() => swarmCompleteCb({
744
+ tasks: taskSummaries,
745
+ total: tasks.length,
746
+ completed: completed.length,
747
+ stopped: stopped.length,
748
+ errored: errored.length
749
+ })), DECISION_CB_TIMEOUT_MS, "swarmCompleteCb").catch((err) => {
750
+ ctx.log(`Swarm complete callback failed: ${err} — falling back to generic summary`);
751
+ sendFallbackSummary();
752
+ });
753
+ } else {
754
+ ctx.log("checkAllTasksComplete: no synthesis callback — sending generic message");
755
+ sendFallbackSummary();
756
+ }
572
757
  }
573
758
  async function fetchRecentOutput(ctx, sessionId, lines = 50) {
574
759
  if (!ctx.ptyService)
@@ -580,11 +765,17 @@ async function fetchRecentOutput(ctx, sessionId, lines = 50) {
580
765
  }
581
766
  }
582
767
  async function makeCoordinationDecision(ctx, taskCtx, promptText, recentOutput) {
583
- const prompt = buildCoordinationPrompt(toContextSummary(taskCtx), promptText, recentOutput, toDecisionHistory(taskCtx));
768
+ const prompt = buildCoordinationPrompt(toContextSummary(taskCtx), promptText, recentOutput, toDecisionHistory(taskCtx), collectSiblings(ctx, taskCtx.sessionId), ctx.sharedDecisions, ctx.getSwarmContext());
584
769
  try {
585
- const result = await ctx.runtime.useModel(ModelType3.TEXT_SMALL, {
586
- prompt
587
- });
770
+ const result = await withTrajectoryContext(ctx.runtime, {
771
+ source: "orchestrator",
772
+ decisionType: "coordination",
773
+ sessionId: taskCtx.sessionId,
774
+ taskLabel: taskCtx.label,
775
+ repo: taskCtx.repo,
776
+ workdir: taskCtx.workdir,
777
+ originalTask: taskCtx.originalTask
778
+ }, () => ctx.runtime.useModel(ModelType3.TEXT_SMALL, { prompt }));
588
779
  return parseCoordinationResponse(result);
589
780
  } catch (err) {
590
781
  ctx.log(`LLM coordination call failed: ${err}`);
@@ -599,7 +790,11 @@ async function executeDecision(ctx, sessionId, decision) {
599
790
  if (decision.useKeys && decision.keys) {
600
791
  await ctx.ptyService.sendKeysToSession(sessionId, decision.keys);
601
792
  } else if (decision.response !== undefined) {
602
- await ctx.ptyService.sendToSession(sessionId, decision.response);
793
+ const { response: enriched, snapshotIndex } = enrichWithSharedDecisions(ctx, sessionId, decision.response);
794
+ await ctx.ptyService.sendToSession(sessionId, enriched);
795
+ if (snapshotIndex !== undefined) {
796
+ commitSharedDecisionIndex(ctx, sessionId, snapshotIndex);
797
+ }
603
798
  }
604
799
  break;
605
800
  case "complete": {
@@ -618,6 +813,9 @@ async function executeDecision(ctx, sessionId, decision) {
618
813
  const rawOutput = await ctx.ptyService.getSessionOutput(sessionId, 50);
619
814
  summary = extractCompletionSummary(rawOutput);
620
815
  } catch {}
816
+ if (taskCtx) {
817
+ taskCtx.completionSummary = summary || decision.reasoning || "";
818
+ }
621
819
  ctx.sendChatMessage(summary ? `Finished "${taskCtx?.label ?? sessionId}".
622
820
 
623
821
  ${summary}` : `Finished "${taskCtx?.label ?? sessionId}".`, "coding-agent");
@@ -695,6 +893,18 @@ async function handleBlocked(ctx, sessionId, taskCtx, data) {
695
893
  }
696
894
  return;
697
895
  }
896
+ const promptFingerprint = promptText.slice(0, 200);
897
+ if (ctx.inFlightDecisions.has(sessionId)) {
898
+ if (ctx.lastBlockedPromptFingerprint.get(sessionId) === promptFingerprint) {
899
+ ctx.log(`Skipping duplicate blocked event for ${taskCtx.label} (decision in-flight, same prompt)`);
900
+ return;
901
+ }
902
+ ctx.log(`New blocked prompt for ${taskCtx.label} while decision in-flight — buffering`);
903
+ ctx.pendingBlocked.set(sessionId, data);
904
+ ctx.lastBlockedPromptFingerprint.set(sessionId, promptFingerprint);
905
+ return;
906
+ }
907
+ ctx.lastBlockedPromptFingerprint.set(sessionId, promptFingerprint);
698
908
  ctx.broadcast({
699
909
  type: "blocked",
700
910
  sessionId,
@@ -759,11 +969,17 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
759
969
  }
760
970
  let decision = null;
761
971
  const decisionFromPipeline = false;
762
- const prompt = buildTurnCompletePrompt(toContextSummary(taskCtx), turnOutput, toDecisionHistory(taskCtx));
972
+ const prompt = buildTurnCompletePrompt(toContextSummary(taskCtx), turnOutput, toDecisionHistory(taskCtx), collectSiblings(ctx, sessionId), ctx.sharedDecisions, ctx.getSwarmContext());
763
973
  try {
764
- const result = await ctx.runtime.useModel(ModelType3.TEXT_SMALL, {
765
- prompt
766
- });
974
+ const result = await withTrajectoryContext(ctx.runtime, {
975
+ source: "orchestrator",
976
+ decisionType: "turn-complete",
977
+ sessionId,
978
+ taskLabel: taskCtx.label,
979
+ repo: taskCtx.repo,
980
+ workdir: taskCtx.workdir,
981
+ originalTask: taskCtx.originalTask
982
+ }, () => ctx.runtime.useModel(ModelType3.TEXT_SMALL, { prompt }));
767
983
  decision = parseCoordinationResponse(result);
768
984
  } catch (err) {
769
985
  ctx.log(`Turn-complete LLM call failed: ${err}`);
@@ -784,6 +1000,7 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
784
1000
  response: formatDecisionResponse(decision),
785
1001
  reasoning: decision.reasoning
786
1002
  });
1003
+ recordKeyDecision(ctx, taskCtx.label, decision);
787
1004
  ctx.broadcast({
788
1005
  type: "turn_assessment",
789
1006
  sessionId,
@@ -806,6 +1023,7 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
806
1023
  } finally {
807
1024
  ctx.inFlightDecisions.delete(sessionId);
808
1025
  await drainPendingTurnComplete(ctx, sessionId);
1026
+ await drainPendingBlocked(ctx, sessionId);
809
1027
  }
810
1028
  }
811
1029
  async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, recentOutput, promptType) {
@@ -834,7 +1052,7 @@ async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, rec
834
1052
  decision = await makeCoordinationDecision(ctx, taskCtx, promptText, output);
835
1053
  } else {
836
1054
  if (agentDecisionCb) {
837
- const eventMessage = buildBlockedEventMessage(toContextSummary(taskCtx), promptText, output, toDecisionHistory(taskCtx));
1055
+ const eventMessage = buildBlockedEventMessage(toContextSummary(taskCtx), promptText, output, toDecisionHistory(taskCtx), collectSiblings(ctx, sessionId), ctx.sharedDecisions, ctx.getSwarmContext());
838
1056
  try {
839
1057
  decision = await withTimeout(agentDecisionCb(eventMessage, sessionId, taskCtx), DECISION_CB_TIMEOUT_MS, "agentDecisionCb");
840
1058
  if (decision)
@@ -882,6 +1100,7 @@ async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, rec
882
1100
  response: formatDecisionResponse(decision),
883
1101
  reasoning: decision.reasoning
884
1102
  });
1103
+ recordKeyDecision(ctx, taskCtx.label, decision);
885
1104
  taskCtx.autoResolvedCount = 0;
886
1105
  ctx.broadcast({
887
1106
  type: "coordination_decision",
@@ -908,6 +1127,7 @@ async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, rec
908
1127
  } finally {
909
1128
  ctx.inFlightDecisions.delete(sessionId);
910
1129
  await drainPendingTurnComplete(ctx, sessionId);
1130
+ await drainPendingBlocked(ctx, sessionId);
911
1131
  }
912
1132
  }
913
1133
  async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recentOutput, promptType) {
@@ -934,7 +1154,7 @@ async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recent
934
1154
  decision = await makeCoordinationDecision(ctx, taskCtx, promptText, output);
935
1155
  } else {
936
1156
  if (agentDecisionCb) {
937
- const eventMessage = buildBlockedEventMessage(toContextSummary(taskCtx), promptText, output, toDecisionHistory(taskCtx));
1157
+ const eventMessage = buildBlockedEventMessage(toContextSummary(taskCtx), promptText, output, toDecisionHistory(taskCtx), collectSiblings(ctx, sessionId), ctx.sharedDecisions, ctx.getSwarmContext());
938
1158
  try {
939
1159
  decision = await withTimeout(agentDecisionCb(eventMessage, sessionId, taskCtx), DECISION_CB_TIMEOUT_MS, "agentDecisionCb");
940
1160
  if (decision)
@@ -984,6 +1204,7 @@ async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recent
984
1204
  } finally {
985
1205
  ctx.inFlightDecisions.delete(sessionId);
986
1206
  await drainPendingTurnComplete(ctx, sessionId);
1207
+ await drainPendingBlocked(ctx, sessionId);
987
1208
  }
988
1209
  }
989
1210
  var DECISION_CB_TIMEOUT_MS = 30000, MAX_AUTO_RESPONSES = 10;
@@ -2013,7 +2234,7 @@ import {
2013
2234
  } from "@elizaos/core";
2014
2235
 
2015
2236
  // src/services/pty-service.ts
2016
- import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2237
+ import { appendFile, mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2017
2238
  import { dirname, join as join2 } from "node:path";
2018
2239
  import { logger as logger2 } from "@elizaos/core";
2019
2240
  import {
@@ -2388,7 +2609,7 @@ async function stopSession(ctx, sessionId, sessionMetadata, sessionWorkdirs, log
2388
2609
  const workdir = sessionWorkdirs.get(sessionId);
2389
2610
  if (workdir) {
2390
2611
  try {
2391
- await cleanupClaudeHooks(workdir, log);
2612
+ await cleanupAgentHooks(workdir, log);
2392
2613
  } catch {}
2393
2614
  }
2394
2615
  sessionMetadata.delete(sessionId);
@@ -2398,17 +2619,27 @@ async function stopSession(ctx, sessionId, sessionMetadata, sessionWorkdirs, log
2398
2619
  log(`Stopped session ${sessionId}`);
2399
2620
  }
2400
2621
  }
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 {}
2622
+ async function cleanupAgentHooks(workdir, log) {
2623
+ const settingsPaths = [
2624
+ join(workdir, ".claude", "settings.json"),
2625
+ join(workdir, ".gemini", "settings.json")
2626
+ ];
2627
+ for (const settingsPath of settingsPaths) {
2628
+ try {
2629
+ const raw = await readFile(settingsPath, "utf-8");
2630
+ const settings = JSON.parse(raw);
2631
+ if (!settings.hooks)
2632
+ continue;
2633
+ delete settings.hooks;
2634
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
2635
+ log(`Cleaned up hooks from ${settingsPath}`);
2636
+ } catch (err) {
2637
+ const code = err.code;
2638
+ if (code !== "ENOENT") {
2639
+ log(`Failed to clean up hooks from ${settingsPath}: ${err}`);
2640
+ }
2641
+ }
2642
+ }
2412
2643
  }
2413
2644
  function subscribeToOutput(ctx, sessionId, callback) {
2414
2645
  if (ctx.usingBunWorker) {
@@ -2739,9 +2970,11 @@ async function classifyStallOutput(ctx) {
2739
2970
  }
2740
2971
  try {
2741
2972
  log(`Stall detected for ${sessionId}, asking LLM to classify...`);
2742
- const result = await runtime.useModel(ModelType.TEXT_SMALL, {
2743
- prompt: systemPrompt
2744
- });
2973
+ const result = await withTrajectoryContext(runtime, {
2974
+ source: "orchestrator",
2975
+ decisionType: "stall-classification",
2976
+ sessionId
2977
+ }, () => runtime.useModel(ModelType.TEXT_SMALL, { prompt: systemPrompt }));
2745
2978
  const jsonMatch = result.match(/\{[\s\S]*\}/);
2746
2979
  if (!jsonMatch) {
2747
2980
  log(`Stall classification: no JSON in LLM response`);
@@ -2853,9 +3086,15 @@ async function classifyAndDecideForCoordinator(ctx) {
2853
3086
  }
2854
3087
  try {
2855
3088
  log(`Stall detected for coordinator-managed ${sessionId}, combined classify+decide...`);
2856
- const result = await runtime.useModel(ModelType.TEXT_SMALL, {
2857
- prompt: systemPrompt
2858
- });
3089
+ const result = await withTrajectoryContext(runtime, {
3090
+ source: "orchestrator",
3091
+ decisionType: "stall-classify-decide",
3092
+ sessionId,
3093
+ taskLabel: taskContext.label,
3094
+ repo: taskContext.repo,
3095
+ workdir: taskContext.workdir,
3096
+ originalTask: taskContext.originalTask
3097
+ }, () => runtime.useModel(ModelType.TEXT_SMALL, { prompt: systemPrompt }));
2859
3098
  const jsonMatch = result.match(/\{[\s\S]*\}/);
2860
3099
  if (!jsonMatch) {
2861
3100
  log(`Combined classify+decide: no JSON in LLM response`);
@@ -3031,12 +3270,29 @@ async function handleIdleCheck(ctx, taskCtx, idleMinutes) {
3031
3270
  response: d.response,
3032
3271
  reasoning: d.reasoning
3033
3272
  }));
3034
- const prompt = buildIdleCheckPrompt(contextSummary, recentOutput, idleMinutes, taskCtx.idleCheckCount, MAX_IDLE_CHECKS, decisionHistory);
3273
+ const siblings = [];
3274
+ for (const [sid, task] of ctx.tasks) {
3275
+ if (sid === sessionId)
3276
+ continue;
3277
+ siblings.push({
3278
+ label: task.label,
3279
+ agentType: task.agentType,
3280
+ originalTask: task.originalTask,
3281
+ status: task.status
3282
+ });
3283
+ }
3284
+ const prompt = buildIdleCheckPrompt(contextSummary, recentOutput, idleMinutes, taskCtx.idleCheckCount, MAX_IDLE_CHECKS, decisionHistory, siblings, ctx.sharedDecisions, ctx.getSwarmContext());
3035
3285
  let decision = null;
3036
3286
  try {
3037
- const result = await ctx.runtime.useModel(ModelType4.TEXT_SMALL, {
3038
- prompt
3039
- });
3287
+ const result = await withTrajectoryContext(ctx.runtime, {
3288
+ source: "orchestrator",
3289
+ decisionType: "idle-check",
3290
+ sessionId,
3291
+ taskLabel: taskCtx.label,
3292
+ repo: taskCtx.repo,
3293
+ workdir: taskCtx.workdir,
3294
+ originalTask: taskCtx.originalTask
3295
+ }, () => ctx.runtime.useModel(ModelType4.TEXT_SMALL, { prompt }));
3040
3296
  decision = parseCoordinationResponse(result);
3041
3297
  } catch (err) {
3042
3298
  ctx.log(`Idle check LLM call failed: ${err}`);
@@ -3095,14 +3351,20 @@ class SwarmCoordinator {
3095
3351
  pendingDecisions = new Map;
3096
3352
  inFlightDecisions = new Set;
3097
3353
  pendingTurnComplete = new Map;
3354
+ lastBlockedPromptFingerprint = new Map;
3355
+ pendingBlocked = new Map;
3098
3356
  chatCallback = null;
3099
3357
  wsBroadcast = null;
3100
3358
  agentDecisionCb = null;
3359
+ swarmCompleteCb = null;
3101
3360
  unregisteredBuffer = new Map;
3102
3361
  idleWatchdogTimer = null;
3103
3362
  lastSeenOutput = new Map;
3104
3363
  lastToolNotification = new Map;
3105
3364
  _paused = false;
3365
+ sharedDecisions = [];
3366
+ _swarmContext = "";
3367
+ swarmCompleteNotified = false;
3106
3368
  pauseBuffer = [];
3107
3369
  pauseTimeout = null;
3108
3370
  constructor(runtime) {
@@ -3116,6 +3378,20 @@ class SwarmCoordinator {
3116
3378
  this.wsBroadcast = cb;
3117
3379
  this.log("WS broadcast callback wired");
3118
3380
  }
3381
+ setSwarmCompleteCallback(cb) {
3382
+ this.swarmCompleteCb = cb;
3383
+ this.log("Swarm complete callback wired");
3384
+ }
3385
+ getSwarmCompleteCallback() {
3386
+ return this.swarmCompleteCb;
3387
+ }
3388
+ setSwarmContext(context) {
3389
+ this._swarmContext = context;
3390
+ this.log(`Swarm context set (${context.length} chars)`);
3391
+ }
3392
+ getSwarmContext() {
3393
+ return this._swarmContext;
3394
+ }
3119
3395
  setAgentDecisionCallback(cb) {
3120
3396
  this.agentDecisionCb = cb;
3121
3397
  this.log("Agent decision callback wired — events will route through Milaidy");
@@ -3163,10 +3439,15 @@ class SwarmCoordinator {
3163
3439
  this.pendingDecisions.clear();
3164
3440
  this.inFlightDecisions.clear();
3165
3441
  this.pendingTurnComplete.clear();
3442
+ this.lastBlockedPromptFingerprint.clear();
3443
+ this.pendingBlocked.clear();
3166
3444
  this.unregisteredBuffer.clear();
3167
3445
  this.lastSeenOutput.clear();
3168
3446
  this.lastToolNotification.clear();
3169
3447
  this.agentDecisionCb = null;
3448
+ this.sharedDecisions.length = 0;
3449
+ this._swarmContext = "";
3450
+ this.swarmCompleteNotified = false;
3170
3451
  this._paused = false;
3171
3452
  if (this.pauseTimeout) {
3172
3453
  clearTimeout(this.pauseTimeout);
@@ -3210,6 +3491,16 @@ class SwarmCoordinator {
3210
3491
  }
3211
3492
  }
3212
3493
  registerTask(sessionId, context) {
3494
+ const allPreviousTerminal = this.tasks.size === 0 || Array.from(this.tasks.values()).every((t) => t.status === "completed" || t.status === "stopped" || t.status === "error");
3495
+ if (allPreviousTerminal) {
3496
+ this.swarmCompleteNotified = false;
3497
+ if (this.tasks.size > 0) {
3498
+ this.tasks.clear();
3499
+ this.sharedDecisions.length = 0;
3500
+ this._swarmContext = "";
3501
+ this.log("Cleared stale swarm state for new swarm");
3502
+ }
3503
+ }
3213
3504
  this.tasks.set(sessionId, {
3214
3505
  sessionId,
3215
3506
  agentType: context.agentType,
@@ -3223,7 +3514,8 @@ class SwarmCoordinator {
3223
3514
  registeredAt: Date.now(),
3224
3515
  lastActivityAt: Date.now(),
3225
3516
  idleCheckCount: 0,
3226
- taskDelivered: false
3517
+ taskDelivered: false,
3518
+ lastSeenDecisionIndex: 0
3227
3519
  });
3228
3520
  this.broadcast({
3229
3521
  type: "task_registered",
@@ -3378,7 +3670,9 @@ class SwarmCoordinator {
3378
3670
  break;
3379
3671
  }
3380
3672
  case "stopped":
3381
- taskCtx.status = "stopped";
3673
+ if (taskCtx.status !== "completed" && taskCtx.status !== "error") {
3674
+ taskCtx.status = "stopped";
3675
+ }
3382
3676
  this.inFlightDecisions.delete(sessionId);
3383
3677
  this.broadcast({
3384
3678
  type: "stopped",
@@ -3711,6 +4005,7 @@ class PTYService {
3711
4005
  this.log(`Failed to write approval config: ${err}`);
3712
4006
  }
3713
4007
  }
4008
+ const hookUrl = `http://localhost:${this.runtime.getSetting("SERVER_PORT") ?? "2138"}/api/coding-agents/hooks`;
3714
4009
  if (resolvedAgentType === "claude") {
3715
4010
  try {
3716
4011
  const settingsPath = join2(workdir, ".claude", "settings.json");
@@ -3721,14 +4016,14 @@ class PTYService {
3721
4016
  const permissions = settings.permissions ?? {};
3722
4017
  permissions.allowedDirectories = [workdir];
3723
4018
  settings.permissions = permissions;
3724
- const serverPort = this.runtime.getSetting("SERVER_PORT") ?? "2138";
3725
4019
  const adapter = this.getAdapter("claude");
3726
4020
  const hookProtocol = adapter.getHookTelemetryProtocol({
3727
- httpUrl: `http://localhost:${serverPort}/api/coding-agents/hooks`,
4021
+ httpUrl: hookUrl,
3728
4022
  sessionId
3729
4023
  });
3730
4024
  if (hookProtocol) {
3731
- settings.hooks = hookProtocol.settingsHooks;
4025
+ const existingHooks = settings.hooks ?? {};
4026
+ settings.hooks = { ...existingHooks, ...hookProtocol.settingsHooks };
3732
4027
  this.log(`Injecting HTTP hooks for session ${sessionId}`);
3733
4028
  }
3734
4029
  await mkdir(dirname(settingsPath), { recursive: true });
@@ -3738,6 +4033,32 @@ class PTYService {
3738
4033
  this.log(`Failed to write Claude settings: ${err}`);
3739
4034
  }
3740
4035
  }
4036
+ if (resolvedAgentType === "gemini") {
4037
+ try {
4038
+ const settingsPath = join2(workdir, ".gemini", "settings.json");
4039
+ let settings = {};
4040
+ try {
4041
+ settings = JSON.parse(await readFile2(settingsPath, "utf-8"));
4042
+ } catch {}
4043
+ const adapter = this.getAdapter("gemini");
4044
+ const hookProtocol = adapter.getHookTelemetryProtocol({
4045
+ httpUrl: hookUrl,
4046
+ sessionId
4047
+ });
4048
+ if (hookProtocol) {
4049
+ const existingHooks = settings.hooks ?? {};
4050
+ settings.hooks = { ...existingHooks, ...hookProtocol.settingsHooks };
4051
+ this.log(`Injecting Gemini CLI hooks for session ${sessionId}`);
4052
+ }
4053
+ await mkdir(dirname(settingsPath), { recursive: true });
4054
+ await writeFile2(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
4055
+ } catch (err) {
4056
+ this.log(`Failed to write Gemini settings: ${err}`);
4057
+ }
4058
+ }
4059
+ if (resolvedAgentType !== "shell" && workdir !== process.cwd()) {
4060
+ await this.ensureOrchestratorGitignore(workdir);
4061
+ }
3741
4062
  const spawnConfig = buildSpawnConfig(sessionId, {
3742
4063
  ...options,
3743
4064
  agentType: resolvedAgentType,
@@ -3893,6 +4214,9 @@ class PTYService {
3893
4214
  } else {
3894
4215
  this.log(`Hook event for ${sessionId}: ${event} ${summary}`);
3895
4216
  }
4217
+ if (this.manager && this.usingBunWorker) {
4218
+ this.manager.notifyHookEvent(sessionId, event).catch((err) => logger2.debug(`[PTYService] Failed to forward hook event to session: ${err}`));
4219
+ }
3896
4220
  switch (event) {
3897
4221
  case "tool_running":
3898
4222
  this.emitEvent(sessionId, "tool_running", data);
@@ -3905,6 +4229,9 @@ class PTYService {
3905
4229
  case "notification":
3906
4230
  this.emitEvent(sessionId, "message", data);
3907
4231
  break;
4232
+ case "session_end":
4233
+ this.emitEvent(sessionId, "stopped", { ...data, reason: "session_end" });
4234
+ break;
3908
4235
  default:
3909
4236
  break;
3910
4237
  }
@@ -3989,6 +4316,56 @@ class PTYService {
3989
4316
  async writeMemoryFile(agentType, workspacePath, content, options) {
3990
4317
  return this.getAdapter(agentType).writeMemoryFile(workspacePath, content, options);
3991
4318
  }
4319
+ static GITIGNORE_MARKER = "# orchestrator-injected (do not commit agent config/memory files)";
4320
+ static gitignoreLocks = new Map;
4321
+ async ensureOrchestratorGitignore(workdir) {
4322
+ const gitignorePath = join2(workdir, ".gitignore");
4323
+ const existing_lock = PTYService.gitignoreLocks.get(gitignorePath);
4324
+ if (existing_lock)
4325
+ await existing_lock;
4326
+ const task = this.doEnsureGitignore(gitignorePath, workdir);
4327
+ PTYService.gitignoreLocks.set(gitignorePath, task);
4328
+ try {
4329
+ await task;
4330
+ } finally {
4331
+ if (PTYService.gitignoreLocks.get(gitignorePath) === task) {
4332
+ PTYService.gitignoreLocks.delete(gitignorePath);
4333
+ }
4334
+ }
4335
+ }
4336
+ async doEnsureGitignore(gitignorePath, workdir) {
4337
+ let existing = "";
4338
+ try {
4339
+ existing = await readFile2(gitignorePath, "utf-8");
4340
+ } catch {}
4341
+ if (existing.includes(PTYService.GITIGNORE_MARKER))
4342
+ return;
4343
+ const entries = [
4344
+ "",
4345
+ PTYService.GITIGNORE_MARKER,
4346
+ "CLAUDE.md",
4347
+ ".claude/",
4348
+ "GEMINI.md",
4349
+ ".gemini/",
4350
+ ".aider*"
4351
+ ];
4352
+ try {
4353
+ if (existing.length === 0) {
4354
+ await writeFile2(gitignorePath, entries.join(`
4355
+ `) + `
4356
+ `, "utf-8");
4357
+ } else {
4358
+ const separator = existing.endsWith(`
4359
+ `) ? "" : `
4360
+ `;
4361
+ await appendFile(gitignorePath, separator + entries.join(`
4362
+ `) + `
4363
+ `, "utf-8");
4364
+ }
4365
+ } catch (err) {
4366
+ this.log(`Failed to update .gitignore in ${workdir}: ${err}`);
4367
+ }
4368
+ }
3992
4369
  onSessionEvent(callback) {
3993
4370
  this.eventCallbacks.push(callback);
3994
4371
  return () => {
@@ -4295,8 +4672,170 @@ var spawnAgentAction = {
4295
4672
 
4296
4673
  // src/actions/coding-task-handlers.ts
4297
4674
  import {
4298
- logger as logger5
4675
+ logger as logger5,
4676
+ ModelType as ModelType5
4299
4677
  } from "@elizaos/core";
4678
+ // src/services/trajectory-feedback.ts
4679
+ var QUERY_TIMEOUT_MS = 5000;
4680
+ function withTimeout2(promise, ms) {
4681
+ return Promise.race([
4682
+ promise,
4683
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Trajectory query timed out after ${ms}ms`)), ms))
4684
+ ]);
4685
+ }
4686
+ function getTrajectoryLogger(runtime) {
4687
+ const runtimeAny = runtime;
4688
+ if (typeof runtimeAny.getService === "function") {
4689
+ const svc = runtimeAny.getService("trajectory_logger");
4690
+ if (svc && typeof svc === "object" && hasListMethod(svc)) {
4691
+ return svc;
4692
+ }
4693
+ }
4694
+ if (typeof runtimeAny.getServicesByType === "function") {
4695
+ const services = runtimeAny.getServicesByType("trajectory_logger");
4696
+ if (Array.isArray(services)) {
4697
+ for (const svc of services) {
4698
+ if (svc && typeof svc === "object" && hasListMethod(svc)) {
4699
+ return svc;
4700
+ }
4701
+ }
4702
+ }
4703
+ }
4704
+ return null;
4705
+ }
4706
+ function hasListMethod(obj) {
4707
+ const candidate = obj;
4708
+ return typeof candidate.listTrajectories === "function" && typeof candidate.getTrajectoryDetail === "function";
4709
+ }
4710
+ function extractInsights(response, purpose) {
4711
+ const insights = [];
4712
+ const decisionPattern = /DECISION:\s*(.+?)(?:\n|$)/gi;
4713
+ let match;
4714
+ while ((match = decisionPattern.exec(response)) !== null) {
4715
+ insights.push(match[1].trim());
4716
+ }
4717
+ const keyDecisionPattern = /"keyDecision"\s*:\s*"([^"]+)"/g;
4718
+ while ((match = keyDecisionPattern.exec(response)) !== null) {
4719
+ insights.push(match[1].trim());
4720
+ }
4721
+ if ((purpose === "turn-complete" || purpose === "coordination") && insights.length === 0) {
4722
+ const reasoningPattern = /"reasoning"\s*:\s*"([^"]{20,200})"/;
4723
+ const reasoningMatch = response.match(reasoningPattern);
4724
+ if (reasoningMatch) {
4725
+ insights.push(reasoningMatch[1].trim());
4726
+ }
4727
+ }
4728
+ return insights;
4729
+ }
4730
+ function isRelevant(experience, taskDescription) {
4731
+ if (!taskDescription)
4732
+ return true;
4733
+ const taskWords = new Set(taskDescription.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 3));
4734
+ const insightWords = experience.insight.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 3);
4735
+ let overlap = 0;
4736
+ for (const word of insightWords) {
4737
+ if (taskWords.has(word))
4738
+ overlap++;
4739
+ if (overlap >= 2)
4740
+ return true;
4741
+ }
4742
+ return false;
4743
+ }
4744
+ async function queryPastExperience(runtime, options = {}) {
4745
+ const {
4746
+ maxTrajectories = 30,
4747
+ maxEntries = 8,
4748
+ lookbackHours = 48,
4749
+ taskDescription,
4750
+ repo
4751
+ } = options;
4752
+ const logger4 = getTrajectoryLogger(runtime);
4753
+ if (!logger4)
4754
+ return [];
4755
+ const startDate = new Date(Date.now() - lookbackHours * 60 * 60 * 1000).toISOString();
4756
+ try {
4757
+ const result = await withTimeout2(logger4.listTrajectories({
4758
+ source: "orchestrator",
4759
+ limit: maxTrajectories,
4760
+ startDate
4761
+ }), QUERY_TIMEOUT_MS);
4762
+ if (!result.trajectories || result.trajectories.length === 0)
4763
+ return [];
4764
+ const experiences = [];
4765
+ const maxScans = Math.min(result.trajectories.length, maxTrajectories);
4766
+ for (let scanIdx = 0;scanIdx < maxScans; scanIdx++) {
4767
+ const summary = result.trajectories[scanIdx];
4768
+ const detail = await withTimeout2(logger4.getTrajectoryDetail(summary.id), QUERY_TIMEOUT_MS).catch(() => null);
4769
+ if (!detail?.steps)
4770
+ continue;
4771
+ const metadata = detail.metadata;
4772
+ const decisionType = metadata?.orchestrator?.decisionType ?? "unknown";
4773
+ const taskLabel = metadata?.orchestrator?.taskLabel ?? "";
4774
+ const trajectoryRepo = metadata?.orchestrator?.repo;
4775
+ if (repo && (!trajectoryRepo || trajectoryRepo !== repo))
4776
+ continue;
4777
+ for (const step of detail.steps) {
4778
+ if (!step.llmCalls)
4779
+ continue;
4780
+ for (const call of step.llmCalls) {
4781
+ if (!call.response)
4782
+ continue;
4783
+ const insights = extractInsights(call.response, call.purpose ?? decisionType);
4784
+ for (const insight of insights) {
4785
+ experiences.push({
4786
+ timestamp: call.timestamp ?? summary.startTime,
4787
+ decisionType: call.purpose ?? decisionType,
4788
+ taskLabel,
4789
+ insight
4790
+ });
4791
+ }
4792
+ }
4793
+ }
4794
+ }
4795
+ let filtered = taskDescription ? experiences.filter((e) => isRelevant(e, taskDescription)) : experiences;
4796
+ if (filtered.length === 0 && experiences.length > 0) {
4797
+ filtered = experiences;
4798
+ }
4799
+ const seen = new Map;
4800
+ for (const exp of filtered) {
4801
+ const key = exp.insight.toLowerCase();
4802
+ const existing = seen.get(key);
4803
+ if (!existing || exp.timestamp > existing.timestamp) {
4804
+ seen.set(key, exp);
4805
+ }
4806
+ }
4807
+ return Array.from(seen.values()).sort((a, b) => b.timestamp - a.timestamp).slice(0, maxEntries);
4808
+ } catch (err) {
4809
+ console.error("[trajectory-feedback] Failed to query past experience:", err);
4810
+ return [];
4811
+ }
4812
+ }
4813
+ function formatPastExperience(experiences) {
4814
+ if (experiences.length === 0)
4815
+ return "";
4816
+ const lines = experiences.map((e) => {
4817
+ const age = formatAge(e.timestamp);
4818
+ const label = e.taskLabel ? ` [${e.taskLabel}]` : "";
4819
+ return `- ${e.insight}${label} (${age})`;
4820
+ });
4821
+ return `# Past Experience
4822
+
4823
+ ` + `The following decisions and insights were captured from recent agent sessions. ` + `Use them to avoid repeating mistakes and to stay consistent with established patterns.
4824
+
4825
+ ` + `${lines.join(`
4826
+ `)}
4827
+ `;
4828
+ }
4829
+ function formatAge(timestamp) {
4830
+ const diffMs = Date.now() - timestamp;
4831
+ const hours = Math.floor(diffMs / (1000 * 60 * 60));
4832
+ if (hours < 1)
4833
+ return "just now";
4834
+ if (hours < 24)
4835
+ return `${hours}h ago`;
4836
+ const days = Math.floor(hours / 24);
4837
+ return `${days}d ago`;
4838
+ }
4300
4839
 
4301
4840
  // src/actions/coding-task-helpers.ts
4302
4841
  import { randomUUID } from "node:crypto";
@@ -4371,6 +4910,88 @@ ${preview}` : `Agent "${label}" completed the task.`
4371
4910
 
4372
4911
  // src/actions/coding-task-handlers.ts
4373
4912
  var MAX_CONCURRENT_AGENTS = 8;
4913
+ var KNOWN_AGENT_PREFIXES = [
4914
+ "claude",
4915
+ "claude-code",
4916
+ "claudecode",
4917
+ "codex",
4918
+ "openai",
4919
+ "gemini",
4920
+ "google",
4921
+ "aider",
4922
+ "pi",
4923
+ "pi-ai",
4924
+ "piai",
4925
+ "pi-coding-agent",
4926
+ "picodingagent",
4927
+ "shell",
4928
+ "bash"
4929
+ ];
4930
+ function stripAgentPrefix(spec) {
4931
+ const colonIdx = spec.indexOf(":");
4932
+ if (colonIdx <= 0 || colonIdx >= 20)
4933
+ return spec;
4934
+ const prefix = spec.slice(0, colonIdx).trim().toLowerCase();
4935
+ if (KNOWN_AGENT_PREFIXES.includes(prefix)) {
4936
+ return spec.slice(colonIdx + 1).trim();
4937
+ }
4938
+ return spec;
4939
+ }
4940
+ function buildSwarmMemoryInstructions(agentLabel, agentTask, allSubtasks, agentIndex) {
4941
+ const siblingTasks = allSubtasks.filter((_, i) => i !== agentIndex).map((t, i) => ` ${i + 1}. ${t}`).join(`
4942
+ `);
4943
+ return `# Swarm Coordination
4944
+
4945
+ ` + `You are agent "${agentLabel}" in a multi-agent swarm of ${allSubtasks.length} agents.
4946
+ ` + `Your task: ${agentTask}
4947
+
4948
+ ` + `Other agents are working on:
4949
+ ${siblingTasks}
4950
+
4951
+ ` + `## Coordination Rules
4952
+
4953
+ ` + `- **Follow the Shared Context exactly.** The planning brief above contains ` + `concrete decisions (names, file paths, APIs, conventions). Use them as-is.
4954
+ ` + `- **Surface design decisions.** If you need to make a creative or architectural ` + `choice not covered by the Shared Context (naming something, choosing a library, ` + `designing an interface, picking an approach), state your decision clearly in your ` + `output so the orchestrator can share it with sibling agents. Write it as:
4955
+ ` + ` "DECISION: [brief description of what you decided and why]"
4956
+ ` + `- **Don't contradict sibling work.** If the orchestrator tells you about decisions ` + `other agents have made, align with them.
4957
+ ` + `- **Ask when uncertain.** If your task depends on another agent's output and you ` + `don't have enough context, ask rather than guessing.
4958
+ `;
4959
+ }
4960
+ async function generateSwarmContext(runtime, subtasks, userRequest) {
4961
+ const taskList = subtasks.map((t, i) => ` ${i + 1}. ${t}`).join(`
4962
+ `);
4963
+ 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.
4964
+
4965
+ ` + `User's request: "${userRequest}"
4966
+
4967
+ ` + `Subtasks being assigned:
4968
+ ${taskList}
4969
+
4970
+ ` + `Generate a concise shared context brief (3-10 bullet points) covering:
4971
+ ` + `- Project intent and overall goal
4972
+ ` + `- Key constraints or preferences from the user's request
4973
+ ` + `- Conventions all agents should follow (naming, style, patterns, tone)
4974
+ ` + `- How subtasks relate to each other (dependencies, shared interfaces, etc.)
4975
+ ` + `- Any decisions that should be consistent across all agents
4976
+
4977
+ ` + `CRITICAL — Concrete Decisions:
4978
+ ` + `If any subtask involves creative choices (naming a feature, choosing an approach, ` + `designing an API, picking a concept), YOU must make those decisions NOW in this brief. ` + `Do NOT leave creative choices to individual agents — they run in parallel and will ` + `each make different choices, causing inconsistency.
4979
+ ` + `For example: if one agent builds a feature and another writes tests for it, ` + `decide the feature name, file paths, function signatures, and key design choices here ` + `so both agents use the same names and structure.
4980
+
4981
+ ` + `Only include what's relevant — skip categories that don't apply. ` + `Be specific and actionable, not generic. Be as detailed as the task requires — ` + `a trivial task needs a few bullets, a complex task deserves a thorough roadmap.
4982
+
4983
+ ` + `Output ONLY the bullet points, no preamble.`;
4984
+ try {
4985
+ const result = await withTrajectoryContext(runtime, { source: "orchestrator", decisionType: "swarm-context-generation" }, () => runtime.useModel(ModelType5.TEXT_SMALL, {
4986
+ prompt,
4987
+ temperature: 0.3
4988
+ }));
4989
+ return result?.trim() || "";
4990
+ } catch (err) {
4991
+ logger5.warn(`Swarm context generation failed: ${err}`);
4992
+ return "";
4993
+ }
4994
+ }
4374
4995
  async function handleMultiAgent(ctx, agentsParam) {
4375
4996
  const {
4376
4997
  runtime,
@@ -4418,6 +5039,20 @@ async function handleMultiAgent(ctx, agentsParam) {
4418
5039
  text: `Launching ${agentSpecs.length} agents${repo ? ` on ${repo}` : ""}...`
4419
5040
  });
4420
5041
  }
5042
+ const cleanSubtasks = agentSpecs.map(stripAgentPrefix);
5043
+ const userRequest = message.content?.text ?? agentsParam;
5044
+ const swarmContext = agentSpecs.length > 1 ? await generateSwarmContext(runtime, cleanSubtasks, userRequest) : "";
5045
+ if (swarmContext) {
5046
+ const coordinator = getCoordinator(runtime);
5047
+ coordinator?.setSwarmContext(swarmContext);
5048
+ }
5049
+ const pastExperience = await queryPastExperience(runtime, {
5050
+ taskDescription: userRequest,
5051
+ lookbackHours: 48,
5052
+ maxEntries: 8,
5053
+ repo
5054
+ });
5055
+ const pastExperienceBlock = formatPastExperience(pastExperience);
4421
5056
  const results = [];
4422
5057
  for (const [i, spec] of agentSpecs.entries()) {
4423
5058
  let specAgentType = defaultAgentType;
@@ -4427,51 +5062,14 @@ async function handleMultiAgent(ctx, agentsParam) {
4427
5062
  const colonIdx = spec.indexOf(":");
4428
5063
  if (ctx.agentSelectionStrategy !== "fixed" && colonIdx > 0 && colonIdx < 20) {
4429
5064
  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)) {
5065
+ if (KNOWN_AGENT_PREFIXES.includes(prefix)) {
4448
5066
  specRequestedType = prefix;
4449
5067
  specPiRequested = isPiAgentType(prefix);
4450
5068
  specAgentType = normalizeAgentType(prefix);
4451
5069
  specTask = spec.slice(colonIdx + 1).trim();
4452
5070
  }
4453
5071
  } 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
- }
5072
+ specTask = stripAgentPrefix(spec);
4475
5073
  }
4476
5074
  const specLabel = explicitLabel ? `${explicitLabel}-${i + 1}` : generateLabel(repo, specTask);
4477
5075
  try {
@@ -4504,14 +5102,23 @@ async function handleMultiAgent(ctx, agentsParam) {
4504
5102
  }
4505
5103
  }
4506
5104
  const coordinator = getCoordinator(runtime);
4507
- const initialTask = specPiRequested ? toPiCommand(specTask) : specTask;
5105
+ const taskWithContext = swarmContext ? `${specTask}
5106
+
5107
+ --- Shared Context (from project planning) ---
5108
+ ${swarmContext}
5109
+ --- End Shared Context ---` : specTask;
5110
+ const initialTask = specPiRequested ? toPiCommand(taskWithContext) : taskWithContext;
4508
5111
  const displayType = specPiRequested ? "pi" : specAgentType;
5112
+ const swarmMemory = agentSpecs.length > 1 && swarmContext ? buildSwarmMemoryInstructions(specLabel, specTask, cleanSubtasks, i) : undefined;
5113
+ const agentMemory = [memoryContent, swarmMemory, pastExperienceBlock].filter(Boolean).join(`
5114
+
5115
+ `) || undefined;
4509
5116
  const session = await ptyService.spawnSession({
4510
5117
  name: `coding-${Date.now()}-${i}`,
4511
5118
  agentType: specAgentType,
4512
5119
  workdir,
4513
5120
  initialTask,
4514
- memoryContent,
5121
+ memoryContent: agentMemory,
4515
5122
  credentials,
4516
5123
  approvalPreset: approvalPreset ?? ptyService.defaultApprovalPreset,
4517
5124
  customCredentials,
@@ -4667,6 +5274,16 @@ Docs: ${preflight.docsUrl}`
4667
5274
  const piRequested = isPiAgentType(rawAgentType);
4668
5275
  const initialTask = piRequested ? toPiCommand(task) : task;
4669
5276
  const displayType = piRequested ? "pi" : agentType;
5277
+ const pastExperience = await queryPastExperience(runtime, {
5278
+ taskDescription: task,
5279
+ lookbackHours: 48,
5280
+ maxEntries: 6,
5281
+ repo
5282
+ });
5283
+ const pastExperienceBlock = formatPastExperience(pastExperience);
5284
+ const agentMemory = [memoryContent, pastExperienceBlock].filter(Boolean).join(`
5285
+
5286
+ `) || undefined;
4670
5287
  const coordinator = getCoordinator(runtime);
4671
5288
  logger5.debug(`[START_CODING_TASK] Calling spawnSession (${agentType}, coordinator=${!!coordinator})`);
4672
5289
  const session = await ptyService.spawnSession({
@@ -4674,7 +5291,7 @@ Docs: ${preflight.docsUrl}`
4674
5291
  agentType,
4675
5292
  workdir,
4676
5293
  initialTask,
4677
- memoryContent,
5294
+ memoryContent: agentMemory,
4678
5295
  credentials,
4679
5296
  approvalPreset: approvalPreset ?? ptyService.defaultApprovalPreset,
4680
5297
  customCredentials,
@@ -6319,6 +6936,8 @@ async function handleHookRoutes(req, res, pathname, ctx) {
6319
6936
  sendError(res, "Missing hook_event_name", 400);
6320
6937
  return true;
6321
6938
  }
6939
+ const toolName = payload.tool_name ?? payload.toolName;
6940
+ const notificationType = payload.notification_type ?? payload.notificationType;
6322
6941
  const headerSessionId = req.headers["x-parallax-session-id"];
6323
6942
  const sessionId = headerSessionId ? headerSessionId : payload.cwd ? ctx.ptyService.findSessionIdByCwd(payload.cwd) : undefined;
6324
6943
  if (!sessionId) {
@@ -6334,13 +6953,13 @@ async function handleHookRoutes(req, res, pathname, ctx) {
6334
6953
  }
6335
6954
  });
6336
6955
  ctx.ptyService.handleHookEvent(sessionId, "permission_approved", {
6337
- tool: payload.tool_name
6956
+ tool: toolName
6338
6957
  });
6339
6958
  return true;
6340
6959
  }
6341
6960
  case "PreToolUse": {
6342
6961
  ctx.ptyService.handleHookEvent(sessionId, "tool_running", {
6343
- toolName: payload.tool_name,
6962
+ toolName,
6344
6963
  source: "hook"
6345
6964
  });
6346
6965
  sendJson(res, {
@@ -6358,19 +6977,56 @@ async function handleHookRoutes(req, res, pathname, ctx) {
6358
6977
  sendJson(res, {});
6359
6978
  return true;
6360
6979
  }
6361
- case "Notification": {
6362
- ctx.ptyService.handleHookEvent(sessionId, "notification", {
6363
- type: payload.notification_type,
6364
- message: payload.message
6980
+ case "TaskCompleted": {
6981
+ ctx.ptyService.handleHookEvent(sessionId, "task_complete", {
6982
+ source: "hook_task_completed"
6365
6983
  });
6366
6984
  sendJson(res, {});
6367
6985
  return true;
6368
6986
  }
6369
- case "TaskCompleted": {
6987
+ case "BeforeTool": {
6988
+ ctx.ptyService.handleHookEvent(sessionId, "tool_running", {
6989
+ toolName,
6990
+ source: "gemini_hook"
6991
+ });
6992
+ sendJson(res, { decision: "allow", continue: true });
6993
+ return true;
6994
+ }
6995
+ case "AfterTool": {
6996
+ ctx.ptyService.handleHookEvent(sessionId, "notification", {
6997
+ type: "tool_complete",
6998
+ message: `Tool ${toolName ?? "unknown"} finished`
6999
+ });
7000
+ sendJson(res, { continue: true });
7001
+ return true;
7002
+ }
7003
+ case "AfterAgent": {
6370
7004
  ctx.ptyService.handleHookEvent(sessionId, "task_complete", {
6371
- source: "hook_task_completed"
7005
+ source: "gemini_hook"
6372
7006
  });
6373
- sendJson(res, {});
7007
+ sendJson(res, { continue: true });
7008
+ return true;
7009
+ }
7010
+ case "SessionEnd": {
7011
+ ctx.ptyService.handleHookEvent(sessionId, "session_end", {
7012
+ source: "hook"
7013
+ });
7014
+ sendJson(res, { continue: true });
7015
+ return true;
7016
+ }
7017
+ case "Notification": {
7018
+ if (notificationType === "ToolPermission") {
7019
+ ctx.ptyService.handleHookEvent(sessionId, "permission_approved", {
7020
+ tool: toolName
7021
+ });
7022
+ sendJson(res, { decision: "allow", continue: true });
7023
+ return true;
7024
+ }
7025
+ ctx.ptyService.handleHookEvent(sessionId, "notification", {
7026
+ type: notificationType,
7027
+ message: payload.message
7028
+ });
7029
+ sendJson(res, { continue: true });
6374
7030
  return true;
6375
7031
  }
6376
7032
  default: {
@@ -6714,5 +7370,5 @@ export {
6714
7370
  CodingWorkspaceService
6715
7371
  };
6716
7372
 
6717
- //# debugId=54C728A058C6B9C264756E2164756E21
7373
+ //# debugId=1AF5CF89CDA1169464756E2164756E21
6718
7374
  //# sourceMappingURL=index.js.map