@elizaos/plugin-agent-orchestrator 0.3.18 → 0.4.0
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 +2 -8
- package/dist/actions/coding-task-handlers.d.ts.map +1 -1
- package/dist/actions/spawn-agent.d.ts.map +1 -1
- package/dist/actions/start-coding-task.d.ts.map +1 -1
- package/dist/index.js +372 -304
- package/dist/index.js.map +12 -11
- package/dist/services/ansi-utils.d.ts.map +1 -1
- package/dist/services/session-event-queue.d.ts +25 -0
- package/dist/services/session-event-queue.d.ts.map +1 -0
- package/dist/services/swarm-coordinator-prompts.d.ts.map +1 -1
- package/dist/services/swarm-coordinator.d.ts +24 -0
- 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-history.d.ts +27 -0
- package/dist/services/swarm-history.d.ts.map +1 -0
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -34,8 +34,14 @@ function cleanForChat(raw) {
|
|
|
34
34
|
return false;
|
|
35
35
|
if (STATUS_LINE.test(trimmed))
|
|
36
36
|
return false;
|
|
37
|
+
if (TOOL_MARKER_LINE.test(trimmed))
|
|
38
|
+
return false;
|
|
39
|
+
if (GIT_NOISE_LINE.test(trimmed))
|
|
40
|
+
return false;
|
|
37
41
|
if (!/[a-zA-Z0-9]/.test(trimmed))
|
|
38
42
|
return false;
|
|
43
|
+
if (trimmed.length <= 3)
|
|
44
|
+
return false;
|
|
39
45
|
return true;
|
|
40
46
|
}).map((line) => line.replace(/ {2,}/g, " ").trim()).filter((line) => line.length > 0).join(`
|
|
41
47
|
`).replace(/\n{3,}/g, `
|
|
@@ -83,7 +89,7 @@ function captureTaskResponse(sessionId, buffers, markers) {
|
|
|
83
89
|
return cleanForChat(responseLines.join(`
|
|
84
90
|
`));
|
|
85
91
|
}
|
|
86
|
-
var CURSOR_MOVEMENT, CURSOR_POSITION, ERASE, OSC, ALL_ANSI, CONTROL_CHARS, ORPHAN_SGR, LONG_SPACES, TUI_DECORATIVE, LOADING_LINE, STATUS_LINE;
|
|
92
|
+
var CURSOR_MOVEMENT, CURSOR_POSITION, ERASE, OSC, ALL_ANSI, CONTROL_CHARS, ORPHAN_SGR, LONG_SPACES, TUI_DECORATIVE, LOADING_LINE, STATUS_LINE, TOOL_MARKER_LINE, GIT_NOISE_LINE;
|
|
87
93
|
var init_ansi_utils = __esm(() => {
|
|
88
94
|
CURSOR_MOVEMENT = /\x1b\[\d*[CDABGdEF]/g;
|
|
89
95
|
CURSOR_POSITION = /\x1b\[\d*(?:;\d+)?[Hf]/g;
|
|
@@ -94,8 +100,10 @@ var init_ansi_utils = __esm(() => {
|
|
|
94
100
|
ORPHAN_SGR = /\[[\d;]*m/g;
|
|
95
101
|
LONG_SPACES = / {3,}/g;
|
|
96
102
|
TUI_DECORATIVE = /[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❮❯▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✽✻✶✳✢⏺←→↑↓⬆⬇◆▪▫■□▲△▼▽◈⟨⟩⌘⏎⏏⌫⌦⇧⇪⌥·⎿✔◼]/g;
|
|
97
|
-
LOADING_LINE = /^\s*(?:thinking|
|
|
103
|
+
LOADING_LINE = /^\s*(?:[A-Z][a-z]+(?:-[a-z]+)?(?:ing|ed)\w*|thinking|Loading|processing)(?:…|\.{3})(?:\s*\(.*\)|\s+for\s+\d+[smh](?:\s+\d+[smh])*)?\s*$|^\s*(?:[A-Z][a-z]+(?:-[a-z]+)?(?:ing|ed)\w*|thinking|Loading|processing)\s+for\s+\d+[smh](?:\s+\d+[smh])*\s*$/;
|
|
98
104
|
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;
|
|
105
|
+
TOOL_MARKER_LINE = /^\s*(?:Bash|Write|Read|Edit|Glob|Grep|Search|TodoWrite|Agent)\s*\(.*\)\s*$/;
|
|
106
|
+
GIT_NOISE_LINE = /^\s*(?:On branch\s+\w|Your branch is|modified:|new file:|deleted:|renamed:|Untracked files:|Changes (?:not staged|to be committed)|\d+\s+files?\s+changed.*(?:insertion|deletion))/i;
|
|
99
107
|
});
|
|
100
108
|
|
|
101
109
|
// src/services/trajectory-context.ts
|
|
@@ -191,8 +199,7 @@ ${recentOutput.slice(-3000)}
|
|
|
191
199
|
` + `- For Y/n confirmations that align with the original task, respond "y".
|
|
192
200
|
` + `- For design questions or choices that could go either way, escalate.
|
|
193
201
|
` + `- For error recovery prompts, try to respond if the path forward is clear.
|
|
194
|
-
` + `- If the output shows a PR was just created (e.g. "Created pull request #N"),
|
|
195
|
-
` + `- Only use "complete" if the agent confirmed it verified ALL test plan items after creating the PR.
|
|
202
|
+
` + `- If the output shows a PR was just created (e.g. "Created pull request #N"), use "complete" — the task is done.
|
|
196
203
|
` + `- 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.
|
|
197
204
|
` + `- When in doubt, escalate — it's better to ask the human than to make a wrong choice.
|
|
198
205
|
` + `- 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.
|
|
@@ -260,35 +267,24 @@ Output from this turn:
|
|
|
260
267
|
${turnOutput.slice(-3000)}
|
|
261
268
|
---
|
|
262
269
|
|
|
263
|
-
` + `The agent completed a turn. Decide if the
|
|
264
|
-
|
|
265
|
-
` + `IMPORTANT: Coding agents work in multiple turns. A single turn completing does NOT mean ` + `the task is done. You must verify that EVERY objective in the original task has been addressed ` + `in the output before declaring "complete".
|
|
266
|
-
|
|
267
|
-
` + `Your options:
|
|
268
|
-
|
|
269
|
-
` + `1. "respond" — The agent finished a step but the overall task is NOT done yet. ` + `Send a follow-up instruction to continue. Set "response" to the next instruction ` + `(e.g. "Now run the tests", "Create a PR with these changes", "Continue with the next part"). ` + `THIS IS THE DEFAULT — most turns are intermediate steps, not the final result.
|
|
270
|
-
|
|
271
|
-
` + `2. "complete" — The original task objectives have ALL been fully met. For repo-based tasks, ` + `this means code was written, changes were committed, pushed, AND a pull request was created. ` + `Only use this when you can point to specific evidence in the output for EVERY objective ` + `(e.g. "Created pull request #N" in the output).
|
|
272
|
-
|
|
273
|
-
` + `3. "escalate" — Something looks wrong or you're unsure whether the task is complete. ` + `Let the human decide.
|
|
270
|
+
` + `The agent completed a turn. Decide if the task is done or needs more work.
|
|
274
271
|
|
|
275
|
-
` + `
|
|
276
|
-
|
|
277
|
-
` + `
|
|
278
|
-
` +
|
|
279
|
-
` +
|
|
280
|
-
` +
|
|
281
|
-
` +
|
|
282
|
-
|
|
283
|
-
` +
|
|
284
|
-
` + `-
|
|
285
|
-
` + `-
|
|
286
|
-
` + `-
|
|
287
|
-
` + `-
|
|
288
|
-
` + `-
|
|
289
|
-
|
|
290
|
-
` +
|
|
291
|
-
` + `- Look for explicit "DECISION:" markers in the agent's output — these are the agent deliberately ` + `surfacing design choices. Always capture these as keyDecision.
|
|
272
|
+
` + `Options:
|
|
273
|
+
` + `1. "complete" — The task objectives have been met.
|
|
274
|
+
` + ` - For repo tasks: ONLY when a PR creation signal appears ("Created pull request #N"). ` + `A generic "done" or "finished" statement is NOT sufficient for repo tasks — a PR must exist.
|
|
275
|
+
` + ` - For scratch/research tasks (no repo): when the agent has produced its deliverable.
|
|
276
|
+
` + `2. "respond" — The agent needs to do more work.
|
|
277
|
+
` + `3. "escalate" — Something is wrong. Let the human decide.
|
|
278
|
+
` + `4. "ignore" — The agent is still working (e.g., spinner text like "Germinating...", "Frosting..."). ` + `Wait for the next turn.
|
|
279
|
+
|
|
280
|
+
` + `CRITICAL RULES:
|
|
281
|
+
` + `- For repo tasks: use "complete" ONLY when "Created pull request #N" appears in output.
|
|
282
|
+
` + `- For scratch/research tasks: use "complete" when the agent delivers its output.
|
|
283
|
+
` + `- Do NOT ask the agent to review, verify, or re-check work it already completed.
|
|
284
|
+
` + `- If output is only spinner text, use "ignore" and wait for the next turn.
|
|
285
|
+
` + `- Use "respond" when the agent hasn't started, or when code was written but not yet committed/pushed/PR'd.
|
|
286
|
+
|
|
287
|
+
` + `If the agent's output reveals a significant decision, include "keyDecision" with a brief summary.
|
|
292
288
|
|
|
293
289
|
` + `Respond with ONLY a JSON object:
|
|
294
290
|
` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "...", "keyDecision": "..."}`;
|
|
@@ -322,7 +318,7 @@ ${recentOutput.slice(-3000)}
|
|
|
322
318
|
` + `- For tool approvals / Y/n that align with the task, respond "y" or keys:["enter"].
|
|
323
319
|
` + `- If the prompt asks for info NOT in the original task, escalate.
|
|
324
320
|
` + `- Decline access to paths outside ${taskCtx.workdir}.
|
|
325
|
-
` + `- If a PR was just created,
|
|
321
|
+
` + `- If a PR was just created, the task is done — use "complete".
|
|
326
322
|
` + `- When in doubt, escalate.
|
|
327
323
|
|
|
328
324
|
` + `If the agent's output reveals a significant decision that sibling agents should know about, include "keyDecision" with a brief summary.
|
|
@@ -353,16 +349,14 @@ ${turnOutput.slice(-3000)}
|
|
|
353
349
|
|
|
354
350
|
` + `Options:
|
|
355
351
|
` + `- "respond" — send a follow-up instruction (DEFAULT for intermediate steps)
|
|
356
|
-
` + `- "complete" —
|
|
352
|
+
` + `- "complete" — For repo tasks: ONLY when "Created pull request #N" appears. ` + `For scratch/research tasks: when the agent delivers its output.
|
|
357
353
|
` + `- "escalate" — something looks wrong, ask the user
|
|
358
|
-
` + `- "ignore" —
|
|
354
|
+
` + `- "ignore" — spinner/loading output, agent still working
|
|
359
355
|
|
|
360
356
|
` + `Guidelines:
|
|
361
|
-
` + `-
|
|
357
|
+
` + `- For repo tasks, a generic "done" is NOT enough — require a PR creation signal.
|
|
362
358
|
` + `- If code was written but not committed/pushed/PR'd, respond with next step.
|
|
363
|
-
` + `-
|
|
364
|
-
` + `- When asking agents to verify work, prefer CLI tools (gh, curl, cat, etc.) over browser automation.
|
|
365
|
-
` + `- Default to "respond" — only "complete" when certain ALL work is done.
|
|
359
|
+
` + `- Do NOT ask the agent to re-verify work it already completed.
|
|
366
360
|
` + `- If the agent's output reveals a significant creative or architectural decision, include "keyDecision" with a brief summary.
|
|
367
361
|
` + `- Look for explicit "DECISION:" markers in the agent's output — always capture these as keyDecision.
|
|
368
362
|
|
|
@@ -822,6 +816,18 @@ async function executeDecision(ctx, sessionId, decision) {
|
|
|
822
816
|
const taskCtx = ctx.tasks.get(sessionId);
|
|
823
817
|
if (taskCtx) {
|
|
824
818
|
taskCtx.status = "completed";
|
|
819
|
+
ctx.history?.append({
|
|
820
|
+
timestamp: Date.now(),
|
|
821
|
+
type: "task_completed",
|
|
822
|
+
sessionId,
|
|
823
|
+
label: taskCtx.label,
|
|
824
|
+
agentType: taskCtx.agentType,
|
|
825
|
+
repo: taskCtx.repo,
|
|
826
|
+
workdir: taskCtx.workdir,
|
|
827
|
+
completionSummary: decision.reasoning
|
|
828
|
+
}).catch((err) => {
|
|
829
|
+
ctx.log(`Failed to persist task completion for "${taskCtx.label}" (${sessionId}): ${err}`);
|
|
830
|
+
});
|
|
825
831
|
}
|
|
826
832
|
ctx.broadcast({
|
|
827
833
|
type: "task_complete",
|
|
@@ -1022,6 +1028,31 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
|
|
|
1022
1028
|
const raw = await fetchRecentOutput(ctx, sessionId);
|
|
1023
1029
|
turnOutput = cleanForChat(raw);
|
|
1024
1030
|
}
|
|
1031
|
+
const PR_CREATED_RE = /(?:Created|Opened)\s+pull\s+request\s+#?\d+|gh\s+pr\s+create/i;
|
|
1032
|
+
if (PR_CREATED_RE.test(turnOutput)) {
|
|
1033
|
+
const fastDecision = {
|
|
1034
|
+
action: "complete",
|
|
1035
|
+
reasoning: "PR detected in turn output — task complete."
|
|
1036
|
+
};
|
|
1037
|
+
ctx.log(`Turn assessment for "${taskCtx.label}": complete (fast-path: PR detected in output)`);
|
|
1038
|
+
taskCtx.decisions.push({
|
|
1039
|
+
timestamp: Date.now(),
|
|
1040
|
+
event: "turn_complete",
|
|
1041
|
+
promptText: "Agent finished a turn",
|
|
1042
|
+
decision: "complete",
|
|
1043
|
+
response: "",
|
|
1044
|
+
reasoning: fastDecision.reasoning
|
|
1045
|
+
});
|
|
1046
|
+
recordKeyDecision(ctx, taskCtx.label, fastDecision);
|
|
1047
|
+
ctx.broadcast({
|
|
1048
|
+
type: "turn_assessment",
|
|
1049
|
+
sessionId,
|
|
1050
|
+
timestamp: Date.now(),
|
|
1051
|
+
data: { action: "complete", reasoning: fastDecision.reasoning }
|
|
1052
|
+
});
|
|
1053
|
+
await executeDecision(ctx, sessionId, fastDecision);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1025
1056
|
let decision = null;
|
|
1026
1057
|
const decisionFromPipeline = false;
|
|
1027
1058
|
const prompt = buildTurnCompletePrompt(toContextSummary(taskCtx), turnOutput, toDecisionHistory(taskCtx), collectSiblings(ctx, sessionId), ctx.sharedDecisions, ctx.getSwarmContext());
|
|
@@ -2283,15 +2314,15 @@ var sendToAgentAction = {
|
|
|
2283
2314
|
};
|
|
2284
2315
|
|
|
2285
2316
|
// src/actions/spawn-agent.ts
|
|
2286
|
-
import * as
|
|
2287
|
-
import * as
|
|
2317
|
+
import * as os2 from "node:os";
|
|
2318
|
+
import * as path3 from "node:path";
|
|
2288
2319
|
import {
|
|
2289
2320
|
logger as logger4
|
|
2290
2321
|
} from "@elizaos/core";
|
|
2291
2322
|
|
|
2292
2323
|
// src/services/pty-service.ts
|
|
2293
|
-
import { appendFile, mkdir, readFile as
|
|
2294
|
-
import { dirname, join as
|
|
2324
|
+
import { appendFile as appendFile2, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
2325
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
2295
2326
|
import { logger as logger3 } from "@elizaos/core";
|
|
2296
2327
|
import {
|
|
2297
2328
|
checkAdapters,
|
|
@@ -3103,9 +3134,9 @@ Classification states:
|
|
|
3103
3134
|
- For Y/n confirmations that align with the original task, respond "y".
|
|
3104
3135
|
- For TUI menus, use "keys:enter" for default or "keys:down,enter" for non-default.
|
|
3105
3136
|
- If the prompt asks for information NOT in the original task, set suggestedResponse to null (this will escalate to the human).
|
|
3106
|
-
|
|
3137
|
+
` + `- If a PR was just created, the task is likely done — classify as "task_complete".
|
|
3107
3138
|
|
|
3108
|
-
Respond with ONLY a JSON object:
|
|
3139
|
+
` + `Respond with ONLY a JSON object:
|
|
3109
3140
|
{"state": "...", "prompt": "...", "suggestedResponse": "..."}`;
|
|
3110
3141
|
}
|
|
3111
3142
|
async function classifyAndDecideForCoordinator(ctx) {
|
|
@@ -3202,9 +3233,124 @@ init_swarm_decision_loop();
|
|
|
3202
3233
|
|
|
3203
3234
|
// src/services/swarm-coordinator.ts
|
|
3204
3235
|
init_ansi_utils();
|
|
3205
|
-
init_swarm_decision_loop();
|
|
3206
3236
|
import { logger } from "@elizaos/core";
|
|
3207
3237
|
|
|
3238
|
+
// src/services/swarm-history.ts
|
|
3239
|
+
import * as fs from "fs/promises";
|
|
3240
|
+
import * as os from "os";
|
|
3241
|
+
import * as path2 from "path";
|
|
3242
|
+
var MAX_ENTRIES = 150;
|
|
3243
|
+
var TRUNCATE_TO = 100;
|
|
3244
|
+
|
|
3245
|
+
class WriteMutex {
|
|
3246
|
+
queue = [];
|
|
3247
|
+
locked = false;
|
|
3248
|
+
async acquire() {
|
|
3249
|
+
if (!this.locked) {
|
|
3250
|
+
this.locked = true;
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
return new Promise((resolve2) => {
|
|
3254
|
+
this.queue.push(resolve2);
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
release() {
|
|
3258
|
+
const next = this.queue.shift();
|
|
3259
|
+
if (next) {
|
|
3260
|
+
next();
|
|
3261
|
+
} else {
|
|
3262
|
+
this.locked = false;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
class SwarmHistory {
|
|
3268
|
+
filePath;
|
|
3269
|
+
appendCount = 0;
|
|
3270
|
+
mutex = new WriteMutex;
|
|
3271
|
+
constructor(stateDir) {
|
|
3272
|
+
const dir = stateDir || process.env.MILADY_STATE_DIR || process.env.ELIZA_STATE_DIR || path2.join(os.homedir(), ".milady");
|
|
3273
|
+
this.filePath = path2.join(dir, "swarm-history.jsonl");
|
|
3274
|
+
}
|
|
3275
|
+
async append(entry) {
|
|
3276
|
+
await this.mutex.acquire();
|
|
3277
|
+
try {
|
|
3278
|
+
const dir = path2.dirname(this.filePath);
|
|
3279
|
+
await fs.mkdir(dir, { recursive: true });
|
|
3280
|
+
await fs.appendFile(this.filePath, `${JSON.stringify(entry)}
|
|
3281
|
+
`, "utf-8");
|
|
3282
|
+
this.appendCount++;
|
|
3283
|
+
if (this.appendCount >= MAX_ENTRIES - TRUNCATE_TO) {
|
|
3284
|
+
const content = await fs.readFile(this.filePath, "utf-8");
|
|
3285
|
+
const lineCount = content.split(`
|
|
3286
|
+
`).filter((l) => l.trim() !== "").length;
|
|
3287
|
+
if (lineCount > MAX_ENTRIES) {
|
|
3288
|
+
await this.truncateInner(TRUNCATE_TO);
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
} catch (err) {
|
|
3292
|
+
console.error("[swarm-history] append failed:", err);
|
|
3293
|
+
throw err;
|
|
3294
|
+
} finally {
|
|
3295
|
+
this.mutex.release();
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
async readAll() {
|
|
3299
|
+
try {
|
|
3300
|
+
const content = await fs.readFile(this.filePath, "utf-8");
|
|
3301
|
+
const entries = [];
|
|
3302
|
+
const lines = content.split(`
|
|
3303
|
+
`);
|
|
3304
|
+
for (let i = 0;i < lines.length; i++) {
|
|
3305
|
+
if (lines[i].trim() === "")
|
|
3306
|
+
continue;
|
|
3307
|
+
try {
|
|
3308
|
+
entries.push(JSON.parse(lines[i]));
|
|
3309
|
+
} catch {
|
|
3310
|
+
console.warn(`[swarm-history] skipping corrupted line at index ${i} (length=${lines[i].length})`);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
return entries;
|
|
3314
|
+
} catch (err) {
|
|
3315
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
3316
|
+
return [];
|
|
3317
|
+
}
|
|
3318
|
+
console.error("[swarm-history] readAll failed:", err);
|
|
3319
|
+
return [];
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
async getLastUsedRepo() {
|
|
3323
|
+
const entries = await this.readAll();
|
|
3324
|
+
for (let i = entries.length - 1;i >= 0; i--) {
|
|
3325
|
+
if (entries[i].repo) {
|
|
3326
|
+
return entries[i].repo;
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
async truncateInner(maxEntries) {
|
|
3332
|
+
const entries = await this.readAll();
|
|
3333
|
+
if (entries.length === 0) {
|
|
3334
|
+
try {
|
|
3335
|
+
await fs.stat(this.filePath);
|
|
3336
|
+
console.error("[swarm-history] truncate aborted: file exists but readAll returned empty");
|
|
3337
|
+
return;
|
|
3338
|
+
} catch {
|
|
3339
|
+
return;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
const kept = entries.slice(-maxEntries);
|
|
3343
|
+
const content = kept.map((e) => JSON.stringify(e)).join(`
|
|
3344
|
+
`) + `
|
|
3345
|
+
`;
|
|
3346
|
+
await fs.writeFile(this.filePath, content, "utf-8");
|
|
3347
|
+
this.appendCount = 0;
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
// src/services/swarm-coordinator.ts
|
|
3352
|
+
init_swarm_decision_loop();
|
|
3353
|
+
|
|
3208
3354
|
// src/services/swarm-idle-watchdog.ts
|
|
3209
3355
|
init_ansi_utils();
|
|
3210
3356
|
init_swarm_decision_loop();
|
|
@@ -3397,7 +3543,9 @@ async function handleIdleCheck(ctx, taskCtx, idleMinutes) {
|
|
|
3397
3543
|
}
|
|
3398
3544
|
|
|
3399
3545
|
// src/services/swarm-coordinator.ts
|
|
3400
|
-
var
|
|
3546
|
+
var UNREGISTERED_RETRY_DELAYS = [2000, 4000, 8000, 16000];
|
|
3547
|
+
var UNREGISTERED_MAX_TOTAL_MS = 30000;
|
|
3548
|
+
var TURN_COMPLETE_COALESCE_MS = 500;
|
|
3401
3549
|
var IDLE_SCAN_INTERVAL_MS = 60 * 1000;
|
|
3402
3550
|
var PAUSE_TIMEOUT_MS = 30000;
|
|
3403
3551
|
var MAX_PRE_BRIDGE_BUFFER = 100;
|
|
@@ -3431,6 +3579,10 @@ class SwarmCoordinator {
|
|
|
3431
3579
|
pauseBuffer = [];
|
|
3432
3580
|
preBridgeBroadcastBuffer = [];
|
|
3433
3581
|
pauseTimeout = null;
|
|
3582
|
+
startedAt = Date.now();
|
|
3583
|
+
unregisteredRetryTimers = new Map;
|
|
3584
|
+
turnCompleteCoalesceTimers = new Map;
|
|
3585
|
+
history = new SwarmHistory;
|
|
3434
3586
|
constructor(runtime) {
|
|
3435
3587
|
this.runtime = runtime;
|
|
3436
3588
|
}
|
|
@@ -3515,6 +3667,14 @@ class SwarmCoordinator {
|
|
|
3515
3667
|
this.lastBlockedPromptFingerprint.clear();
|
|
3516
3668
|
this.pendingBlocked.clear();
|
|
3517
3669
|
this.unregisteredBuffer.clear();
|
|
3670
|
+
for (const timer of this.unregisteredRetryTimers.values()) {
|
|
3671
|
+
clearTimeout(timer);
|
|
3672
|
+
}
|
|
3673
|
+
this.unregisteredRetryTimers.clear();
|
|
3674
|
+
for (const timer of this.turnCompleteCoalesceTimers.values()) {
|
|
3675
|
+
clearTimeout(timer);
|
|
3676
|
+
}
|
|
3677
|
+
this.turnCompleteCoalesceTimers.clear();
|
|
3518
3678
|
this.lastSeenOutput.clear();
|
|
3519
3679
|
this.lastToolNotification.clear();
|
|
3520
3680
|
this.agentDecisionCb = null;
|
|
@@ -3601,6 +3761,19 @@ class SwarmCoordinator {
|
|
|
3601
3761
|
taskDelivered: false,
|
|
3602
3762
|
lastSeenDecisionIndex: 0
|
|
3603
3763
|
});
|
|
3764
|
+
if (context.repo) {
|
|
3765
|
+
this._lastUsedRepo = context.repo;
|
|
3766
|
+
}
|
|
3767
|
+
this.history.append({
|
|
3768
|
+
timestamp: Date.now(),
|
|
3769
|
+
type: "task_registered",
|
|
3770
|
+
sessionId,
|
|
3771
|
+
label: context.label,
|
|
3772
|
+
agentType: context.agentType,
|
|
3773
|
+
repo: context.repo,
|
|
3774
|
+
workdir: context.workdir,
|
|
3775
|
+
originalTask: context.originalTask
|
|
3776
|
+
}).catch(() => {});
|
|
3604
3777
|
this.broadcast({
|
|
3605
3778
|
type: "task_registered",
|
|
3606
3779
|
sessionId,
|
|
@@ -3611,6 +3784,11 @@ class SwarmCoordinator {
|
|
|
3611
3784
|
originalTask: context.originalTask
|
|
3612
3785
|
}
|
|
3613
3786
|
});
|
|
3787
|
+
const retryTimer = this.unregisteredRetryTimers.get(sessionId);
|
|
3788
|
+
if (retryTimer) {
|
|
3789
|
+
clearTimeout(retryTimer);
|
|
3790
|
+
this.unregisteredRetryTimers.delete(sessionId);
|
|
3791
|
+
}
|
|
3614
3792
|
const buffered = this.unregisteredBuffer.get(sessionId);
|
|
3615
3793
|
if (buffered) {
|
|
3616
3794
|
this.unregisteredBuffer.delete(sessionId);
|
|
@@ -3621,6 +3799,7 @@ class SwarmCoordinator {
|
|
|
3621
3799
|
}
|
|
3622
3800
|
}
|
|
3623
3801
|
}
|
|
3802
|
+
_lastUsedRepo;
|
|
3624
3803
|
getLastUsedRepo() {
|
|
3625
3804
|
let latest;
|
|
3626
3805
|
for (const task of this.tasks.values()) {
|
|
@@ -3628,7 +3807,17 @@ class SwarmCoordinator {
|
|
|
3628
3807
|
latest = task;
|
|
3629
3808
|
}
|
|
3630
3809
|
}
|
|
3631
|
-
return latest?.repo;
|
|
3810
|
+
return latest?.repo ?? this._lastUsedRepo;
|
|
3811
|
+
}
|
|
3812
|
+
async getLastUsedRepoAsync() {
|
|
3813
|
+
const memoryRepo = this.getLastUsedRepo();
|
|
3814
|
+
if (memoryRepo)
|
|
3815
|
+
return memoryRepo;
|
|
3816
|
+
try {
|
|
3817
|
+
return await this.history.getLastUsedRepo();
|
|
3818
|
+
} catch {
|
|
3819
|
+
return;
|
|
3820
|
+
}
|
|
3632
3821
|
}
|
|
3633
3822
|
getTaskContext(sessionId) {
|
|
3634
3823
|
return this.tasks.get(sessionId);
|
|
@@ -3636,6 +3825,33 @@ class SwarmCoordinator {
|
|
|
3636
3825
|
getAllTaskContexts() {
|
|
3637
3826
|
return Array.from(this.tasks.values());
|
|
3638
3827
|
}
|
|
3828
|
+
scheduleUnregisteredRetry(sessionId, attempt) {
|
|
3829
|
+
const delay = UNREGISTERED_RETRY_DELAYS[Math.min(attempt, UNREGISTERED_RETRY_DELAYS.length - 1)];
|
|
3830
|
+
const timer = setTimeout(() => {
|
|
3831
|
+
this.unregisteredRetryTimers.delete(sessionId);
|
|
3832
|
+
const stillBuffered = this.unregisteredBuffer.get(sessionId);
|
|
3833
|
+
if (!stillBuffered || stillBuffered.length === 0)
|
|
3834
|
+
return;
|
|
3835
|
+
const ctx = this.tasks.get(sessionId);
|
|
3836
|
+
if (ctx) {
|
|
3837
|
+
this.unregisteredBuffer.delete(sessionId);
|
|
3838
|
+
for (const entry of stillBuffered) {
|
|
3839
|
+
this.handleSessionEvent(sessionId, entry.event, entry.data).catch(() => {});
|
|
3840
|
+
}
|
|
3841
|
+
return;
|
|
3842
|
+
}
|
|
3843
|
+
const oldest = stillBuffered[0].receivedAt;
|
|
3844
|
+
const totalElapsed = Date.now() - oldest;
|
|
3845
|
+
if (totalElapsed >= UNREGISTERED_MAX_TOTAL_MS) {
|
|
3846
|
+
this.unregisteredBuffer.delete(sessionId);
|
|
3847
|
+
this.log(`Discarding ${stillBuffered.length} buffered events for unregistered session ${sessionId} after ${Math.round(totalElapsed / 1000)}s`);
|
|
3848
|
+
return;
|
|
3849
|
+
}
|
|
3850
|
+
this.log(`Retry ${attempt + 1} for unregistered session ${sessionId} (next in ${delay}ms)`);
|
|
3851
|
+
this.scheduleUnregisteredRetry(sessionId, attempt + 1);
|
|
3852
|
+
}, delay);
|
|
3853
|
+
this.unregisteredRetryTimers.set(sessionId, timer);
|
|
3854
|
+
}
|
|
3639
3855
|
addSseClient(res) {
|
|
3640
3856
|
this.sseClients.add(res);
|
|
3641
3857
|
const snapshot = {
|
|
@@ -3681,6 +3897,13 @@ class SwarmCoordinator {
|
|
|
3681
3897
|
} catch {}
|
|
3682
3898
|
}
|
|
3683
3899
|
async handleSessionEvent(sessionId, event, data) {
|
|
3900
|
+
const tsMatch = sessionId.match(/^pty-(\d+)-/);
|
|
3901
|
+
if (tsMatch) {
|
|
3902
|
+
const sessionCreatedAt = Number(tsMatch[1]);
|
|
3903
|
+
if (sessionCreatedAt < this.startedAt - 60000) {
|
|
3904
|
+
return;
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3684
3907
|
const taskCtx = this.tasks.get(sessionId);
|
|
3685
3908
|
if (!taskCtx) {
|
|
3686
3909
|
if (event === "blocked" || event === "task_complete" || event === "error") {
|
|
@@ -3690,21 +3913,9 @@ class SwarmCoordinator {
|
|
|
3690
3913
|
this.unregisteredBuffer.set(sessionId, buffer);
|
|
3691
3914
|
}
|
|
3692
3915
|
buffer.push({ event, data, receivedAt: Date.now() });
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
const ctx = this.tasks.get(sessionId);
|
|
3697
|
-
if (ctx) {
|
|
3698
|
-
this.unregisteredBuffer.delete(sessionId);
|
|
3699
|
-
for (const entry of stillBuffered) {
|
|
3700
|
-
this.handleSessionEvent(sessionId, entry.event, entry.data).catch(() => {});
|
|
3701
|
-
}
|
|
3702
|
-
} else {
|
|
3703
|
-
this.unregisteredBuffer.delete(sessionId);
|
|
3704
|
-
this.log(`Discarding ${stillBuffered.length} buffered events for unregistered session ${sessionId}`);
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
3707
|
-
}, UNREGISTERED_BUFFER_MS);
|
|
3916
|
+
if (!this.unregisteredRetryTimers.has(sessionId)) {
|
|
3917
|
+
this.scheduleUnregisteredRetry(sessionId, 0);
|
|
3918
|
+
}
|
|
3708
3919
|
}
|
|
3709
3920
|
return;
|
|
3710
3921
|
}
|
|
@@ -3754,7 +3965,20 @@ class SwarmCoordinator {
|
|
|
3754
3965
|
timestamp: Date.now(),
|
|
3755
3966
|
data
|
|
3756
3967
|
});
|
|
3757
|
-
|
|
3968
|
+
const existingCoalesce = this.turnCompleteCoalesceTimers.get(sessionId);
|
|
3969
|
+
if (existingCoalesce)
|
|
3970
|
+
clearTimeout(existingCoalesce);
|
|
3971
|
+
const coalescedData = data;
|
|
3972
|
+
const coalesceTimer = setTimeout(() => {
|
|
3973
|
+
this.turnCompleteCoalesceTimers.delete(sessionId);
|
|
3974
|
+
const currentTask = this.tasks.get(sessionId);
|
|
3975
|
+
if (currentTask && currentTask.status === "active") {
|
|
3976
|
+
handleTurnComplete(this, sessionId, currentTask, coalescedData).catch((err) => {
|
|
3977
|
+
this.log(`Coalesced turn-complete failed: ${err}`);
|
|
3978
|
+
});
|
|
3979
|
+
}
|
|
3980
|
+
}, TURN_COMPLETE_COALESCE_MS);
|
|
3981
|
+
this.turnCompleteCoalesceTimers.set(sessionId, coalesceTimer);
|
|
3758
3982
|
break;
|
|
3759
3983
|
}
|
|
3760
3984
|
case "error": {
|
|
@@ -4169,10 +4393,10 @@ class PTYService {
|
|
|
4169
4393
|
const hookUrl = `http://localhost:${this.runtime.getSetting("SERVER_PORT") ?? "2138"}/api/coding-agents/hooks`;
|
|
4170
4394
|
if (resolvedAgentType === "claude") {
|
|
4171
4395
|
try {
|
|
4172
|
-
const settingsPath =
|
|
4396
|
+
const settingsPath = join3(workdir, ".claude", "settings.json");
|
|
4173
4397
|
let settings = {};
|
|
4174
4398
|
try {
|
|
4175
|
-
settings = JSON.parse(await
|
|
4399
|
+
settings = JSON.parse(await readFile3(settingsPath, "utf-8"));
|
|
4176
4400
|
} catch {}
|
|
4177
4401
|
const permissions = settings.permissions ?? {};
|
|
4178
4402
|
permissions.allowedDirectories = [workdir];
|
|
@@ -4187,8 +4411,8 @@ class PTYService {
|
|
|
4187
4411
|
settings.hooks = { ...existingHooks, ...hookProtocol.settingsHooks };
|
|
4188
4412
|
this.log(`Injecting HTTP hooks for session ${sessionId}`);
|
|
4189
4413
|
}
|
|
4190
|
-
await
|
|
4191
|
-
await
|
|
4414
|
+
await mkdir2(dirname2(settingsPath), { recursive: true });
|
|
4415
|
+
await writeFile3(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
4192
4416
|
this.log(`Wrote allowedDirectories [${workdir}] to ${settingsPath}`);
|
|
4193
4417
|
} catch (err) {
|
|
4194
4418
|
this.log(`Failed to write Claude settings: ${err}`);
|
|
@@ -4196,10 +4420,10 @@ class PTYService {
|
|
|
4196
4420
|
}
|
|
4197
4421
|
if (resolvedAgentType === "gemini") {
|
|
4198
4422
|
try {
|
|
4199
|
-
const settingsPath =
|
|
4423
|
+
const settingsPath = join3(workdir, ".gemini", "settings.json");
|
|
4200
4424
|
let settings = {};
|
|
4201
4425
|
try {
|
|
4202
|
-
settings = JSON.parse(await
|
|
4426
|
+
settings = JSON.parse(await readFile3(settingsPath, "utf-8"));
|
|
4203
4427
|
} catch {}
|
|
4204
4428
|
const adapter = this.getAdapter("gemini");
|
|
4205
4429
|
const hookProtocol = adapter.getHookTelemetryProtocol({
|
|
@@ -4211,8 +4435,8 @@ class PTYService {
|
|
|
4211
4435
|
settings.hooks = { ...existingHooks, ...hookProtocol.settingsHooks };
|
|
4212
4436
|
this.log(`Injecting Gemini CLI hooks for session ${sessionId}`);
|
|
4213
4437
|
}
|
|
4214
|
-
await
|
|
4215
|
-
await
|
|
4438
|
+
await mkdir2(dirname2(settingsPath), { recursive: true });
|
|
4439
|
+
await writeFile3(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
4216
4440
|
} catch (err) {
|
|
4217
4441
|
this.log(`Failed to write Gemini settings: ${err}`);
|
|
4218
4442
|
}
|
|
@@ -4504,7 +4728,7 @@ class PTYService {
|
|
|
4504
4728
|
static GITIGNORE_MARKER = "# orchestrator-injected (do not commit agent config/memory files)";
|
|
4505
4729
|
static gitignoreLocks = new Map;
|
|
4506
4730
|
async ensureOrchestratorGitignore(workdir) {
|
|
4507
|
-
const gitignorePath =
|
|
4731
|
+
const gitignorePath = join3(workdir, ".gitignore");
|
|
4508
4732
|
const existing_lock = PTYService.gitignoreLocks.get(gitignorePath);
|
|
4509
4733
|
if (existing_lock)
|
|
4510
4734
|
await existing_lock;
|
|
@@ -4521,7 +4745,7 @@ class PTYService {
|
|
|
4521
4745
|
async doEnsureGitignore(gitignorePath, workdir) {
|
|
4522
4746
|
let existing = "";
|
|
4523
4747
|
try {
|
|
4524
|
-
existing = await
|
|
4748
|
+
existing = await readFile3(gitignorePath, "utf-8");
|
|
4525
4749
|
} catch {}
|
|
4526
4750
|
if (existing.includes(PTYService.GITIGNORE_MARKER))
|
|
4527
4751
|
return;
|
|
@@ -4536,14 +4760,14 @@ class PTYService {
|
|
|
4536
4760
|
];
|
|
4537
4761
|
try {
|
|
4538
4762
|
if (existing.length === 0) {
|
|
4539
|
-
await
|
|
4763
|
+
await writeFile3(gitignorePath, entries.join(`
|
|
4540
4764
|
`) + `
|
|
4541
4765
|
`, "utf-8");
|
|
4542
4766
|
} else {
|
|
4543
4767
|
const separator = existing.endsWith(`
|
|
4544
4768
|
`) ? "" : `
|
|
4545
4769
|
`;
|
|
4546
|
-
await
|
|
4770
|
+
await appendFile2(gitignorePath, separator + entries.join(`
|
|
4547
4771
|
`) + `
|
|
4548
4772
|
`, "utf-8");
|
|
4549
4773
|
}
|
|
@@ -4687,13 +4911,13 @@ var spawnAgentAction = {
|
|
|
4687
4911
|
}
|
|
4688
4912
|
return { success: false, error: "NO_WORKSPACE" };
|
|
4689
4913
|
}
|
|
4690
|
-
const resolvedWorkdir =
|
|
4691
|
-
const workspaceBaseDir =
|
|
4914
|
+
const resolvedWorkdir = path3.resolve(workdir);
|
|
4915
|
+
const workspaceBaseDir = path3.join(os2.homedir(), ".milady", "workspaces");
|
|
4692
4916
|
const allowedPrefixes = [
|
|
4693
|
-
|
|
4694
|
-
|
|
4917
|
+
path3.resolve(workspaceBaseDir),
|
|
4918
|
+
path3.resolve(process.cwd())
|
|
4695
4919
|
];
|
|
4696
|
-
const isAllowed = allowedPrefixes.some((prefix) => resolvedWorkdir.startsWith(prefix +
|
|
4920
|
+
const isAllowed = allowedPrefixes.some((prefix) => resolvedWorkdir.startsWith(prefix + path3.sep) || resolvedWorkdir === prefix);
|
|
4697
4921
|
if (!isAllowed) {
|
|
4698
4922
|
if (callback) {
|
|
4699
4923
|
await callback({
|
|
@@ -5045,17 +5269,17 @@ function formatAge(timestamp) {
|
|
|
5045
5269
|
|
|
5046
5270
|
// src/actions/coding-task-helpers.ts
|
|
5047
5271
|
import { randomUUID } from "node:crypto";
|
|
5048
|
-
import * as
|
|
5049
|
-
import * as
|
|
5050
|
-
import * as
|
|
5272
|
+
import * as fs2 from "node:fs";
|
|
5273
|
+
import * as os3 from "node:os";
|
|
5274
|
+
import * as path4 from "node:path";
|
|
5051
5275
|
import {
|
|
5052
5276
|
logger as logger5
|
|
5053
5277
|
} from "@elizaos/core";
|
|
5054
5278
|
function createScratchDir() {
|
|
5055
|
-
const baseDir =
|
|
5279
|
+
const baseDir = path4.join(os3.homedir(), ".milady", "workspaces");
|
|
5056
5280
|
const scratchId = randomUUID();
|
|
5057
|
-
const scratchDir =
|
|
5058
|
-
|
|
5281
|
+
const scratchDir = path4.join(baseDir, scratchId);
|
|
5282
|
+
fs2.mkdirSync(scratchDir, { recursive: true });
|
|
5059
5283
|
return scratchDir;
|
|
5060
5284
|
}
|
|
5061
5285
|
function generateLabel(repo, task) {
|
|
@@ -5190,7 +5414,8 @@ ${taskList}
|
|
|
5190
5414
|
try {
|
|
5191
5415
|
const result = await withTrajectoryContext(runtime, { source: "orchestrator", decisionType: "swarm-context-generation" }, () => runtime.useModel(ModelType5.TEXT_SMALL, {
|
|
5192
5416
|
prompt,
|
|
5193
|
-
temperature: 0.3
|
|
5417
|
+
temperature: 0.3,
|
|
5418
|
+
stream: false
|
|
5194
5419
|
}));
|
|
5195
5420
|
return result?.trim() || "";
|
|
5196
5421
|
} catch (err) {
|
|
@@ -5217,12 +5442,7 @@ async function handleMultiAgent(ctx, agentsParam) {
|
|
|
5217
5442
|
} = ctx;
|
|
5218
5443
|
const agentSpecs = agentsParam.split("|").map((s) => s.trim()).filter(Boolean);
|
|
5219
5444
|
if (agentSpecs.length === 0) {
|
|
5220
|
-
|
|
5221
|
-
await callback({
|
|
5222
|
-
text: "No agent tasks provided in agents parameter."
|
|
5223
|
-
});
|
|
5224
|
-
}
|
|
5225
|
-
return { success: false, error: "EMPTY_AGENTS_PARAM" };
|
|
5445
|
+
agentSpecs.push("");
|
|
5226
5446
|
}
|
|
5227
5447
|
if (agentSpecs.length > MAX_CONCURRENT_AGENTS) {
|
|
5228
5448
|
if (callback) {
|
|
@@ -5397,169 +5617,6 @@ ${swarmContext}
|
|
|
5397
5617
|
data: { agents: results }
|
|
5398
5618
|
};
|
|
5399
5619
|
}
|
|
5400
|
-
async function handleSingleAgent(ctx, task) {
|
|
5401
|
-
logger6.debug(`[START_CODING_TASK] handleSingleAgent called, agentType=${ctx.defaultAgentType}, task=${task ? "yes" : "none"}, repo=${ctx.repo ?? "none"}`);
|
|
5402
|
-
const {
|
|
5403
|
-
runtime,
|
|
5404
|
-
ptyService,
|
|
5405
|
-
wsService,
|
|
5406
|
-
credentials,
|
|
5407
|
-
customCredentials,
|
|
5408
|
-
callback,
|
|
5409
|
-
message,
|
|
5410
|
-
state,
|
|
5411
|
-
repo,
|
|
5412
|
-
defaultAgentType: agentType,
|
|
5413
|
-
rawAgentType,
|
|
5414
|
-
memoryContent,
|
|
5415
|
-
approvalPreset,
|
|
5416
|
-
explicitLabel
|
|
5417
|
-
} = ctx;
|
|
5418
|
-
const label = explicitLabel || generateLabel(repo, task);
|
|
5419
|
-
let workdir;
|
|
5420
|
-
let workspaceId;
|
|
5421
|
-
let branch;
|
|
5422
|
-
if (repo) {
|
|
5423
|
-
if (!wsService) {
|
|
5424
|
-
if (callback) {
|
|
5425
|
-
await callback({
|
|
5426
|
-
text: "Workspace Service is not available. Cannot clone repository."
|
|
5427
|
-
});
|
|
5428
|
-
}
|
|
5429
|
-
return { success: false, error: "WORKSPACE_SERVICE_UNAVAILABLE" };
|
|
5430
|
-
}
|
|
5431
|
-
try {
|
|
5432
|
-
if (callback) {
|
|
5433
|
-
await callback({ text: `Cloning ${repo}...` });
|
|
5434
|
-
}
|
|
5435
|
-
const workspace = await wsService.provisionWorkspace({ repo });
|
|
5436
|
-
workdir = workspace.path;
|
|
5437
|
-
workspaceId = workspace.id;
|
|
5438
|
-
branch = workspace.branch;
|
|
5439
|
-
wsService.setLabel(workspace.id, label);
|
|
5440
|
-
if (state) {
|
|
5441
|
-
state.codingWorkspace = {
|
|
5442
|
-
id: workspace.id,
|
|
5443
|
-
path: workspace.path,
|
|
5444
|
-
branch: workspace.branch,
|
|
5445
|
-
isWorktree: workspace.isWorktree,
|
|
5446
|
-
label
|
|
5447
|
-
};
|
|
5448
|
-
}
|
|
5449
|
-
} catch (error) {
|
|
5450
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5451
|
-
if (callback) {
|
|
5452
|
-
await callback({
|
|
5453
|
-
text: `Failed to clone repository: ${errorMessage}`
|
|
5454
|
-
});
|
|
5455
|
-
}
|
|
5456
|
-
return { success: false, error: errorMessage };
|
|
5457
|
-
}
|
|
5458
|
-
} else {
|
|
5459
|
-
workdir = createScratchDir();
|
|
5460
|
-
}
|
|
5461
|
-
logger6.debug(`[START_CODING_TASK] Spawning ${agentType} agent, task: ${task ? `"${task.slice(0, 80)}..."` : "(none)"}, workdir: ${workdir}`);
|
|
5462
|
-
try {
|
|
5463
|
-
if (agentType !== "shell" && agentType !== "pi") {
|
|
5464
|
-
const [preflight] = await ptyService.checkAvailableAgents([
|
|
5465
|
-
agentType
|
|
5466
|
-
]);
|
|
5467
|
-
if (preflight && !preflight.installed) {
|
|
5468
|
-
logger6.warn(`[START_CODING_TASK] ${preflight.adapter} CLI not installed`);
|
|
5469
|
-
if (callback) {
|
|
5470
|
-
await callback({
|
|
5471
|
-
text: `${preflight.adapter} CLI is not installed.
|
|
5472
|
-
Install with: ${preflight.installCommand}
|
|
5473
|
-
Docs: ${preflight.docsUrl}`
|
|
5474
|
-
});
|
|
5475
|
-
}
|
|
5476
|
-
return { success: false, error: "AGENT_NOT_INSTALLED" };
|
|
5477
|
-
}
|
|
5478
|
-
logger6.debug(`[START_CODING_TASK] Preflight OK: ${preflight?.adapter} installed`);
|
|
5479
|
-
}
|
|
5480
|
-
const piRequested = isPiAgentType(rawAgentType);
|
|
5481
|
-
const initialTask = piRequested ? toPiCommand(task) : task;
|
|
5482
|
-
const displayType = piRequested ? "pi" : agentType;
|
|
5483
|
-
const pastExperience = await queryPastExperience(runtime, {
|
|
5484
|
-
taskDescription: task,
|
|
5485
|
-
lookbackHours: 48,
|
|
5486
|
-
maxEntries: 6,
|
|
5487
|
-
repo
|
|
5488
|
-
});
|
|
5489
|
-
const pastExperienceBlock = formatPastExperience(pastExperience);
|
|
5490
|
-
const agentMemory = [memoryContent, pastExperienceBlock].filter(Boolean).join(`
|
|
5491
|
-
|
|
5492
|
-
`) || undefined;
|
|
5493
|
-
const coordinator = getCoordinator(runtime);
|
|
5494
|
-
logger6.debug(`[START_CODING_TASK] Calling spawnSession (${agentType}, coordinator=${!!coordinator})`);
|
|
5495
|
-
const session = await ptyService.spawnSession({
|
|
5496
|
-
name: `coding-${Date.now()}`,
|
|
5497
|
-
agentType,
|
|
5498
|
-
workdir,
|
|
5499
|
-
initialTask,
|
|
5500
|
-
memoryContent: agentMemory,
|
|
5501
|
-
credentials,
|
|
5502
|
-
approvalPreset: approvalPreset ?? ptyService.defaultApprovalPreset,
|
|
5503
|
-
customCredentials,
|
|
5504
|
-
...coordinator ? { skipAdapterAutoResponse: true } : {},
|
|
5505
|
-
metadata: {
|
|
5506
|
-
requestedType: rawAgentType,
|
|
5507
|
-
messageId: message.id,
|
|
5508
|
-
userId: message.userId,
|
|
5509
|
-
workspaceId,
|
|
5510
|
-
label
|
|
5511
|
-
}
|
|
5512
|
-
});
|
|
5513
|
-
logger6.debug(`[START_CODING_TASK] Session spawned: ${session.id} (${session.status})`);
|
|
5514
|
-
const isScratchWorkspace = !repo;
|
|
5515
|
-
const scratchDir = isScratchWorkspace ? workdir : null;
|
|
5516
|
-
registerSessionEvents(ptyService, runtime, session.id, label, scratchDir, callback, !!coordinator);
|
|
5517
|
-
if (coordinator && task) {
|
|
5518
|
-
coordinator.registerTask(session.id, {
|
|
5519
|
-
agentType,
|
|
5520
|
-
label,
|
|
5521
|
-
originalTask: task,
|
|
5522
|
-
workdir,
|
|
5523
|
-
repo
|
|
5524
|
-
});
|
|
5525
|
-
}
|
|
5526
|
-
if (state) {
|
|
5527
|
-
state.codingSession = {
|
|
5528
|
-
id: session.id,
|
|
5529
|
-
agentType: session.agentType,
|
|
5530
|
-
workdir: session.workdir,
|
|
5531
|
-
status: session.status
|
|
5532
|
-
};
|
|
5533
|
-
}
|
|
5534
|
-
const summary = repo ? `Cloned ${repo} and started ${displayType} agent as "${label}"${task ? ` with task: "${task}"` : ""}` : `Started ${displayType} agent as "${label}" in scratch workspace${task ? ` with task: "${task}"` : ""}`;
|
|
5535
|
-
if (callback) {
|
|
5536
|
-
await callback({ text: `${summary}
|
|
5537
|
-
Session ID: ${session.id}` });
|
|
5538
|
-
}
|
|
5539
|
-
return {
|
|
5540
|
-
success: true,
|
|
5541
|
-
text: summary,
|
|
5542
|
-
data: {
|
|
5543
|
-
sessionId: session.id,
|
|
5544
|
-
agentType: displayType,
|
|
5545
|
-
workdir: session.workdir,
|
|
5546
|
-
workspaceId,
|
|
5547
|
-
branch,
|
|
5548
|
-
label,
|
|
5549
|
-
status: session.status
|
|
5550
|
-
}
|
|
5551
|
-
};
|
|
5552
|
-
} catch (error) {
|
|
5553
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5554
|
-
logger6.error("[START_CODING_TASK] Failed to spawn agent:", errorMessage);
|
|
5555
|
-
if (callback) {
|
|
5556
|
-
await callback({
|
|
5557
|
-
text: `Failed to start coding agent: ${errorMessage}`
|
|
5558
|
-
});
|
|
5559
|
-
}
|
|
5560
|
-
return { success: false, error: errorMessage };
|
|
5561
|
-
}
|
|
5562
|
-
}
|
|
5563
5620
|
|
|
5564
5621
|
// src/actions/start-coding-task.ts
|
|
5565
5622
|
var startCodingTaskAction = {
|
|
@@ -5633,13 +5690,23 @@ var startCodingTaskAction = {
|
|
|
5633
5690
|
repo = urlMatch[0];
|
|
5634
5691
|
}
|
|
5635
5692
|
}
|
|
5636
|
-
|
|
5693
|
+
const reuseRepo = params?.reuseRepo ?? content.reuseRepo ?? /\b(same\s+repo|same\s+project|continue|that\s+repo|the\s+repo|this\s+repo|in\s+the\s+repo)\b/i.test(content.text ?? "");
|
|
5694
|
+
if (!repo && reuseRepo) {
|
|
5637
5695
|
const coordinator = getCoordinator(runtime);
|
|
5638
|
-
const lastRepo = coordinator?.
|
|
5696
|
+
const lastRepo = await coordinator?.getLastUsedRepoAsync();
|
|
5639
5697
|
if (lastRepo) {
|
|
5640
5698
|
repo = lastRepo;
|
|
5641
5699
|
}
|
|
5642
5700
|
}
|
|
5701
|
+
if (!repo && reuseRepo) {
|
|
5702
|
+
const wsService2 = runtime.getService("CODING_WORKSPACE_SERVICE");
|
|
5703
|
+
if (wsService2 && typeof wsService2.listWorkspaces === "function") {
|
|
5704
|
+
const withRepo = wsService2.listWorkspaces().find((ws) => ws.repo);
|
|
5705
|
+
if (withRepo) {
|
|
5706
|
+
repo = withRepo.repo;
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5643
5710
|
const customCredentialKeys = runtime.getSetting("CUSTOM_CREDENTIAL_KEYS");
|
|
5644
5711
|
let customCredentials;
|
|
5645
5712
|
if (customCredentialKeys) {
|
|
@@ -5679,7 +5746,8 @@ var startCodingTaskAction = {
|
|
|
5679
5746
|
return handleMultiAgent(ctx, agentsParam);
|
|
5680
5747
|
}
|
|
5681
5748
|
const task = params?.task ?? content.task;
|
|
5682
|
-
|
|
5749
|
+
const singleAgentSpec = task || "";
|
|
5750
|
+
return handleMultiAgent(ctx, singleAgentSpec);
|
|
5683
5751
|
},
|
|
5684
5752
|
parameters: [
|
|
5685
5753
|
{
|
|
@@ -6192,9 +6260,9 @@ var activeWorkspaceContextProvider = {
|
|
|
6192
6260
|
};
|
|
6193
6261
|
|
|
6194
6262
|
// src/services/workspace-service.ts
|
|
6195
|
-
import * as
|
|
6196
|
-
import * as
|
|
6197
|
-
import * as
|
|
6263
|
+
import * as os4 from "node:os";
|
|
6264
|
+
import * as path6 from "node:path";
|
|
6265
|
+
import * as fs4 from "node:fs/promises";
|
|
6198
6266
|
import {
|
|
6199
6267
|
CredentialService,
|
|
6200
6268
|
GitHubPatClient as GitHubPatClient2,
|
|
@@ -6407,17 +6475,17 @@ async function createPR(workspaceService, workspace, workspaceId, options, log)
|
|
|
6407
6475
|
}
|
|
6408
6476
|
|
|
6409
6477
|
// src/services/workspace-lifecycle.ts
|
|
6410
|
-
import * as
|
|
6411
|
-
import * as
|
|
6478
|
+
import * as fs3 from "node:fs";
|
|
6479
|
+
import * as path5 from "node:path";
|
|
6412
6480
|
async function removeScratchDir(dirPath, baseDir, log) {
|
|
6413
|
-
const resolved =
|
|
6414
|
-
const resolvedBase =
|
|
6415
|
-
if (!resolved.startsWith(resolvedBase) && resolved !==
|
|
6481
|
+
const resolved = path5.resolve(dirPath);
|
|
6482
|
+
const resolvedBase = path5.resolve(baseDir) + path5.sep;
|
|
6483
|
+
if (!resolved.startsWith(resolvedBase) && resolved !== path5.resolve(baseDir)) {
|
|
6416
6484
|
console.warn(`[CodingWorkspaceService] Refusing to remove dir outside base: ${resolved}`);
|
|
6417
6485
|
return;
|
|
6418
6486
|
}
|
|
6419
6487
|
try {
|
|
6420
|
-
await
|
|
6488
|
+
await fs3.promises.rm(resolved, { recursive: true, force: true });
|
|
6421
6489
|
log(`Removed scratch dir ${resolved}`);
|
|
6422
6490
|
} catch (err) {
|
|
6423
6491
|
console.warn(`[CodingWorkspaceService] Failed to remove scratch dir ${resolved}:`, err);
|
|
@@ -6430,7 +6498,7 @@ async function gcOrphanedWorkspaces(baseDir, workspaceTtlMs, trackedWorkspaceIds
|
|
|
6430
6498
|
}
|
|
6431
6499
|
let entries;
|
|
6432
6500
|
try {
|
|
6433
|
-
entries = await
|
|
6501
|
+
entries = await fs3.promises.readdir(baseDir, { withFileTypes: true });
|
|
6434
6502
|
} catch {
|
|
6435
6503
|
return;
|
|
6436
6504
|
}
|
|
@@ -6444,12 +6512,12 @@ async function gcOrphanedWorkspaces(baseDir, workspaceTtlMs, trackedWorkspaceIds
|
|
|
6444
6512
|
skipped++;
|
|
6445
6513
|
continue;
|
|
6446
6514
|
}
|
|
6447
|
-
const dirPath =
|
|
6515
|
+
const dirPath = path5.join(baseDir, entry.name);
|
|
6448
6516
|
try {
|
|
6449
|
-
const
|
|
6450
|
-
const age = now -
|
|
6517
|
+
const stat2 = await fs3.promises.stat(dirPath);
|
|
6518
|
+
const age = now - stat2.mtimeMs;
|
|
6451
6519
|
if (age > workspaceTtlMs) {
|
|
6452
|
-
await
|
|
6520
|
+
await fs3.promises.rm(dirPath, { recursive: true, force: true });
|
|
6453
6521
|
removed++;
|
|
6454
6522
|
} else {
|
|
6455
6523
|
skipped++;
|
|
@@ -6483,7 +6551,7 @@ class CodingWorkspaceService {
|
|
|
6483
6551
|
constructor(runtime, config = {}) {
|
|
6484
6552
|
this.runtime = runtime;
|
|
6485
6553
|
this.serviceConfig = {
|
|
6486
|
-
baseDir: config.baseDir ??
|
|
6554
|
+
baseDir: config.baseDir ?? path6.join(os4.homedir(), ".milady", "workspaces"),
|
|
6487
6555
|
branchPrefix: config.branchPrefix ?? "milady",
|
|
6488
6556
|
debug: config.debug ?? false,
|
|
6489
6557
|
workspaceTtlMs: config.workspaceTtlMs ?? 24 * 60 * 60 * 1000
|
|
@@ -6802,14 +6870,14 @@ class CodingWorkspaceService {
|
|
|
6802
6870
|
const suggestedName = this.sanitizeWorkspaceName(name || record.label);
|
|
6803
6871
|
const targetPath = await this.allocatePromotedPath(baseDir, suggestedName);
|
|
6804
6872
|
try {
|
|
6805
|
-
await
|
|
6873
|
+
await fs4.rename(record.path, targetPath);
|
|
6806
6874
|
} catch (error) {
|
|
6807
6875
|
const isExdev = typeof error === "object" && error !== null && "code" in error && error.code === "EXDEV";
|
|
6808
6876
|
if (!isExdev)
|
|
6809
6877
|
throw error;
|
|
6810
|
-
await
|
|
6811
|
-
await
|
|
6812
|
-
await
|
|
6878
|
+
await fs4.cp(record.path, targetPath, { recursive: true });
|
|
6879
|
+
await fs4.access(targetPath);
|
|
6880
|
+
await fs4.rm(record.path, { recursive: true, force: true });
|
|
6813
6881
|
}
|
|
6814
6882
|
const next = {
|
|
6815
6883
|
...record,
|
|
@@ -6882,15 +6950,15 @@ class CodingWorkspaceService {
|
|
|
6882
6950
|
return compact || `scratch-${Date.now().toString(36)}`;
|
|
6883
6951
|
}
|
|
6884
6952
|
async allocatePromotedPath(baseDir, baseName) {
|
|
6885
|
-
const baseResolved =
|
|
6953
|
+
const baseResolved = path6.resolve(baseDir);
|
|
6886
6954
|
for (let i = 0;i < 1000; i++) {
|
|
6887
6955
|
const candidateName = i === 0 ? baseName : `${baseName}-${i}`;
|
|
6888
|
-
const candidate =
|
|
6889
|
-
if (candidate !== baseResolved && !candidate.startsWith(`${baseResolved}${
|
|
6956
|
+
const candidate = path6.resolve(baseResolved, candidateName);
|
|
6957
|
+
if (candidate !== baseResolved && !candidate.startsWith(`${baseResolved}${path6.sep}`)) {
|
|
6890
6958
|
continue;
|
|
6891
6959
|
}
|
|
6892
6960
|
try {
|
|
6893
|
-
await
|
|
6961
|
+
await fs4.access(candidate);
|
|
6894
6962
|
} catch {
|
|
6895
6963
|
return candidate;
|
|
6896
6964
|
}
|
|
@@ -6899,10 +6967,10 @@ class CodingWorkspaceService {
|
|
|
6899
6967
|
}
|
|
6900
6968
|
}
|
|
6901
6969
|
// src/api/agent-routes.ts
|
|
6902
|
-
import { access as access2, readFile as
|
|
6970
|
+
import { access as access2, readFile as readFile4, realpath, rm as rm2 } from "node:fs/promises";
|
|
6903
6971
|
import { createHash } from "node:crypto";
|
|
6904
|
-
import * as
|
|
6905
|
-
import * as
|
|
6972
|
+
import * as os5 from "node:os";
|
|
6973
|
+
import * as path7 from "node:path";
|
|
6906
6974
|
import { execFile } from "node:child_process";
|
|
6907
6975
|
import { promisify } from "node:util";
|
|
6908
6976
|
var execFileAsync = promisify(execFile);
|
|
@@ -6914,23 +6982,23 @@ function shouldAutoPreflight() {
|
|
|
6914
6982
|
return false;
|
|
6915
6983
|
}
|
|
6916
6984
|
function isPathInside(parent, candidate) {
|
|
6917
|
-
return candidate === parent || candidate.startsWith(`${parent}${
|
|
6985
|
+
return candidate === parent || candidate.startsWith(`${parent}${path7.sep}`);
|
|
6918
6986
|
}
|
|
6919
6987
|
async function resolveSafeVenvPath(workdir, venvDirRaw) {
|
|
6920
6988
|
const venvDir = venvDirRaw.trim();
|
|
6921
6989
|
if (!venvDir) {
|
|
6922
6990
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be non-empty");
|
|
6923
6991
|
}
|
|
6924
|
-
if (
|
|
6992
|
+
if (path7.isAbsolute(venvDir)) {
|
|
6925
6993
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be relative to workdir");
|
|
6926
6994
|
}
|
|
6927
|
-
const normalized =
|
|
6928
|
-
if (normalized === "." || normalized === ".." || normalized.startsWith(`..${
|
|
6995
|
+
const normalized = path7.normalize(venvDir);
|
|
6996
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith(`..${path7.sep}`)) {
|
|
6929
6997
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must stay within workdir");
|
|
6930
6998
|
}
|
|
6931
|
-
const workdirResolved =
|
|
6999
|
+
const workdirResolved = path7.resolve(workdir);
|
|
6932
7000
|
const workdirReal = await realpath(workdirResolved);
|
|
6933
|
-
const resolved =
|
|
7001
|
+
const resolved = path7.resolve(workdirReal, normalized);
|
|
6934
7002
|
if (!isPathInside(workdirReal, resolved)) {
|
|
6935
7003
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV resolves outside workdir");
|
|
6936
7004
|
}
|
|
@@ -6946,7 +7014,7 @@ async function resolveSafeVenvPath(workdir, venvDirRaw) {
|
|
|
6946
7014
|
const maybeErr = err;
|
|
6947
7015
|
if (maybeErr?.code !== "ENOENT")
|
|
6948
7016
|
throw err;
|
|
6949
|
-
const parentReal = await realpath(
|
|
7017
|
+
const parentReal = await realpath(path7.dirname(resolved));
|
|
6950
7018
|
if (!isPathInside(workdirReal, parentReal)) {
|
|
6951
7019
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV parent resolves outside workdir");
|
|
6952
7020
|
}
|
|
@@ -6962,10 +7030,10 @@ async function fileExists(filePath) {
|
|
|
6962
7030
|
}
|
|
6963
7031
|
}
|
|
6964
7032
|
async function resolveRequirementsPath(workdir) {
|
|
6965
|
-
const workdirReal = await realpath(
|
|
7033
|
+
const workdirReal = await realpath(path7.resolve(workdir));
|
|
6966
7034
|
const candidates = [
|
|
6967
|
-
|
|
6968
|
-
|
|
7035
|
+
path7.join(workdir, "apps", "api", "requirements.txt"),
|
|
7036
|
+
path7.join(workdir, "requirements.txt")
|
|
6969
7037
|
];
|
|
6970
7038
|
for (const candidate of candidates) {
|
|
6971
7039
|
if (!await fileExists(candidate))
|
|
@@ -6979,7 +7047,7 @@ async function resolveRequirementsPath(workdir) {
|
|
|
6979
7047
|
return null;
|
|
6980
7048
|
}
|
|
6981
7049
|
async function fingerprintRequirementsFile(requirementsPath) {
|
|
6982
|
-
const file = await
|
|
7050
|
+
const file = await readFile4(requirementsPath);
|
|
6983
7051
|
return createHash("sha256").update(file).digest("hex");
|
|
6984
7052
|
}
|
|
6985
7053
|
async function runBenchmarkPreflight(workdir) {
|
|
@@ -6992,7 +7060,7 @@ async function runBenchmarkPreflight(workdir) {
|
|
|
6992
7060
|
const mode = process.env.PARALLAX_BENCHMARK_PREFLIGHT_MODE?.toLowerCase() === "warm" ? "warm" : "cold";
|
|
6993
7061
|
const venvDir = process.env.PARALLAX_BENCHMARK_PREFLIGHT_VENV || ".benchmark-venv";
|
|
6994
7062
|
const venvPath = await resolveSafeVenvPath(workdir, venvDir);
|
|
6995
|
-
const pythonInVenv =
|
|
7063
|
+
const pythonInVenv = path7.join(venvPath, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
|
|
6996
7064
|
const key = `${workdir}::${mode}::${venvPath}::${requirementsFingerprint}`;
|
|
6997
7065
|
if (PREFLIGHT_DONE.has(key)) {
|
|
6998
7066
|
if (await fileExists(pythonInVenv))
|
|
@@ -7200,21 +7268,21 @@ async function handleAgentRoutes(req, res, pathname, ctx) {
|
|
|
7200
7268
|
customCredentials,
|
|
7201
7269
|
metadata
|
|
7202
7270
|
} = body;
|
|
7203
|
-
const workspaceBaseDir =
|
|
7204
|
-
const workspaceBaseDirResolved =
|
|
7205
|
-
const cwdResolved =
|
|
7271
|
+
const workspaceBaseDir = path7.join(os5.homedir(), ".milady", "workspaces");
|
|
7272
|
+
const workspaceBaseDirResolved = path7.resolve(workspaceBaseDir);
|
|
7273
|
+
const cwdResolved = path7.resolve(process.cwd());
|
|
7206
7274
|
const workspaceBaseDirReal = await realpath(workspaceBaseDirResolved).catch(() => workspaceBaseDirResolved);
|
|
7207
7275
|
const cwdReal = await realpath(cwdResolved).catch(() => cwdResolved);
|
|
7208
7276
|
const allowedPrefixes = [workspaceBaseDirReal, cwdReal];
|
|
7209
7277
|
let workdir = rawWorkdir;
|
|
7210
7278
|
if (workdir) {
|
|
7211
|
-
const resolved =
|
|
7279
|
+
const resolved = path7.resolve(workdir);
|
|
7212
7280
|
const resolvedReal = await realpath(resolved).catch(() => null);
|
|
7213
7281
|
if (!resolvedReal) {
|
|
7214
7282
|
sendError(res, "workdir must exist", 403);
|
|
7215
7283
|
return true;
|
|
7216
7284
|
}
|
|
7217
|
-
const isAllowed = allowedPrefixes.some((prefix2) => resolvedReal === prefix2 || resolvedReal.startsWith(prefix2 +
|
|
7285
|
+
const isAllowed = allowedPrefixes.some((prefix2) => resolvedReal === prefix2 || resolvedReal.startsWith(prefix2 + path7.sep));
|
|
7218
7286
|
if (!isAllowed) {
|
|
7219
7287
|
sendError(res, "workdir must be within workspace base directory or cwd", 403);
|
|
7220
7288
|
return true;
|
|
@@ -7960,5 +8028,5 @@ export {
|
|
|
7960
8028
|
CodingWorkspaceService
|
|
7961
8029
|
};
|
|
7962
8030
|
|
|
7963
|
-
//# debugId=
|
|
8031
|
+
//# debugId=5999C4AE1407A27464756E2164756E21
|
|
7964
8032
|
//# sourceMappingURL=index.js.map
|