@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/actions/coding-task-handlers.d.ts.map +1 -1
- package/dist/api/hook-routes.d.ts +3 -2
- package/dist/api/hook-routes.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +766 -110
- package/dist/index.js.map +16 -14
- package/dist/services/pty-service.d.ts +13 -0
- package/dist/services/pty-service.d.ts.map +1 -1
- package/dist/services/pty-session-io.d.ts.map +1 -1
- package/dist/services/stall-classifier.d.ts.map +1 -1
- package/dist/services/swarm-coordinator-prompts.d.ts +27 -5
- package/dist/services/swarm-coordinator-prompts.d.ts.map +1 -1
- package/dist/services/swarm-coordinator.d.ts +59 -1
- package/dist/services/swarm-coordinator.d.ts.map +1 -1
- package/dist/services/swarm-decision-loop.d.ts.map +1 -1
- package/dist/services/swarm-event-triage.d.ts.map +1 -1
- package/dist/services/swarm-idle-watchdog.d.ts.map +1 -1
- package/dist/services/trajectory-context.d.ts +70 -0
- package/dist/services/trajectory-context.d.ts.map +1 -0
- package/dist/services/trajectory-feedback.d.ts +53 -0
- package/dist/services/trajectory-feedback.d.ts.map +1 -0
- package/package.json +2 -2
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
|
|
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.
|
|
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
|
|
586
|
-
|
|
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
|
-
|
|
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
|
|
765
|
-
|
|
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
|
|
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
|
|
2402
|
-
const
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
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
|
|
2743
|
-
|
|
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
|
|
2857
|
-
|
|
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
|
|
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
|
|
3038
|
-
|
|
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
|
|
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:
|
|
4021
|
+
httpUrl: hookUrl,
|
|
3728
4022
|
sessionId
|
|
3729
4023
|
});
|
|
3730
4024
|
if (hookProtocol) {
|
|
3731
|
-
settings.hooks
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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 "
|
|
6362
|
-
ctx.ptyService.handleHookEvent(sessionId, "
|
|
6363
|
-
|
|
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 "
|
|
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: "
|
|
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=
|
|
7373
|
+
//# debugId=1AF5CF89CDA1169464756E2164756E21
|
|
6718
7374
|
//# sourceMappingURL=index.js.map
|