@elizaos/plugin-agent-orchestrator 0.2.0 → 0.3.2
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 +495 -72
- package/dist/index.js.map +14 -13
- package/package.json +2 -2
- package/dist/src/actions/coding-task-handlers.d.ts +0 -44
- package/dist/src/actions/coding-task-handlers.d.ts.map +0 -1
- package/dist/src/actions/coding-task-helpers.d.ts +0 -27
- package/dist/src/actions/coding-task-helpers.d.ts.map +0 -1
- package/dist/src/actions/finalize-workspace.d.ts +0 -11
- package/dist/src/actions/finalize-workspace.d.ts.map +0 -1
- package/dist/src/actions/list-agents.d.ts +0 -11
- package/dist/src/actions/list-agents.d.ts.map +0 -1
- package/dist/src/actions/manage-issues.d.ts +0 -11
- package/dist/src/actions/manage-issues.d.ts.map +0 -1
- package/dist/src/actions/provision-workspace.d.ts +0 -11
- package/dist/src/actions/provision-workspace.d.ts.map +0 -1
- package/dist/src/actions/send-to-agent.d.ts +0 -11
- package/dist/src/actions/send-to-agent.d.ts.map +0 -1
- package/dist/src/actions/spawn-agent.d.ts +0 -11
- package/dist/src/actions/spawn-agent.d.ts.map +0 -1
- package/dist/src/actions/start-coding-task.d.ts +0 -17
- package/dist/src/actions/start-coding-task.d.ts.map +0 -1
- package/dist/src/actions/stop-agent.d.ts +0 -11
- package/dist/src/actions/stop-agent.d.ts.map +0 -1
- package/dist/src/api/agent-routes.d.ts +0 -18
- package/dist/src/api/agent-routes.d.ts.map +0 -1
- package/dist/src/api/coordinator-routes.d.ts +0 -22
- package/dist/src/api/coordinator-routes.d.ts.map +0 -1
- package/dist/src/api/issue-routes.d.ts +0 -17
- package/dist/src/api/issue-routes.d.ts.map +0 -1
- package/dist/src/api/routes.d.ts +0 -36
- package/dist/src/api/routes.d.ts.map +0 -1
- package/dist/src/api/workspace-routes.d.ts +0 -17
- package/dist/src/api/workspace-routes.d.ts.map +0 -1
- package/dist/src/index.d.ts +0 -32
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/providers/action-examples.d.ts +0 -13
- package/dist/src/providers/action-examples.d.ts.map +0 -1
- package/dist/src/providers/active-workspace-context.d.ts +0 -13
- package/dist/src/providers/active-workspace-context.d.ts.map +0 -1
- package/dist/src/services/agent-metrics.d.ts +0 -28
- package/dist/src/services/agent-metrics.d.ts.map +0 -1
- package/dist/src/services/agent-selection.d.ts +0 -53
- package/dist/src/services/agent-selection.d.ts.map +0 -1
- package/dist/src/services/ansi-utils.d.ts +0 -48
- package/dist/src/services/ansi-utils.d.ts.map +0 -1
- package/dist/src/services/pty-auto-response.d.ts +0 -30
- package/dist/src/services/pty-auto-response.d.ts.map +0 -1
- package/dist/src/services/pty-init.d.ts +0 -43
- package/dist/src/services/pty-init.d.ts.map +0 -1
- package/dist/src/services/pty-service.d.ts +0 -92
- package/dist/src/services/pty-service.d.ts.map +0 -1
- package/dist/src/services/pty-session-io.d.ts +0 -46
- package/dist/src/services/pty-session-io.d.ts.map +0 -1
- package/dist/src/services/pty-spawn.d.ts +0 -50
- package/dist/src/services/pty-spawn.d.ts.map +0 -1
- package/dist/src/services/pty-types.d.ts +0 -80
- package/dist/src/services/pty-types.d.ts.map +0 -1
- package/dist/src/services/stall-classifier.d.ts +0 -44
- package/dist/src/services/stall-classifier.d.ts.map +0 -1
- package/dist/src/services/swarm-coordinator-prompts.d.ts +0 -62
- package/dist/src/services/swarm-coordinator-prompts.d.ts.map +0 -1
- package/dist/src/services/swarm-coordinator.d.ts +0 -163
- package/dist/src/services/swarm-coordinator.d.ts.map +0 -1
- package/dist/src/services/swarm-decision-loop.d.ts +0 -39
- package/dist/src/services/swarm-decision-loop.d.ts.map +0 -1
- package/dist/src/services/swarm-idle-watchdog.d.ts +0 -22
- package/dist/src/services/swarm-idle-watchdog.d.ts.map +0 -1
- package/dist/src/services/workspace-git-ops.d.ts +0 -28
- package/dist/src/services/workspace-git-ops.d.ts.map +0 -1
- package/dist/src/services/workspace-github.d.ts +0 -58
- package/dist/src/services/workspace-github.d.ts.map +0 -1
- package/dist/src/services/workspace-lifecycle.d.ts +0 -18
- package/dist/src/services/workspace-lifecycle.d.ts.map +0 -1
- package/dist/src/services/workspace-service.d.ts +0 -84
- package/dist/src/services/workspace-service.d.ts.map +0 -1
- package/dist/src/services/workspace-types.d.ts +0 -81
- package/dist/src/services/workspace-types.d.ts.map +0 -1
- package/dist/tsconfig.build.tsbuildinfo +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
package/dist/index.js
CHANGED
|
@@ -114,11 +114,31 @@ function buildTurnCompletePrompt(taskCtx, turnOutput, decisionHistory) {
|
|
|
114
114
|
|
|
115
115
|
` + `Guidelines:\n` + `- BEFORE choosing "complete", enumerate each objective from the original task and verify ` + `evidence in the output. If ANY objective lacks evidence, use "respond" with the missing work.\n` + `- A PR being created does NOT mean the task is done \u2014 check that the PR covers ALL requested changes.
|
|
116
116
|
` + `- If the task mentions multiple features/fixes, verify EACH one is addressed, not just the first.\n` + `- If the agent only analyzed code or read files, it hasn't done the actual work yet \u2014 send a follow-up.
|
|
117
|
-
` + `- If the agent wrote code but didn't test it and testing seems appropriate, ask it to run tests.\n` + `- If the output shows errors or failed tests, send a follow-up to fix them.\n` + `- IMPORTANT: If the working directory is a git repository clone (not a scratch dir), the agent ` + `MUST commit its changes, push them, and create a pull request before the task can be "complete". ` + `If the output only shows code edits with no git commit or PR, respond with "Now commit your changes, push, and create a pull request".\n` + `-
|
|
118
|
-
` + `-
|
|
117
|
+
` + `- If the agent wrote code but didn't test it and testing seems appropriate, ask it to run tests.\n` + `- If the output shows errors or failed tests, send a follow-up to fix them.\n` + `- IMPORTANT: If the working directory is a git repository clone (not a scratch dir), the agent ` + `MUST commit its changes, push them, and create a pull request before the task can be "complete". ` + `If the output only shows code edits with no git commit or PR, respond with "Now commit your changes, push, and create a pull request".\n` + `- IMPORTANT: Creating a PR is NOT the final step. If this is the turn where the PR was created ` + `(i.e. "Created pull request" or a PR URL appears for the FIRST time and no previous decision ` + `already sent a review follow-up), respond with "Review your PR, run each test plan item to verify ` + `it works, update the PR to check off each item, then confirm all items pass".\n` + `- If a previous decision ALREADY sent a review/verification follow-up (check the decision history), ` + `and the agent has now responded with its review results, you MAY mark "complete" if the agent ` + `indicates the work is done (e.g. "Done", "verified", "all checks pass", "Here's what I did", ` + `or a clear summary of completed work). Do NOT require exact phrases \u2014 use judgment.
|
|
118
|
+
` + `- Keep follow-up instructions concise and specific.\n` + `- 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.\n` + `- Default to "respond" \u2014 only use "complete" when you're certain ALL work is done.
|
|
119
119
|
|
|
120
120
|
` + `Respond with ONLY a JSON object:\n` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "..."}`;
|
|
121
121
|
}
|
|
122
|
+
function buildBlockedEventMessage(taskCtx, promptText, recentOutput, decisionHistory) {
|
|
123
|
+
const historySection = decisionHistory.length > 0 ? `\nPrevious decisions:\n${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] "${d.promptText}" \u2192 ${d.action}${d.response ? ` ("${d.response}")` : ""} \u2014 ${d.reasoning}`).join("\n")}\n` : "";
|
|
124
|
+
return `[Coding Agent Event] A ${taskCtx.agentType} agent ("${taskCtx.label}") is blocked and waiting for input.\n\n` + `Task: "${taskCtx.originalTask}"\n` + `Workdir: ${taskCtx.workdir}\n` + `Repo: ${taskCtx.repo ?? "none (scratch directory)"}\n` + historySection + `\nRecent terminal output:\n---\n${recentOutput.slice(-3000)}\n---\n\n` + `Blocking prompt: "${promptText}"\n\n` + `Decide how to handle this. Options:\n` + `- "respond" \u2014 send text or keys to unblock the agent
|
|
125
|
+
` + `- "complete" \u2014 the task is fully done
|
|
126
|
+
` + `- "escalate" \u2014 you need the user's input
|
|
127
|
+
` + `- "ignore" \u2014 not actually blocking
|
|
128
|
+
|
|
129
|
+
` + `Guidelines:\n` + `- For tool approvals / Y/n that align with the task, respond "y" or keys:["enter"].\n` + `- If the prompt asks for info NOT in the original task, escalate.\n` + `- Decline access to paths outside ${taskCtx.workdir}.\n` + `- If a PR was just created, respond to review & verify test plan items before completing.\n` + `- When in doubt, escalate.\n\n` + `Include a JSON action block at the end of your response:\n` + "```json\n" + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "..."}\n` + "```";
|
|
130
|
+
}
|
|
131
|
+
function buildTurnCompleteEventMessage(taskCtx, turnOutput, decisionHistory) {
|
|
132
|
+
const historySection = decisionHistory.length > 0 ? `\nPrevious decisions:\n${decisionHistory.slice(-5).map((d, i) => ` ${i + 1}. [${d.event}] "${d.promptText}" \u2192 ${d.action}${d.response ? ` ("${d.response}")` : ""} \u2014 ${d.reasoning}`).join("\n")}\n` : "";
|
|
133
|
+
return `[Coding Agent Event] A ${taskCtx.agentType} agent ("${taskCtx.label}") just finished a turn and is idle.\n\n` + `Task: "${taskCtx.originalTask}"\n` + `Workdir: ${taskCtx.workdir}\n` + `Repo: ${taskCtx.repo ?? "none (scratch directory)"}\n` + historySection + `\nTurn output:\n---\n${turnOutput.slice(-3000)}\n---\n\n` + `Decide if the overall task is done or if the agent needs more work.\n\n` + `Options:\n` + `- "respond" \u2014 send a follow-up instruction (DEFAULT for intermediate steps)
|
|
134
|
+
` + `- "complete" \u2014 ALL task objectives met (code written, committed, PR created & verified)
|
|
135
|
+
` + `- "escalate" \u2014 something looks wrong, ask the user
|
|
136
|
+
` + `- "ignore" \u2014 should not normally be used here
|
|
137
|
+
|
|
138
|
+
` + `Guidelines:\n` + `- Verify evidence for EVERY objective before using "complete".\n` + `- If code was written but not committed/pushed/PR'd, respond with next step.\n` + `- If a PR was just created, respond to review & verify test plan items.\n` + `- When asking agents to verify work, prefer CLI tools (gh, curl, cat, etc.) over browser automation.\n` + `- Default to "respond" \u2014 only "complete" when certain ALL work is done.
|
|
139
|
+
|
|
140
|
+
` + `Include a JSON action block at the end of your response:\n` + "```json\n" + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "..."}\n` + "```";
|
|
141
|
+
}
|
|
122
142
|
function parseCoordinationResponse(llmOutput) {
|
|
123
143
|
const jsonMatch = llmOutput.match(/\{[\s\S]*\}/);
|
|
124
144
|
if (!jsonMatch)
|
|
@@ -148,6 +168,123 @@ function parseCoordinationResponse(llmOutput) {
|
|
|
148
168
|
}
|
|
149
169
|
}
|
|
150
170
|
|
|
171
|
+
// src/services/swarm-event-triage.ts
|
|
172
|
+
import {ModelType as ModelType2} from "@elizaos/core";
|
|
173
|
+
function classifyByHeuristic(ctx) {
|
|
174
|
+
if (ctx.promptType) {
|
|
175
|
+
if (ROUTINE_PROMPT_TYPES.has(ctx.promptType))
|
|
176
|
+
return "routine";
|
|
177
|
+
if (CREATIVE_PROMPT_TYPES.has(ctx.promptType))
|
|
178
|
+
return "creative";
|
|
179
|
+
}
|
|
180
|
+
if (ctx.eventType === "blocked" && ctx.promptText) {
|
|
181
|
+
const hasRoutine = ROUTINE_PATTERNS.some((r) => r.test(ctx.promptText));
|
|
182
|
+
const hasCreative = CREATIVE_PATTERNS.some((r) => r.test(ctx.promptText));
|
|
183
|
+
if (hasRoutine && !hasCreative)
|
|
184
|
+
return "routine";
|
|
185
|
+
if (hasCreative && !hasRoutine)
|
|
186
|
+
return "creative";
|
|
187
|
+
if (hasCreative)
|
|
188
|
+
return "creative";
|
|
189
|
+
}
|
|
190
|
+
if (ctx.eventType === "turn_complete" && ctx.recentOutput) {
|
|
191
|
+
const isTerminal = TERMINAL_OUTPUT_PATTERNS.some((r) => r.test(ctx.recentOutput));
|
|
192
|
+
const isIntermediate = INTERMEDIATE_OUTPUT_PATTERNS.some((r) => r.test(ctx.recentOutput));
|
|
193
|
+
if (isTerminal || isIntermediate)
|
|
194
|
+
return "routine";
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
function buildTriagePrompt(ctx) {
|
|
199
|
+
const eventDesc = ctx.eventType === "blocked" ? `BLOCKED prompt: "${ctx.promptText.slice(0, 300)}"` : `TURN COMPLETE. Recent output:\n${(ctx.recentOutput ?? "").slice(-500)}`;
|
|
200
|
+
return `Classify this coding agent event as "routine" or "creative".\n\n` + `Task: ${ctx.originalTask.slice(0, 200)}\n` + `Event: ${eventDesc}\n\n` + `"routine" = simple approval, permission, config, yes/no, tool consent, obvious pass/fail.\n` + `"creative" = needs task context, error recovery, design choice, ambiguous situation, approach selection.\n\n` + `Respond with ONLY a JSON object: {"tier": "routine"} or {"tier": "creative"}`;
|
|
201
|
+
}
|
|
202
|
+
function parseTriageResponse(llmOutput) {
|
|
203
|
+
const matches = llmOutput.matchAll(/\{[\s\S]*?\}/g);
|
|
204
|
+
for (const match of matches) {
|
|
205
|
+
try {
|
|
206
|
+
const parsed = JSON.parse(match[0]);
|
|
207
|
+
if (parsed.tier === "routine" || parsed.tier === "creative") {
|
|
208
|
+
return parsed.tier;
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
async function classifyEventTier(runtime, ctx, log) {
|
|
216
|
+
const heuristicResult = classifyByHeuristic(ctx);
|
|
217
|
+
if (heuristicResult) {
|
|
218
|
+
log(`Triage: heuristic \u2192 ${heuristicResult}`);
|
|
219
|
+
return heuristicResult;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const prompt = buildTriagePrompt(ctx);
|
|
223
|
+
const result = await runtime.useModel(ModelType2.TEXT_SMALL, { prompt });
|
|
224
|
+
const tier = parseTriageResponse(result);
|
|
225
|
+
if (tier) {
|
|
226
|
+
log(`Triage: LLM \u2192 ${tier}`);
|
|
227
|
+
return tier;
|
|
228
|
+
}
|
|
229
|
+
log(`Triage: LLM returned unparseable response \u2014 defaulting to creative`);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
log(`Triage: LLM classifier failed: ${err} \u2014 defaulting to creative`);
|
|
232
|
+
}
|
|
233
|
+
return "creative";
|
|
234
|
+
}
|
|
235
|
+
var ROUTINE_PROMPT_TYPES, CREATIVE_PROMPT_TYPES, ROUTINE_PATTERNS, CREATIVE_PATTERNS, TERMINAL_OUTPUT_PATTERNS, INTERMEDIATE_OUTPUT_PATTERNS;
|
|
236
|
+
var init_swarm_event_triage = __esm(() => {
|
|
237
|
+
ROUTINE_PROMPT_TYPES = new Set([
|
|
238
|
+
"permission",
|
|
239
|
+
"config",
|
|
240
|
+
"tos",
|
|
241
|
+
"tool_wait"
|
|
242
|
+
]);
|
|
243
|
+
CREATIVE_PROMPT_TYPES = new Set(["project_select", "model_select"]);
|
|
244
|
+
ROUTINE_PATTERNS = [
|
|
245
|
+
/\bAllow\s+tool\b/i,
|
|
246
|
+
/\(Y\/n\)/,
|
|
247
|
+
/\(y\/N\)/,
|
|
248
|
+
/\bTrust\s+(this\s+)?directory\b/i,
|
|
249
|
+
/\bProceed\?/i,
|
|
250
|
+
/\boverwrite\?/i,
|
|
251
|
+
/\bDo you trust\b/i,
|
|
252
|
+
/\bAllow access\b/i,
|
|
253
|
+
/\bGrant permission\b/i,
|
|
254
|
+
/\bAccept\?/i,
|
|
255
|
+
/\bContinue\?/i,
|
|
256
|
+
/\bPermit\b.*\?/i,
|
|
257
|
+
/\bApprove\b.*\?/i
|
|
258
|
+
];
|
|
259
|
+
CREATIVE_PATTERNS = [
|
|
260
|
+
/\bWhich approach\b/i,
|
|
261
|
+
/\bHow should\b/i,
|
|
262
|
+
/\btests? failing\b/i,
|
|
263
|
+
/\bchoose between\b/i,
|
|
264
|
+
/\bpick (one|a|an)\b/i,
|
|
265
|
+
/\bWhat do you (want|think)\b/i,
|
|
266
|
+
/\berror recover/i,
|
|
267
|
+
/\bfailed with\b/i,
|
|
268
|
+
/\bcompilation error/i,
|
|
269
|
+
/\bbuild failed\b/i,
|
|
270
|
+
/\btype error/i,
|
|
271
|
+
/\bmerge conflict/i
|
|
272
|
+
];
|
|
273
|
+
TERMINAL_OUTPUT_PATTERNS = [
|
|
274
|
+
/All \d+ tests? pass/i,
|
|
275
|
+
/Tests?:\s+\d+ passed/i,
|
|
276
|
+
/✓ All checks passed/i,
|
|
277
|
+
/https:\/\/github\.com\/[^\s]+\/pull\/\d+/,
|
|
278
|
+
/Successfully created PR/i,
|
|
279
|
+
/Commit [a-f0-9]{7,40}/i
|
|
280
|
+
];
|
|
281
|
+
INTERMEDIATE_OUTPUT_PATTERNS = [
|
|
282
|
+
/^Running tests?\.\.\./im,
|
|
283
|
+
/^Building\.\.\./im,
|
|
284
|
+
/^Installing dependencies/im
|
|
285
|
+
];
|
|
286
|
+
});
|
|
287
|
+
|
|
151
288
|
// src/services/swarm-decision-loop.ts
|
|
152
289
|
var exports_swarm_decision_loop = {};
|
|
153
290
|
__export(exports_swarm_decision_loop, {
|
|
@@ -157,10 +294,11 @@ __export(exports_swarm_decision_loop, {
|
|
|
157
294
|
handleConfirmDecision: () => handleConfirmDecision,
|
|
158
295
|
handleBlocked: () => handleBlocked,
|
|
159
296
|
handleAutonomousDecision: () => handleAutonomousDecision,
|
|
160
|
-
executeDecision: () => executeDecision
|
|
297
|
+
executeDecision: () => executeDecision,
|
|
298
|
+
checkAllTasksComplete: () => checkAllTasksComplete
|
|
161
299
|
});
|
|
162
300
|
import * as path from "node:path";
|
|
163
|
-
import {ModelType as
|
|
301
|
+
import {ModelType as ModelType3} from "@elizaos/core";
|
|
164
302
|
function toContextSummary(taskCtx) {
|
|
165
303
|
return {
|
|
166
304
|
sessionId: taskCtx.sessionId,
|
|
@@ -203,6 +341,40 @@ function isOutOfScopeAccess(promptText, workdir) {
|
|
|
203
341
|
return !resolved.startsWith(resolvedWorkdir + path.sep) && resolved !== resolvedWorkdir;
|
|
204
342
|
});
|
|
205
343
|
}
|
|
344
|
+
function checkAllTasksComplete(ctx) {
|
|
345
|
+
const tasks = Array.from(ctx.tasks.values());
|
|
346
|
+
if (tasks.length === 0)
|
|
347
|
+
return;
|
|
348
|
+
const terminalStates = new Set(["completed", "stopped", "error"]);
|
|
349
|
+
const allDone = tasks.every((t) => terminalStates.has(t.status));
|
|
350
|
+
if (!allDone)
|
|
351
|
+
return;
|
|
352
|
+
const completed = tasks.filter((t) => t.status === "completed");
|
|
353
|
+
const stopped = tasks.filter((t) => t.status === "stopped");
|
|
354
|
+
const errored = tasks.filter((t) => t.status === "error");
|
|
355
|
+
const parts = [];
|
|
356
|
+
if (completed.length > 0) {
|
|
357
|
+
parts.push(`${completed.length} completed`);
|
|
358
|
+
}
|
|
359
|
+
if (stopped.length > 0) {
|
|
360
|
+
parts.push(`${stopped.length} stopped`);
|
|
361
|
+
}
|
|
362
|
+
if (errored.length > 0) {
|
|
363
|
+
parts.push(`${errored.length} errored`);
|
|
364
|
+
}
|
|
365
|
+
ctx.sendChatMessage(`All ${tasks.length} coding agents finished (${parts.join(", ")}). Review their work when you're ready.`, "coding-agent");
|
|
366
|
+
ctx.broadcast({
|
|
367
|
+
type: "swarm_complete",
|
|
368
|
+
sessionId: "",
|
|
369
|
+
timestamp: Date.now(),
|
|
370
|
+
data: {
|
|
371
|
+
total: tasks.length,
|
|
372
|
+
completed: completed.length,
|
|
373
|
+
stopped: stopped.length,
|
|
374
|
+
errored: errored.length
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
206
378
|
async function fetchRecentOutput(ctx, sessionId, lines = 50) {
|
|
207
379
|
if (!ctx.ptyService)
|
|
208
380
|
return "";
|
|
@@ -215,7 +387,7 @@ async function fetchRecentOutput(ctx, sessionId, lines = 50) {
|
|
|
215
387
|
async function makeCoordinationDecision(ctx, taskCtx, promptText, recentOutput) {
|
|
216
388
|
const prompt = buildCoordinationPrompt(toContextSummary(taskCtx), promptText, recentOutput, toDecisionHistory(taskCtx));
|
|
217
389
|
try {
|
|
218
|
-
const result = await ctx.runtime.useModel(
|
|
390
|
+
const result = await ctx.runtime.useModel(ModelType3.TEXT_SMALL, {
|
|
219
391
|
prompt
|
|
220
392
|
});
|
|
221
393
|
return parseCoordinationResponse(result);
|
|
@@ -253,9 +425,10 @@ async function executeDecision(ctx, sessionId, decision) {
|
|
|
253
425
|
} catch {
|
|
254
426
|
}
|
|
255
427
|
ctx.sendChatMessage(summary ? `Finished "${taskCtx?.label ?? sessionId}".\n\n${summary}` : `Finished "${taskCtx?.label ?? sessionId}".`, "coding-agent");
|
|
256
|
-
ctx.ptyService.stopSession(sessionId).catch((err) => {
|
|
428
|
+
ctx.ptyService.stopSession(sessionId, true).catch((err) => {
|
|
257
429
|
ctx.log(`Failed to stop session after LLM-detected completion: ${err}`);
|
|
258
430
|
});
|
|
431
|
+
checkAllTasksComplete(ctx);
|
|
259
432
|
break;
|
|
260
433
|
}
|
|
261
434
|
case "escalate":
|
|
@@ -296,7 +469,7 @@ async function handleBlocked(ctx, sessionId, taskCtx, data) {
|
|
|
296
469
|
});
|
|
297
470
|
ctx.sendChatMessage(`[${taskCtx.label}] WARNING: Auto-approved access to path outside workspace (${taskCtx.workdir}). ` + `Prompt: "${promptText.slice(0, 150)}". Stopping session for safety.`, "coding-agent");
|
|
298
471
|
taskCtx.status = "error";
|
|
299
|
-
ctx.ptyService?.stopSession(sessionId).catch((err) => {
|
|
472
|
+
ctx.ptyService?.stopSession(sessionId, true).catch((err) => {
|
|
300
473
|
ctx.log(`Failed to stop session after out-of-scope auto-approval: ${err}`);
|
|
301
474
|
});
|
|
302
475
|
return;
|
|
@@ -357,10 +530,10 @@ async function handleBlocked(ctx, sessionId, taskCtx, data) {
|
|
|
357
530
|
}
|
|
358
531
|
switch (ctx.getSupervisionLevel()) {
|
|
359
532
|
case "autonomous":
|
|
360
|
-
await handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, "");
|
|
533
|
+
await handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, "", eventData.promptInfo?.type);
|
|
361
534
|
break;
|
|
362
535
|
case "confirm":
|
|
363
|
-
await handleConfirmDecision(ctx, sessionId, taskCtx, promptText, "");
|
|
536
|
+
await handleConfirmDecision(ctx, sessionId, taskCtx, promptText, "", eventData.promptInfo?.type);
|
|
364
537
|
break;
|
|
365
538
|
case "notify":
|
|
366
539
|
taskCtx.decisions.push({
|
|
@@ -387,21 +560,54 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
|
|
|
387
560
|
const raw = await fetchRecentOutput(ctx, sessionId);
|
|
388
561
|
turnOutput = cleanForChat(raw);
|
|
389
562
|
}
|
|
390
|
-
const
|
|
563
|
+
const agentDecisionCb = ctx.getAgentDecisionCallback();
|
|
391
564
|
let decision = null;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
565
|
+
let decisionFromPipeline = false;
|
|
566
|
+
const triageCtx = {
|
|
567
|
+
eventType: "turn_complete",
|
|
568
|
+
promptText: "",
|
|
569
|
+
recentOutput: turnOutput,
|
|
570
|
+
originalTask: taskCtx.originalTask
|
|
571
|
+
};
|
|
572
|
+
const tier = agentDecisionCb ? await classifyEventTier(ctx.runtime, triageCtx, ctx.log) : "routine";
|
|
573
|
+
if (tier === "routine") {
|
|
574
|
+
const prompt = buildTurnCompletePrompt(toContextSummary(taskCtx), turnOutput, toDecisionHistory(taskCtx));
|
|
575
|
+
try {
|
|
576
|
+
const result = await ctx.runtime.useModel(ModelType3.TEXT_SMALL, {
|
|
577
|
+
prompt
|
|
578
|
+
});
|
|
579
|
+
decision = parseCoordinationResponse(result);
|
|
580
|
+
} catch (err) {
|
|
581
|
+
ctx.log(`Turn-complete LLM call failed: ${err}`);
|
|
582
|
+
}
|
|
583
|
+
} else {
|
|
584
|
+
if (agentDecisionCb) {
|
|
585
|
+
const eventMessage = buildTurnCompleteEventMessage(toContextSummary(taskCtx), turnOutput, toDecisionHistory(taskCtx));
|
|
586
|
+
try {
|
|
587
|
+
decision = await agentDecisionCb(eventMessage, sessionId, taskCtx);
|
|
588
|
+
if (decision)
|
|
589
|
+
decisionFromPipeline = true;
|
|
590
|
+
} catch (err) {
|
|
591
|
+
ctx.log(`Agent decision callback failed for turn-complete: ${err} \u2014 falling back to small LLM`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (!decision) {
|
|
595
|
+
const prompt = buildTurnCompletePrompt(toContextSummary(taskCtx), turnOutput, toDecisionHistory(taskCtx));
|
|
596
|
+
try {
|
|
597
|
+
const result = await ctx.runtime.useModel(ModelType3.TEXT_SMALL, {
|
|
598
|
+
prompt
|
|
599
|
+
});
|
|
600
|
+
decision = parseCoordinationResponse(result);
|
|
601
|
+
} catch (err) {
|
|
602
|
+
ctx.log(`Turn-complete LLM fallback call failed: ${err}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
399
605
|
}
|
|
400
606
|
if (!decision) {
|
|
401
|
-
ctx.log(`Turn-complete for "${taskCtx.label}":
|
|
607
|
+
ctx.log(`Turn-complete for "${taskCtx.label}": all decision paths failed \u2014 escalating`);
|
|
402
608
|
decision = {
|
|
403
|
-
action: "
|
|
404
|
-
reasoning: "
|
|
609
|
+
action: "escalate",
|
|
610
|
+
reasoning: "All decision paths returned invalid response \u2014 escalating for human review"
|
|
405
611
|
};
|
|
406
612
|
}
|
|
407
613
|
ctx.log(`Turn assessment for "${taskCtx.label}": ${decision.action}${decision.action === "respond" ? ` \u2192 "${(decision.response ?? "").slice(0, 80)}"` : ""} \u2014 ${decision.reasoning.slice(0, 120)}`);
|
|
@@ -422,19 +628,21 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
|
|
|
422
628
|
reasoning: decision.reasoning
|
|
423
629
|
}
|
|
424
630
|
});
|
|
425
|
-
if (
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
631
|
+
if (!decisionFromPipeline) {
|
|
632
|
+
if (decision.action === "respond") {
|
|
633
|
+
const instruction = decision.response ?? "";
|
|
634
|
+
const preview = instruction.length > 120 ? `${instruction.slice(0, 120)}...` : instruction;
|
|
635
|
+
ctx.sendChatMessage(`[${taskCtx.label}] Turn done, continuing: ${preview}`, "coding-agent");
|
|
636
|
+
} else if (decision.action === "escalate") {
|
|
637
|
+
ctx.sendChatMessage(`[${taskCtx.label}] Turn finished \u2014 needs your attention: ${decision.reasoning}`, "coding-agent");
|
|
638
|
+
}
|
|
431
639
|
}
|
|
432
640
|
await executeDecision(ctx, sessionId, decision);
|
|
433
641
|
} finally {
|
|
434
642
|
ctx.inFlightDecisions.delete(sessionId);
|
|
435
643
|
}
|
|
436
644
|
}
|
|
437
|
-
async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, recentOutput) {
|
|
645
|
+
async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, recentOutput, promptType) {
|
|
438
646
|
if (ctx.inFlightDecisions.has(sessionId)) {
|
|
439
647
|
ctx.log(`Skipping duplicate decision for ${sessionId} (in-flight)`);
|
|
440
648
|
return;
|
|
@@ -445,14 +653,41 @@ async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, rec
|
|
|
445
653
|
if (!output) {
|
|
446
654
|
output = await fetchRecentOutput(ctx, sessionId);
|
|
447
655
|
}
|
|
448
|
-
|
|
656
|
+
const agentDecisionCb = ctx.getAgentDecisionCallback();
|
|
657
|
+
let decision = null;
|
|
658
|
+
let decisionFromPipeline = false;
|
|
659
|
+
const triageCtx = {
|
|
660
|
+
eventType: "blocked",
|
|
661
|
+
promptText,
|
|
662
|
+
promptType,
|
|
663
|
+
recentOutput: output,
|
|
664
|
+
originalTask: taskCtx.originalTask
|
|
665
|
+
};
|
|
666
|
+
const tier = agentDecisionCb ? await classifyEventTier(ctx.runtime, triageCtx, ctx.log) : "routine";
|
|
667
|
+
if (tier === "routine") {
|
|
668
|
+
decision = await makeCoordinationDecision(ctx, taskCtx, promptText, output);
|
|
669
|
+
} else {
|
|
670
|
+
if (agentDecisionCb) {
|
|
671
|
+
const eventMessage = buildBlockedEventMessage(toContextSummary(taskCtx), promptText, output, toDecisionHistory(taskCtx));
|
|
672
|
+
try {
|
|
673
|
+
decision = await agentDecisionCb(eventMessage, sessionId, taskCtx);
|
|
674
|
+
if (decision)
|
|
675
|
+
decisionFromPipeline = true;
|
|
676
|
+
} catch (err) {
|
|
677
|
+
ctx.log(`Agent decision callback failed: ${err} \u2014 falling back to small LLM`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (!decision) {
|
|
681
|
+
decision = await makeCoordinationDecision(ctx, taskCtx, promptText, output);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
449
684
|
if (!decision) {
|
|
450
685
|
taskCtx.decisions.push({
|
|
451
686
|
timestamp: Date.now(),
|
|
452
687
|
event: "blocked",
|
|
453
688
|
promptText,
|
|
454
689
|
decision: "escalate",
|
|
455
|
-
reasoning: "
|
|
690
|
+
reasoning: "All decision paths returned invalid coordination response"
|
|
456
691
|
});
|
|
457
692
|
ctx.broadcast({
|
|
458
693
|
type: "escalation",
|
|
@@ -494,19 +729,21 @@ async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, rec
|
|
|
494
729
|
reasoning: decision.reasoning
|
|
495
730
|
}
|
|
496
731
|
});
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
732
|
+
if (!decisionFromPipeline) {
|
|
733
|
+
if (decision.action === "respond") {
|
|
734
|
+
const actionDesc = decision.useKeys ? `Sent keys: ${decision.keys?.join(", ")}` : decision.response ? `Responded: ${decision.response.length > 100 ? `${decision.response.slice(0, 100)}...` : decision.response}` : "Responded";
|
|
735
|
+
const reasonExcerpt = decision.reasoning.length > 150 ? `${decision.reasoning.slice(0, 150)}...` : decision.reasoning;
|
|
736
|
+
ctx.sendChatMessage(`[${taskCtx.label}] ${actionDesc} \u2014 ${reasonExcerpt}`, "coding-agent");
|
|
737
|
+
} else if (decision.action === "escalate") {
|
|
738
|
+
ctx.sendChatMessage(`[${taskCtx.label}] Needs your attention: ${decision.reasoning}`, "coding-agent");
|
|
739
|
+
}
|
|
503
740
|
}
|
|
504
741
|
await executeDecision(ctx, sessionId, decision);
|
|
505
742
|
} finally {
|
|
506
743
|
ctx.inFlightDecisions.delete(sessionId);
|
|
507
744
|
}
|
|
508
745
|
}
|
|
509
|
-
async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recentOutput) {
|
|
746
|
+
async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recentOutput, promptType) {
|
|
510
747
|
if (ctx.inFlightDecisions.has(sessionId))
|
|
511
748
|
return;
|
|
512
749
|
ctx.inFlightDecisions.add(sessionId);
|
|
@@ -515,7 +752,34 @@ async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recent
|
|
|
515
752
|
if (!output) {
|
|
516
753
|
output = await fetchRecentOutput(ctx, sessionId);
|
|
517
754
|
}
|
|
518
|
-
const
|
|
755
|
+
const agentDecisionCb = ctx.getAgentDecisionCallback();
|
|
756
|
+
let decision = null;
|
|
757
|
+
let decisionFromPipeline = false;
|
|
758
|
+
const triageCtx = {
|
|
759
|
+
eventType: "blocked",
|
|
760
|
+
promptText,
|
|
761
|
+
promptType,
|
|
762
|
+
recentOutput: output,
|
|
763
|
+
originalTask: taskCtx.originalTask
|
|
764
|
+
};
|
|
765
|
+
const tier = agentDecisionCb ? await classifyEventTier(ctx.runtime, triageCtx, ctx.log) : "routine";
|
|
766
|
+
if (tier === "routine") {
|
|
767
|
+
decision = await makeCoordinationDecision(ctx, taskCtx, promptText, output);
|
|
768
|
+
} else {
|
|
769
|
+
if (agentDecisionCb) {
|
|
770
|
+
const eventMessage = buildBlockedEventMessage(toContextSummary(taskCtx), promptText, output, toDecisionHistory(taskCtx));
|
|
771
|
+
try {
|
|
772
|
+
decision = await agentDecisionCb(eventMessage, sessionId, taskCtx);
|
|
773
|
+
if (decision)
|
|
774
|
+
decisionFromPipeline = true;
|
|
775
|
+
} catch (err) {
|
|
776
|
+
ctx.log(`Agent decision callback failed (confirm): ${err} \u2014 falling back to small LLM`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (!decision) {
|
|
780
|
+
decision = await makeCoordinationDecision(ctx, taskCtx, promptText, output);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
519
783
|
if (!decision) {
|
|
520
784
|
ctx.pendingDecisions.set(sessionId, {
|
|
521
785
|
sessionId,
|
|
@@ -523,7 +787,7 @@ async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recent
|
|
|
523
787
|
recentOutput: output,
|
|
524
788
|
llmDecision: {
|
|
525
789
|
action: "escalate",
|
|
526
|
-
reasoning: "
|
|
790
|
+
reasoning: "All decision paths returned invalid response \u2014 needs human review"
|
|
527
791
|
},
|
|
528
792
|
taskContext: taskCtx,
|
|
529
793
|
createdAt: Date.now()
|
|
@@ -546,7 +810,8 @@ async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recent
|
|
|
546
810
|
prompt: promptText,
|
|
547
811
|
suggestedAction: decision?.action,
|
|
548
812
|
suggestedResponse: decision?.response,
|
|
549
|
-
reasoning: decision?.reasoning
|
|
813
|
+
reasoning: decision?.reasoning,
|
|
814
|
+
fromPipeline: decisionFromPipeline
|
|
550
815
|
}
|
|
551
816
|
});
|
|
552
817
|
} finally {
|
|
@@ -556,6 +821,7 @@ async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recent
|
|
|
556
821
|
var MAX_AUTO_RESPONSES = 10;
|
|
557
822
|
var init_swarm_decision_loop = __esm(() => {
|
|
558
823
|
init_ansi_utils();
|
|
824
|
+
init_swarm_event_triage();
|
|
559
825
|
});
|
|
560
826
|
|
|
561
827
|
// src/actions/finalize-workspace.ts
|
|
@@ -1714,6 +1980,14 @@ isBun,
|
|
|
1714
1980
|
PTYManager,
|
|
1715
1981
|
ShellAdapter
|
|
1716
1982
|
} from "pty-manager";
|
|
1983
|
+
function forwardReadyAsTaskComplete(ctx, session) {
|
|
1984
|
+
if (!ctx.hasActiveTask?.(session.id) || !ctx.hasTaskActivity?.(session.id)) {
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
const response = ctx.taskResponseMarkers.has(session.id) ? captureTaskResponse(session.id, ctx.sessionOutputBuffers, ctx.taskResponseMarkers) : "";
|
|
1988
|
+
ctx.log(`session_ready for active task ${session.id} \u2014 forwarding as task_complete (stall classifier path, response: ${response.length} chars)`);
|
|
1989
|
+
ctx.emitEvent(session.id, "task_complete", { session, response });
|
|
1990
|
+
}
|
|
1717
1991
|
async function initializePTYManager(ctx) {
|
|
1718
1992
|
const usingBunWorker = isBun();
|
|
1719
1993
|
if (usingBunWorker) {
|
|
@@ -1730,6 +2004,7 @@ async function initializePTYManager(ctx) {
|
|
|
1730
2004
|
bunManager.on("session_ready", (session) => {
|
|
1731
2005
|
ctx.log(`session_ready event received for ${session.id} (type: ${session.type}, status: ${session.status})`);
|
|
1732
2006
|
ctx.emitEvent(session.id, "ready", { session });
|
|
2007
|
+
forwardReadyAsTaskComplete(ctx, session);
|
|
1733
2008
|
});
|
|
1734
2009
|
bunManager.on("session_exit", (id, code) => {
|
|
1735
2010
|
ctx.emitEvent(id, "stopped", { reason: `exit code ${code}` });
|
|
@@ -1808,6 +2083,7 @@ async function initializePTYManager(ctx) {
|
|
|
1808
2083
|
}
|
|
1809
2084
|
nodeManager.on("session_ready", (session) => {
|
|
1810
2085
|
ctx.emitEvent(session.id, "ready", { session });
|
|
2086
|
+
forwardReadyAsTaskComplete(ctx, session);
|
|
1811
2087
|
});
|
|
1812
2088
|
nodeManager.on("blocking_prompt", (session, promptInfo, autoResponded) => {
|
|
1813
2089
|
ctx.emitEvent(session.id, "blocked", { promptInfo, autoResponded });
|
|
@@ -1875,26 +2151,40 @@ async function sendKeysToSession(ctx, sessionId, keys) {
|
|
|
1875
2151
|
ptySession.sendKeys(keys);
|
|
1876
2152
|
}
|
|
1877
2153
|
}
|
|
1878
|
-
async function stopSession(ctx, sessionId, sessionMetadata, sessionWorkdirs, log) {
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2154
|
+
async function stopSession(ctx, sessionId, sessionMetadata, sessionWorkdirs, log, force = false) {
|
|
2155
|
+
try {
|
|
2156
|
+
const session = ctx.manager.get(sessionId);
|
|
2157
|
+
if (!session) {
|
|
2158
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
2159
|
+
}
|
|
2160
|
+
if (ctx.usingBunWorker) {
|
|
2161
|
+
if (force) {
|
|
2162
|
+
await ctx.manager.kill(sessionId, "SIGKILL");
|
|
2163
|
+
} else {
|
|
2164
|
+
await ctx.manager.kill(sessionId);
|
|
2165
|
+
}
|
|
2166
|
+
} else {
|
|
2167
|
+
if (force) {
|
|
2168
|
+
await ctx.manager.stop(sessionId, { force: true });
|
|
2169
|
+
} else {
|
|
2170
|
+
await ctx.manager.stop(sessionId);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
} finally {
|
|
2174
|
+
try {
|
|
2175
|
+
const unsubscribe = ctx.outputUnsubscribers.get(sessionId);
|
|
2176
|
+
if (unsubscribe) {
|
|
2177
|
+
unsubscribe();
|
|
2178
|
+
}
|
|
2179
|
+
} catch {
|
|
2180
|
+
}
|
|
1891
2181
|
ctx.outputUnsubscribers.delete(sessionId);
|
|
2182
|
+
sessionMetadata.delete(sessionId);
|
|
2183
|
+
sessionWorkdirs.delete(sessionId);
|
|
2184
|
+
ctx.sessionOutputBuffers.delete(sessionId);
|
|
2185
|
+
ctx.taskResponseMarkers.delete(sessionId);
|
|
2186
|
+
log(`Stopped session ${sessionId}`);
|
|
1892
2187
|
}
|
|
1893
|
-
sessionMetadata.delete(sessionId);
|
|
1894
|
-
sessionWorkdirs.delete(sessionId);
|
|
1895
|
-
ctx.sessionOutputBuffers.delete(sessionId);
|
|
1896
|
-
ctx.taskResponseMarkers.delete(sessionId);
|
|
1897
|
-
log(`Stopped session ${sessionId}`);
|
|
1898
2188
|
}
|
|
1899
2189
|
function subscribeToOutput(ctx, sessionId, callback) {
|
|
1900
2190
|
if (ctx.usingBunWorker) {
|
|
@@ -2228,12 +2518,35 @@ import {logger} from "@elizaos/core";
|
|
|
2228
2518
|
// src/services/swarm-idle-watchdog.ts
|
|
2229
2519
|
init_ansi_utils();
|
|
2230
2520
|
init_swarm_decision_loop();
|
|
2231
|
-
import {ModelType as
|
|
2521
|
+
import {ModelType as ModelType4} from "@elizaos/core";
|
|
2232
2522
|
async function scanIdleSessions(ctx) {
|
|
2233
2523
|
const now = Date.now();
|
|
2234
2524
|
for (const taskCtx of ctx.tasks.values()) {
|
|
2235
2525
|
if (taskCtx.status !== "active")
|
|
2236
2526
|
continue;
|
|
2527
|
+
if (ctx.ptyService) {
|
|
2528
|
+
const session = ctx.ptyService.getSession(taskCtx.sessionId);
|
|
2529
|
+
if (!session) {
|
|
2530
|
+
ctx.log(`Idle watchdog: "${taskCtx.label}" \u2014 PTY session no longer exists, marking as stopped`);
|
|
2531
|
+
taskCtx.status = "stopped";
|
|
2532
|
+
taskCtx.decisions.push({
|
|
2533
|
+
timestamp: now,
|
|
2534
|
+
event: "idle_watchdog",
|
|
2535
|
+
promptText: "PTY session no longer exists",
|
|
2536
|
+
decision: "stopped",
|
|
2537
|
+
reasoning: "Underlying PTY process is gone (likely killed during restart)"
|
|
2538
|
+
});
|
|
2539
|
+
ctx.broadcast({
|
|
2540
|
+
type: "stopped",
|
|
2541
|
+
sessionId: taskCtx.sessionId,
|
|
2542
|
+
timestamp: now,
|
|
2543
|
+
data: { reason: "pty_session_gone" }
|
|
2544
|
+
});
|
|
2545
|
+
ctx.sendChatMessage(`[${taskCtx.label}] Session lost \u2014 the agent process is no longer running (likely killed during a restart).`, "coding-agent");
|
|
2546
|
+
checkAllTasksComplete(ctx);
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2237
2550
|
const idleMs = now - taskCtx.lastActivityAt;
|
|
2238
2551
|
if (idleMs < IDLE_THRESHOLD_MS)
|
|
2239
2552
|
continue;
|
|
@@ -2241,7 +2554,8 @@ async function scanIdleSessions(ctx) {
|
|
|
2241
2554
|
continue;
|
|
2242
2555
|
if (ctx.ptyService) {
|
|
2243
2556
|
try {
|
|
2244
|
-
const
|
|
2557
|
+
const rawOutput = await ctx.ptyService.getSessionOutput(taskCtx.sessionId, 20);
|
|
2558
|
+
const currentOutput = stripAnsi(rawOutput).trim();
|
|
2245
2559
|
const lastSeen = ctx.lastSeenOutput.get(taskCtx.sessionId) ?? "";
|
|
2246
2560
|
ctx.lastSeenOutput.set(taskCtx.sessionId, currentOutput);
|
|
2247
2561
|
if (currentOutput !== lastSeen) {
|
|
@@ -2256,17 +2570,18 @@ async function scanIdleSessions(ctx) {
|
|
|
2256
2570
|
taskCtx.idleCheckCount++;
|
|
2257
2571
|
const idleMinutes = Math.round(idleMs / 60000);
|
|
2258
2572
|
ctx.log(`Idle watchdog: "${taskCtx.label}" idle for ${idleMinutes}m (check ${taskCtx.idleCheckCount}/${MAX_IDLE_CHECKS})`);
|
|
2259
|
-
if (taskCtx.idleCheckCount
|
|
2260
|
-
ctx.log(`Idle watchdog: force-
|
|
2573
|
+
if (taskCtx.idleCheckCount >= MAX_IDLE_CHECKS) {
|
|
2574
|
+
ctx.log(`Idle watchdog: force-stopping "${taskCtx.label}" after ${MAX_IDLE_CHECKS} checks`);
|
|
2575
|
+
taskCtx.status = "stopped";
|
|
2261
2576
|
taskCtx.decisions.push({
|
|
2262
2577
|
timestamp: now,
|
|
2263
2578
|
event: "idle_watchdog",
|
|
2264
2579
|
promptText: `Session idle for ${idleMinutes} minutes`,
|
|
2265
|
-
decision: "
|
|
2266
|
-
reasoning: `Force-
|
|
2580
|
+
decision: "stopped",
|
|
2581
|
+
reasoning: `Force-stopped after ${MAX_IDLE_CHECKS} idle checks with no activity`
|
|
2267
2582
|
});
|
|
2268
2583
|
ctx.broadcast({
|
|
2269
|
-
type: "
|
|
2584
|
+
type: "stopped",
|
|
2270
2585
|
sessionId: taskCtx.sessionId,
|
|
2271
2586
|
timestamp: now,
|
|
2272
2587
|
data: {
|
|
@@ -2275,7 +2590,22 @@ async function scanIdleSessions(ctx) {
|
|
|
2275
2590
|
idleCheckCount: taskCtx.idleCheckCount
|
|
2276
2591
|
}
|
|
2277
2592
|
});
|
|
2278
|
-
ctx.sendChatMessage(`[${taskCtx.label}] Session
|
|
2593
|
+
ctx.sendChatMessage(`[${taskCtx.label}] Session stopped \u2014 idle for ${idleMinutes} minutes with no progress.`, "coding-agent");
|
|
2594
|
+
if (ctx.ptyService) {
|
|
2595
|
+
try {
|
|
2596
|
+
await ctx.ptyService.stopSession(taskCtx.sessionId, true);
|
|
2597
|
+
} catch (err) {
|
|
2598
|
+
ctx.log(`Idle watchdog: failed to stop session ${taskCtx.sessionId}: ${err}`);
|
|
2599
|
+
taskCtx.status = "error";
|
|
2600
|
+
ctx.broadcast({
|
|
2601
|
+
type: "error",
|
|
2602
|
+
sessionId: taskCtx.sessionId,
|
|
2603
|
+
timestamp: now,
|
|
2604
|
+
data: { message: `Failed to stop idle session: ${err}` }
|
|
2605
|
+
});
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
checkAllTasksComplete(ctx);
|
|
2279
2609
|
continue;
|
|
2280
2610
|
}
|
|
2281
2611
|
await handleIdleCheck(ctx, taskCtx, idleMinutes);
|
|
@@ -2311,7 +2641,7 @@ async function handleIdleCheck(ctx, taskCtx, idleMinutes) {
|
|
|
2311
2641
|
const prompt = buildIdleCheckPrompt(contextSummary, recentOutput, idleMinutes, taskCtx.idleCheckCount, MAX_IDLE_CHECKS, decisionHistory);
|
|
2312
2642
|
let decision = null;
|
|
2313
2643
|
try {
|
|
2314
|
-
const result = await ctx.runtime.useModel(
|
|
2644
|
+
const result = await ctx.runtime.useModel(ModelType4.TEXT_SMALL, {
|
|
2315
2645
|
prompt
|
|
2316
2646
|
});
|
|
2317
2647
|
decision = parseCoordinationResponse(result);
|
|
@@ -2362,6 +2692,7 @@ var MAX_IDLE_CHECKS = 4;
|
|
|
2362
2692
|
// src/services/swarm-coordinator.ts
|
|
2363
2693
|
var UNREGISTERED_BUFFER_MS = 2000;
|
|
2364
2694
|
var IDLE_SCAN_INTERVAL_MS = 60 * 1000;
|
|
2695
|
+
var PAUSE_TIMEOUT_MS = 30000;
|
|
2365
2696
|
|
|
2366
2697
|
class SwarmCoordinator {
|
|
2367
2698
|
static serviceType = "SWARM_COORDINATOR";
|
|
@@ -2375,10 +2706,14 @@ class SwarmCoordinator {
|
|
|
2375
2706
|
inFlightDecisions = new Set;
|
|
2376
2707
|
chatCallback = null;
|
|
2377
2708
|
wsBroadcast = null;
|
|
2709
|
+
agentDecisionCb = null;
|
|
2378
2710
|
unregisteredBuffer = new Map;
|
|
2379
2711
|
idleWatchdogTimer = null;
|
|
2380
2712
|
lastSeenOutput = new Map;
|
|
2381
2713
|
lastToolNotification = new Map;
|
|
2714
|
+
_paused = false;
|
|
2715
|
+
pauseBuffer = [];
|
|
2716
|
+
pauseTimeout = null;
|
|
2382
2717
|
constructor(runtime) {
|
|
2383
2718
|
this.runtime = runtime;
|
|
2384
2719
|
}
|
|
@@ -2390,6 +2725,13 @@ class SwarmCoordinator {
|
|
|
2390
2725
|
this.wsBroadcast = cb;
|
|
2391
2726
|
this.log("WS broadcast callback wired");
|
|
2392
2727
|
}
|
|
2728
|
+
setAgentDecisionCallback(cb) {
|
|
2729
|
+
this.agentDecisionCb = cb;
|
|
2730
|
+
this.log("Agent decision callback wired \u2014 events will route through Milaidy");
|
|
2731
|
+
}
|
|
2732
|
+
getAgentDecisionCallback() {
|
|
2733
|
+
return this.agentDecisionCb;
|
|
2734
|
+
}
|
|
2393
2735
|
sendChatMessage(text, source) {
|
|
2394
2736
|
if (!this.chatCallback)
|
|
2395
2737
|
return;
|
|
@@ -2432,8 +2774,49 @@ class SwarmCoordinator {
|
|
|
2432
2774
|
this.unregisteredBuffer.clear();
|
|
2433
2775
|
this.lastSeenOutput.clear();
|
|
2434
2776
|
this.lastToolNotification.clear();
|
|
2777
|
+
this.agentDecisionCb = null;
|
|
2778
|
+
this._paused = false;
|
|
2779
|
+
if (this.pauseTimeout) {
|
|
2780
|
+
clearTimeout(this.pauseTimeout);
|
|
2781
|
+
this.pauseTimeout = null;
|
|
2782
|
+
}
|
|
2783
|
+
this.pauseBuffer = [];
|
|
2435
2784
|
this.log("SwarmCoordinator stopped");
|
|
2436
2785
|
}
|
|
2786
|
+
get isPaused() {
|
|
2787
|
+
return this._paused;
|
|
2788
|
+
}
|
|
2789
|
+
pause() {
|
|
2790
|
+
if (this._paused)
|
|
2791
|
+
return;
|
|
2792
|
+
this._paused = true;
|
|
2793
|
+
this.log("Coordinator paused \u2014 buffering LLM decisions until user message is processed");
|
|
2794
|
+
this.broadcast({ type: "coordinator_paused", sessionId: "", timestamp: Date.now(), data: {} });
|
|
2795
|
+
this.pauseTimeout = setTimeout(() => {
|
|
2796
|
+
if (this._paused) {
|
|
2797
|
+
this.log("Coordinator auto-resuming after timeout");
|
|
2798
|
+
this.resume();
|
|
2799
|
+
}
|
|
2800
|
+
}, PAUSE_TIMEOUT_MS);
|
|
2801
|
+
}
|
|
2802
|
+
resume() {
|
|
2803
|
+
if (!this._paused)
|
|
2804
|
+
return;
|
|
2805
|
+
this._paused = false;
|
|
2806
|
+
if (this.pauseTimeout) {
|
|
2807
|
+
clearTimeout(this.pauseTimeout);
|
|
2808
|
+
this.pauseTimeout = null;
|
|
2809
|
+
}
|
|
2810
|
+
this.log(`Coordinator resumed \u2014 replaying ${this.pauseBuffer.length} buffered events`);
|
|
2811
|
+
this.broadcast({ type: "coordinator_resumed", sessionId: "", timestamp: Date.now(), data: {} });
|
|
2812
|
+
const buffered = [...this.pauseBuffer];
|
|
2813
|
+
this.pauseBuffer = [];
|
|
2814
|
+
for (const entry of buffered) {
|
|
2815
|
+
this.handleSessionEvent(entry.sessionId, entry.event, entry.data).catch((err) => {
|
|
2816
|
+
this.log(`Error replaying buffered event: ${err}`);
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2437
2820
|
registerTask(sessionId, context) {
|
|
2438
2821
|
this.tasks.set(sessionId, {
|
|
2439
2822
|
sessionId,
|
|
@@ -2560,6 +2943,20 @@ class SwarmCoordinator {
|
|
|
2560
2943
|
}
|
|
2561
2944
|
taskCtx.lastActivityAt = Date.now();
|
|
2562
2945
|
taskCtx.idleCheckCount = 0;
|
|
2946
|
+
if (this._paused && (event === "blocked" || event === "task_complete")) {
|
|
2947
|
+
const eventData = data;
|
|
2948
|
+
if (!(event === "blocked" && eventData.autoResponded)) {
|
|
2949
|
+
this.broadcast({
|
|
2950
|
+
type: event === "blocked" ? "blocked_buffered" : "turn_complete_buffered",
|
|
2951
|
+
sessionId,
|
|
2952
|
+
timestamp: Date.now(),
|
|
2953
|
+
data
|
|
2954
|
+
});
|
|
2955
|
+
this.pauseBuffer.push({ sessionId, event, data });
|
|
2956
|
+
this.log(`Buffered "${event}" for ${taskCtx.label} (coordinator paused)`);
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2563
2960
|
switch (event) {
|
|
2564
2961
|
case "blocked":
|
|
2565
2962
|
await handleBlocked(this, sessionId, taskCtx, data);
|
|
@@ -2584,6 +2981,7 @@ class SwarmCoordinator {
|
|
|
2584
2981
|
});
|
|
2585
2982
|
const errorMsg = data.message ?? "unknown error";
|
|
2586
2983
|
this.sendChatMessage(`"${taskCtx.label}" hit an error: ${errorMsg}`, "coding-agent");
|
|
2984
|
+
checkAllTasksComplete(this);
|
|
2587
2985
|
break;
|
|
2588
2986
|
}
|
|
2589
2987
|
case "stopped":
|
|
@@ -2595,6 +2993,7 @@ class SwarmCoordinator {
|
|
|
2595
2993
|
timestamp: Date.now(),
|
|
2596
2994
|
data
|
|
2597
2995
|
});
|
|
2996
|
+
checkAllTasksComplete(this);
|
|
2598
2997
|
break;
|
|
2599
2998
|
case "ready":
|
|
2600
2999
|
this.broadcast({
|
|
@@ -2615,6 +3014,10 @@ class SwarmCoordinator {
|
|
|
2615
3014
|
});
|
|
2616
3015
|
const toolData = data;
|
|
2617
3016
|
const now = Date.now();
|
|
3017
|
+
const STARTUP_GRACE_MS = 1e4;
|
|
3018
|
+
if (now - taskCtx.registeredAt < STARTUP_GRACE_MS) {
|
|
3019
|
+
break;
|
|
3020
|
+
}
|
|
2618
3021
|
const lastNotif = this.lastToolNotification.get(sessionId) ?? 0;
|
|
2619
3022
|
if (now - lastNotif > 30000) {
|
|
2620
3023
|
this.lastToolNotification.set(sessionId, now);
|
|
@@ -2650,6 +3053,9 @@ class SwarmCoordinator {
|
|
|
2650
3053
|
async executeDecision(sessionId, decision) {
|
|
2651
3054
|
return executeDecision(this, sessionId, decision);
|
|
2652
3055
|
}
|
|
3056
|
+
async executeEventDecision(sessionId, decision) {
|
|
3057
|
+
return executeDecision(this, sessionId, decision);
|
|
3058
|
+
}
|
|
2653
3059
|
setSupervisionLevel(level) {
|
|
2654
3060
|
this.supervisionLevel = level;
|
|
2655
3061
|
this.broadcast({
|
|
@@ -2794,7 +3200,21 @@ class PTYService {
|
|
|
2794
3200
|
metricsTracker: this.metricsTracker,
|
|
2795
3201
|
traceEntries: this.traceEntries,
|
|
2796
3202
|
maxTraceEntries: PTYService.MAX_TRACE_ENTRIES,
|
|
2797
|
-
log: (msg) => this.log(msg)
|
|
3203
|
+
log: (msg) => this.log(msg),
|
|
3204
|
+
hasActiveTask: (sessionId) => {
|
|
3205
|
+
const coordinator = this.coordinator;
|
|
3206
|
+
if (!coordinator)
|
|
3207
|
+
return false;
|
|
3208
|
+
const taskCtx = coordinator.getTaskContext(sessionId);
|
|
3209
|
+
return taskCtx?.status === "active";
|
|
3210
|
+
},
|
|
3211
|
+
hasTaskActivity: (sessionId) => {
|
|
3212
|
+
const coordinator = this.coordinator;
|
|
3213
|
+
if (!coordinator)
|
|
3214
|
+
return false;
|
|
3215
|
+
const taskCtx = coordinator.getTaskContext(sessionId);
|
|
3216
|
+
return (taskCtx?.decisions.length ?? 0) > 0;
|
|
3217
|
+
}
|
|
2798
3218
|
});
|
|
2799
3219
|
this.manager = result.manager;
|
|
2800
3220
|
this.usingBunWorker = result.usingBunWorker;
|
|
@@ -2962,10 +3382,10 @@ class PTYService {
|
|
|
2962
3382
|
throw new Error("PTYService not initialized");
|
|
2963
3383
|
return sendKeysToSession(this.ioContext(), sessionId, keys);
|
|
2964
3384
|
}
|
|
2965
|
-
async stopSession(sessionId) {
|
|
3385
|
+
async stopSession(sessionId, force = false) {
|
|
2966
3386
|
if (!this.manager)
|
|
2967
3387
|
throw new Error("PTYService not initialized");
|
|
2968
|
-
return stopSession(this.ioContext(), sessionId, this.sessionMetadata, this.sessionWorkdirs, (msg) => this.log(msg));
|
|
3388
|
+
return stopSession(this.ioContext(), sessionId, this.sessionMetadata, this.sessionWorkdirs, (msg) => this.log(msg), force);
|
|
2969
3389
|
}
|
|
2970
3390
|
get defaultApprovalPreset() {
|
|
2971
3391
|
const fromEnv = this.runtime.getSetting("PARALLAX_DEFAULT_APPROVAL_PRESET");
|
|
@@ -3435,7 +3855,7 @@ function registerSessionEvents(ptyService, runtime, sessionId, label, scratchDir
|
|
|
3435
3855
|
text: preview ? `Agent "${label}" completed the task.\n\n${preview}` : `Agent "${label}" completed the task.`
|
|
3436
3856
|
});
|
|
3437
3857
|
}
|
|
3438
|
-
ptyService.stopSession(sessionId).catch((err) => {
|
|
3858
|
+
ptyService.stopSession(sessionId, true).catch((err) => {
|
|
3439
3859
|
logger4.warn(`[START_CODING_TASK] Failed to stop session for "${label}" after task complete: ${err}`);
|
|
3440
3860
|
});
|
|
3441
3861
|
}
|
|
@@ -5248,7 +5668,8 @@ async function handleCoordinatorRoutes(req, res, pathname, ctx) {
|
|
|
5248
5668
|
return true;
|
|
5249
5669
|
}
|
|
5250
5670
|
if (method === "GET" && subPath === "/status") {
|
|
5251
|
-
const
|
|
5671
|
+
const allTasks = coordinator.getAllTaskContexts();
|
|
5672
|
+
const tasks = allTasks.filter((t) => t.status !== "stopped" && t.status !== "completed" && t.status !== "error");
|
|
5252
5673
|
sendJson(res, {
|
|
5253
5674
|
supervisionLevel: coordinator.getSupervisionLevel(),
|
|
5254
5675
|
taskCount: tasks.length,
|
|
@@ -5652,10 +6073,12 @@ export {
|
|
|
5652
6073
|
src_default as default,
|
|
5653
6074
|
createCodingAgentRouteHandler,
|
|
5654
6075
|
codingAgentPlugin,
|
|
6076
|
+
buildTurnCompleteEventMessage,
|
|
6077
|
+
buildBlockedEventMessage,
|
|
5655
6078
|
SwarmCoordinator,
|
|
5656
6079
|
PTYService,
|
|
5657
6080
|
CodingWorkspaceService
|
|
5658
6081
|
};
|
|
5659
6082
|
|
|
5660
|
-
//# debugId=
|
|
6083
|
+
//# debugId=BA5A6C5F800D868764756E2164756E21
|
|
5661
6084
|
//# sourceMappingURL=index.js.map
|