@elizaos/plugin-agent-orchestrator 0.6.1 → 0.6.2-alpha.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 +1 -0
- package/dist/actions/coding-task-handlers.d.ts.map +1 -1
- package/dist/actions/finalize-workspace.d.ts.map +1 -1
- package/dist/actions/list-agents.d.ts.map +1 -1
- package/dist/actions/manage-issues.d.ts.map +1 -1
- package/dist/actions/provision-workspace.d.ts.map +1 -1
- package/dist/actions/send-to-agent.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/actions/stop-agent.d.ts.map +1 -1
- package/dist/api/agent-routes.d.ts.map +1 -1
- package/dist/index.js +2076 -480
- package/dist/index.js.map +30 -28
- package/dist/services/agent-credentials.d.ts +2 -0
- package/dist/services/agent-credentials.d.ts.map +1 -1
- package/dist/services/agent-metrics.d.ts +3 -1
- package/dist/services/agent-metrics.d.ts.map +1 -1
- package/dist/services/ansi-utils.d.ts +13 -0
- package/dist/services/ansi-utils.d.ts.map +1 -1
- package/dist/services/coordinator-event-normalizer.d.ts +47 -0
- package/dist/services/coordinator-event-normalizer.d.ts.map +1 -0
- package/dist/services/pty-init.d.ts +2 -1
- package/dist/services/pty-init.d.ts.map +1 -1
- package/dist/services/pty-service.d.ts +19 -6
- package/dist/services/pty-service.d.ts.map +1 -1
- package/dist/services/pty-spawn.d.ts.map +1 -1
- package/dist/services/stall-classifier.d.ts +2 -0
- package/dist/services/stall-classifier.d.ts.map +1 -1
- package/dist/services/swarm-coordinator.d.ts +5 -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/task-agent-frameworks.d.ts +31 -1
- package/dist/services/task-agent-frameworks.d.ts.map +1 -1
- package/dist/services/task-policy.d.ts.map +1 -1
- package/dist/services/task-verifier-runner.d.ts +5 -0
- package/dist/services/task-verifier-runner.d.ts.map +1 -0
- package/dist/services/trajectory-context.d.ts +5 -1
- package/dist/services/trajectory-context.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -23,6 +23,9 @@ function applyAnsiStrip(input) {
|
|
|
23
23
|
function stripAnsi(raw) {
|
|
24
24
|
return applyAnsiStrip(raw);
|
|
25
25
|
}
|
|
26
|
+
function isSessionBootstrapNoiseLine(line) {
|
|
27
|
+
return SESSION_BOOTSTRAP_NOISE_PATTERNS.some((pattern) => pattern.test(line));
|
|
28
|
+
}
|
|
26
29
|
function cleanForChat(raw) {
|
|
27
30
|
const stripped = applyAnsiStrip(raw);
|
|
28
31
|
return stripped.replace(TUI_DECORATIVE, " ").replace(/\xa0/g, " ").split(`
|
|
@@ -38,6 +41,8 @@ function cleanForChat(raw) {
|
|
|
38
41
|
return false;
|
|
39
42
|
if (GIT_NOISE_LINE.test(trimmed))
|
|
40
43
|
return false;
|
|
44
|
+
if (isSessionBootstrapNoiseLine(trimmed))
|
|
45
|
+
return false;
|
|
41
46
|
if (!/[a-zA-Z0-9]/.test(trimmed))
|
|
42
47
|
return false;
|
|
43
48
|
if (trimmed.length <= 3)
|
|
@@ -46,6 +51,23 @@ function cleanForChat(raw) {
|
|
|
46
51
|
}).map((line) => line.replace(/ {2,}/g, " ").trim()).filter((line) => line.length > 0).join(`
|
|
47
52
|
`).replace(/\n{3,}/g, `
|
|
48
53
|
|
|
54
|
+
`).trim();
|
|
55
|
+
}
|
|
56
|
+
function isWorkdirEchoLine(line, workdir) {
|
|
57
|
+
if (!workdir)
|
|
58
|
+
return false;
|
|
59
|
+
const normalizedWorkdir = workdir.trim();
|
|
60
|
+
if (!normalizedWorkdir)
|
|
61
|
+
return false;
|
|
62
|
+
if (line === normalizedWorkdir || line === `/private${normalizedWorkdir}`) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
const basename = normalizedWorkdir.split("/").filter(Boolean).at(-1);
|
|
66
|
+
return Boolean(basename && line.includes(basename) && (/^\/(?:private\/)?/.test(line) || /^\/…\//.test(line)));
|
|
67
|
+
}
|
|
68
|
+
function cleanForFailoverContext(raw, workdir) {
|
|
69
|
+
return cleanForChat(raw).split(`
|
|
70
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => !FAILOVER_CONTEXT_NOISE_PATTERNS.some((pattern) => pattern.test(line))).filter((line) => !isWorkdirEchoLine(line, workdir)).join(`
|
|
49
71
|
`).trim();
|
|
50
72
|
}
|
|
51
73
|
function extractCompletionSummary(raw) {
|
|
@@ -89,7 +111,15 @@ function captureTaskResponse(sessionId, buffers, markers) {
|
|
|
89
111
|
return cleanForChat(responseLines.join(`
|
|
90
112
|
`));
|
|
91
113
|
}
|
|
92
|
-
|
|
114
|
+
function peekTaskResponse(sessionId, buffers, markers) {
|
|
115
|
+
const buffer = buffers.get(sessionId);
|
|
116
|
+
const marker = markers.get(sessionId);
|
|
117
|
+
if (!buffer || marker === undefined)
|
|
118
|
+
return "";
|
|
119
|
+
return cleanForChat(buffer.slice(marker).join(`
|
|
120
|
+
`));
|
|
121
|
+
}
|
|
122
|
+
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, SESSION_BOOTSTRAP_NOISE_PATTERNS, FAILOVER_CONTEXT_NOISE_PATTERNS;
|
|
93
123
|
var init_ansi_utils = __esm(() => {
|
|
94
124
|
CURSOR_MOVEMENT = /\x1b\[\d*[CDABGdEF]/g;
|
|
95
125
|
CURSOR_POSITION = /\x1b\[\d*(?:;\d+)?[Hf]/g;
|
|
@@ -99,11 +129,44 @@ var init_ansi_utils = __esm(() => {
|
|
|
99
129
|
CONTROL_CHARS = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
100
130
|
ORPHAN_SGR = /\[[\d;]*m/g;
|
|
101
131
|
LONG_SPACES = / {3,}/g;
|
|
102
|
-
TUI_DECORATIVE = /[
|
|
132
|
+
TUI_DECORATIVE = /[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❮❯▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✽✻✶✳✢⏺←→↑↓⬆⬇◆▪▫■□▲△▼▽◈⟨⟩⌘⏎⏏⌫⌦⇧⇪⌥·⎿✔◼█▌▐▖▗▘▝▛▜▟▙◐◑◒◓⏵]/g;
|
|
103
133
|
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*$/;
|
|
104
134
|
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
135
|
TOOL_MARKER_LINE = /^\s*(?:Bash|Write|Read|Edit|Glob|Grep|Search|TodoWrite|Agent)\s*\(.*\)\s*$/;
|
|
106
136
|
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;
|
|
137
|
+
SESSION_BOOTSTRAP_NOISE_PATTERNS = [
|
|
138
|
+
/^OpenAI Codex\b/i,
|
|
139
|
+
/^model:\s/i,
|
|
140
|
+
/^directory:\s/i,
|
|
141
|
+
/^Tip:\s+New Try the Codex App\b/i,
|
|
142
|
+
/^until .*Run ['"]codex app['"]/i,
|
|
143
|
+
/Do you trust the contents of this directory/i,
|
|
144
|
+
/higher risk of prompt injection/i,
|
|
145
|
+
/Yes,\s*continue.*No,\s*quit/i,
|
|
146
|
+
/^Press enter to continue$/i,
|
|
147
|
+
/^Quick safety check:/i,
|
|
148
|
+
/^Claude Code can make mistakes\./i,
|
|
149
|
+
/^Claude Code(?:'ll| will)\s+be able to read, edit, and execute files here\.?$/i,
|
|
150
|
+
/^\d+\.\s+Yes,\s*I trust this folder$/i,
|
|
151
|
+
/^\d+\.\s+No,\s*exit$/i,
|
|
152
|
+
/^Enter to confirm(?:\s+Esc to cancel)?$/i,
|
|
153
|
+
/^Welcome back .*Run \/init to create a CLAUDE\.md file with instructions for Claude\./i,
|
|
154
|
+
/^Your bash commands will be sandboxed\. Disable with \/sandbox\./i
|
|
155
|
+
];
|
|
156
|
+
FAILOVER_CONTEXT_NOISE_PATTERNS = [
|
|
157
|
+
/^Accessing workspace:?$/i,
|
|
158
|
+
/work from your team\)\. If not, take a moment to review what's in this folder first\.$/i,
|
|
159
|
+
/(?:se)?curity guide$/i,
|
|
160
|
+
/^Yes,\s*I trust this folder$/i,
|
|
161
|
+
/^Claude Code v[\d.]+$/i,
|
|
162
|
+
/^Tips for getting started$/i,
|
|
163
|
+
/^Welcome back .*Run \/init to create a CLAUDE\.md file with instructions for Claude\.?$/i,
|
|
164
|
+
/^Recent activity$/i,
|
|
165
|
+
/^No recent activity$/i,
|
|
166
|
+
/^.*\(\d+[MK]? context\)\s+Claude\b.*$/i,
|
|
167
|
+
/^don'?t ask on \(shift\+tab to cycle\)$/i,
|
|
168
|
+
/^\w+\s+\/effort$/i
|
|
169
|
+
];
|
|
107
170
|
});
|
|
108
171
|
|
|
109
172
|
// src/services/trajectory-context.ts
|
|
@@ -533,7 +596,7 @@ import { execFile } from "node:child_process";
|
|
|
533
596
|
import { createHash } from "node:crypto";
|
|
534
597
|
import { mkdir, readdir, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
535
598
|
import { homedir as homedir2 } from "node:os";
|
|
536
|
-
import
|
|
599
|
+
import path4 from "node:path";
|
|
537
600
|
import { promisify } from "node:util";
|
|
538
601
|
import { ModelType as ModelType3 } from "@elizaos/core";
|
|
539
602
|
function extractJsonBlock(raw) {
|
|
@@ -562,8 +625,8 @@ function parseValidationResponse(raw) {
|
|
|
562
625
|
}
|
|
563
626
|
}
|
|
564
627
|
function getValidationRootDir() {
|
|
565
|
-
const stateDir = process.env.MILADY_STATE_DIR?.trim() || process.env.ELIZA_STATE_DIR?.trim() ||
|
|
566
|
-
return
|
|
628
|
+
const stateDir = process.env.MILADY_STATE_DIR?.trim() || process.env.ELIZA_STATE_DIR?.trim() || path4.join(homedir2(), ".milady");
|
|
629
|
+
return path4.join(stateDir, "task-validation");
|
|
567
630
|
}
|
|
568
631
|
function truncate(text, limit = 1200) {
|
|
569
632
|
const compact = text.replace(/\s+/g, " ").trim();
|
|
@@ -600,9 +663,9 @@ async function captureValidationScreenshot(runtime, task, thread, sessionId) {
|
|
|
600
663
|
reason: "Screenshot endpoint returned an empty PNG payload"
|
|
601
664
|
};
|
|
602
665
|
}
|
|
603
|
-
const dir =
|
|
666
|
+
const dir = path4.join(getValidationRootDir(), task.threadId);
|
|
604
667
|
await mkdir(dir, { recursive: true });
|
|
605
|
-
const screenshotPath =
|
|
668
|
+
const screenshotPath = path4.join(dir, `screenshot-${sessionId}-${Date.now()}.png`);
|
|
606
669
|
await writeFile2(screenshotPath, bytes);
|
|
607
670
|
const screenshotDescription = await describeScreenshotContent(runtime, task, thread, bytes);
|
|
608
671
|
return {
|
|
@@ -626,7 +689,7 @@ async function captureValidationScreenshot(runtime, task, thread, sessionId) {
|
|
|
626
689
|
}
|
|
627
690
|
}
|
|
628
691
|
async function listRelevantTrajectories(runtime, task, thread) {
|
|
629
|
-
const logger2 = runtime.getService("
|
|
692
|
+
const logger2 = runtime.getService("trajectories");
|
|
630
693
|
if (!logger2?.listTrajectories) {
|
|
631
694
|
return [];
|
|
632
695
|
}
|
|
@@ -731,7 +794,7 @@ async function collectWorkspaceEvidence(workdir) {
|
|
|
731
794
|
evidence.notes.push("no workdir supplied");
|
|
732
795
|
return evidence;
|
|
733
796
|
}
|
|
734
|
-
const resolved = workdir.startsWith("~") ?
|
|
797
|
+
const resolved = workdir.startsWith("~") ? path4.join(homedir2(), workdir.slice(1)) : path4.resolve(workdir);
|
|
735
798
|
evidence.workdir = resolved;
|
|
736
799
|
try {
|
|
737
800
|
const rootStat = await stat(resolved);
|
|
@@ -766,13 +829,13 @@ async function collectWorkspaceEvidence(workdir) {
|
|
|
766
829
|
walkCeilingHit = true;
|
|
767
830
|
break;
|
|
768
831
|
}
|
|
769
|
-
const full =
|
|
832
|
+
const full = path4.join(dir, entry.name);
|
|
770
833
|
if (entry.isDirectory()) {
|
|
771
834
|
await walk(full, depth + 1);
|
|
772
835
|
} else if (entry.isFile()) {
|
|
773
836
|
totalCount++;
|
|
774
837
|
if (collected.length < WORKSPACE_EVIDENCE_FILE_LIMIT) {
|
|
775
|
-
collected.push(
|
|
838
|
+
collected.push(path4.relative(resolved, full));
|
|
776
839
|
}
|
|
777
840
|
}
|
|
778
841
|
}
|
|
@@ -913,9 +976,9 @@ function buildValidationPrompt(task, thread, completionReasoning, completionSumm
|
|
|
913
976
|
`);
|
|
914
977
|
}
|
|
915
978
|
async function persistValidationReport(threadId, sessionId, report) {
|
|
916
|
-
const dir =
|
|
979
|
+
const dir = path4.join(getValidationRootDir(), threadId);
|
|
917
980
|
await mkdir(dir, { recursive: true });
|
|
918
|
-
const reportPath =
|
|
981
|
+
const reportPath = path4.join(dir, `validation-${sessionId}-${Date.now()}.json`);
|
|
919
982
|
await writeFile2(reportPath, JSON.stringify(report, null, 2), "utf8");
|
|
920
983
|
return reportPath;
|
|
921
984
|
}
|
|
@@ -1047,6 +1110,338 @@ var init_task_validation = __esm(() => {
|
|
|
1047
1110
|
]);
|
|
1048
1111
|
});
|
|
1049
1112
|
|
|
1113
|
+
// src/services/task-verifier-runner.ts
|
|
1114
|
+
import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
|
|
1115
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1116
|
+
import { homedir as homedir3 } from "node:os";
|
|
1117
|
+
import path5 from "node:path";
|
|
1118
|
+
import { ModelType as ModelType4 } from "@elizaos/core";
|
|
1119
|
+
function extractJsonBlock2(raw) {
|
|
1120
|
+
const trimmed = raw.trim();
|
|
1121
|
+
const fenceMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
1122
|
+
return (fenceMatch?.[1] ?? trimmed).trim();
|
|
1123
|
+
}
|
|
1124
|
+
function parseAcceptanceEvaluation(raw) {
|
|
1125
|
+
try {
|
|
1126
|
+
const parsed = JSON.parse(extractJsonBlock2(raw));
|
|
1127
|
+
if (parsed.verdict !== "pass" && parsed.verdict !== "fail" || typeof parsed.summary !== "string" || parsed.summary.trim().length === 0) {
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
const checklist = Array.isArray(parsed.checklist) ? parsed.checklist.filter((entry) => Boolean(entry && typeof entry === "object" && typeof entry.criterion === "string" && typeof entry.status === "string" && typeof entry.evidence === "string")).map((entry) => ({
|
|
1131
|
+
criterion: entry.criterion.trim(),
|
|
1132
|
+
status: entry.status === "pass" || entry.status === "fail" || entry.status === "partial" ? entry.status : "partial",
|
|
1133
|
+
evidence: entry.evidence.trim()
|
|
1134
|
+
})).filter((entry) => entry.criterion.length > 0 && entry.evidence.length > 0) : [];
|
|
1135
|
+
return {
|
|
1136
|
+
verdict: parsed.verdict,
|
|
1137
|
+
summary: parsed.summary.trim(),
|
|
1138
|
+
checklist
|
|
1139
|
+
};
|
|
1140
|
+
} catch {
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function getVerifierRootDir() {
|
|
1145
|
+
const stateDir = process.env.MILADY_STATE_DIR?.trim() || process.env.ELIZA_STATE_DIR?.trim() || path5.join(homedir3(), ".milady");
|
|
1146
|
+
return path5.join(stateDir, "task-verifiers");
|
|
1147
|
+
}
|
|
1148
|
+
function truncate2(text, limit = 1200) {
|
|
1149
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
1150
|
+
return compact.length <= limit ? compact : `${compact.slice(0, limit)}...`;
|
|
1151
|
+
}
|
|
1152
|
+
function isVerifierReady(thread, job) {
|
|
1153
|
+
if (job.status !== "pending")
|
|
1154
|
+
return false;
|
|
1155
|
+
if (job.verifierType !== "acceptance_criteria")
|
|
1156
|
+
return false;
|
|
1157
|
+
const executionNodes = thread.nodes.filter((node2) => node2.kind !== "goal");
|
|
1158
|
+
if (executionNodes.some((node2) => !terminalNodeStates.has(node2.status))) {
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
if (!job.nodeId)
|
|
1162
|
+
return true;
|
|
1163
|
+
const node = thread.nodes.find((entry) => entry.id === job.nodeId);
|
|
1164
|
+
return node ? terminalNodeStates.has(node.status) : false;
|
|
1165
|
+
}
|
|
1166
|
+
function collectAcceptancePrerequisiteFailures(thread) {
|
|
1167
|
+
const executionNodes = thread.nodes.filter((node) => node.kind === "execution");
|
|
1168
|
+
const passedCompletionVerifierNodeIds = new Set(thread.verifierJobs.filter((job) => job.verifierType === "task_completion" && job.status === "passed" && typeof job.nodeId === "string").map((job) => job.nodeId));
|
|
1169
|
+
const evidenceBackedNodeIds = new Set(thread.evidence.filter((entry) => typeof entry.nodeId === "string" && (entry.evidenceType === "validation_summary" || entry.evidenceType === "acceptance_report")).map((entry) => entry.nodeId));
|
|
1170
|
+
return {
|
|
1171
|
+
failedExecutionNodes: executionNodes.filter((node) => node.status === "failed" || node.status === "canceled" || node.status === "interrupted"),
|
|
1172
|
+
completedWithoutEvidence: executionNodes.filter((node) => node.status === "completed" && !passedCompletionVerifierNodeIds.has(node.id) && !evidenceBackedNodeIds.has(node.id))
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
function buildDeterministicFailureEvaluation(thread, failures) {
|
|
1176
|
+
if (failures.failedExecutionNodes.length > 0) {
|
|
1177
|
+
return {
|
|
1178
|
+
verdict: "fail",
|
|
1179
|
+
summary: `Acceptance cannot pass because execution nodes failed: ${failures.failedExecutionNodes.map((node) => node.title).join(", ")}.`,
|
|
1180
|
+
checklist: thread.acceptanceCriteria.map((criterion) => ({
|
|
1181
|
+
criterion,
|
|
1182
|
+
status: "fail",
|
|
1183
|
+
evidence: `Execution nodes failed before acceptance verification completed: ${failures.failedExecutionNodes.map((node) => `${node.title}=${node.status}`).join(", ")}.`
|
|
1184
|
+
}))
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
if (failures.completedWithoutEvidence.length > 0) {
|
|
1188
|
+
return {
|
|
1189
|
+
verdict: "fail",
|
|
1190
|
+
summary: `Acceptance cannot pass because completed execution nodes lack verification evidence: ${failures.completedWithoutEvidence.map((node) => node.title).join(", ")}.`,
|
|
1191
|
+
checklist: thread.acceptanceCriteria.map((criterion) => ({
|
|
1192
|
+
criterion,
|
|
1193
|
+
status: "partial",
|
|
1194
|
+
evidence: `Missing task-completion evidence for: ${failures.completedWithoutEvidence.map((node) => node.title).join(", ")}.`
|
|
1195
|
+
}))
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
return null;
|
|
1199
|
+
}
|
|
1200
|
+
function summarizeThreadEvidence(thread) {
|
|
1201
|
+
const sessionSummaries = thread.sessions.map((session) => [
|
|
1202
|
+
`${session.label} [${session.framework}] status=${session.status}`,
|
|
1203
|
+
session.completionSummary ? `summary=${truncate2(session.completionSummary, 400)}` : ""
|
|
1204
|
+
].filter(Boolean).join(" | ")).filter(Boolean).join(`
|
|
1205
|
+
`);
|
|
1206
|
+
const decisions = thread.decisions.slice(-8).map((decision) => `${decision.decision}: ${truncate2(decision.reasoning, 240)}`).join(`
|
|
1207
|
+
`);
|
|
1208
|
+
const artifacts = thread.artifacts.slice(-12).map((artifact) => `${artifact.artifactType}: ${artifact.title}${artifact.path ? ` (${artifact.path})` : ""}`).join(`
|
|
1209
|
+
`);
|
|
1210
|
+
const evidence = thread.evidence.slice(-16).map((entry) => `${entry.evidenceType}: ${entry.title}${entry.summary ? ` - ${truncate2(entry.summary, 220)}` : ""}`).join(`
|
|
1211
|
+
`);
|
|
1212
|
+
const transcripts = thread.transcripts.slice(-8).map((entry) => `${entry.direction}: ${truncate2(entry.content, 240)}`).join(`
|
|
1213
|
+
`);
|
|
1214
|
+
return [
|
|
1215
|
+
sessionSummaries ? `Sessions
|
|
1216
|
+
${sessionSummaries}` : "",
|
|
1217
|
+
decisions ? `Decisions
|
|
1218
|
+
${decisions}` : "",
|
|
1219
|
+
artifacts ? `Artifacts
|
|
1220
|
+
${artifacts}` : "",
|
|
1221
|
+
evidence ? `Evidence
|
|
1222
|
+
${evidence}` : "",
|
|
1223
|
+
transcripts ? `Recent transcripts
|
|
1224
|
+
${transcripts}` : ""
|
|
1225
|
+
].filter(Boolean).join(`
|
|
1226
|
+
|
|
1227
|
+
`);
|
|
1228
|
+
}
|
|
1229
|
+
async function evaluateAcceptanceCriteria(runtime, thread, job) {
|
|
1230
|
+
const prompt = [
|
|
1231
|
+
"You are a strict task verifier for an agent coordinator.",
|
|
1232
|
+
"Decide whether the thread satisfies its acceptance criteria based only on the provided evidence.",
|
|
1233
|
+
"Return JSON only with keys: verdict, summary, checklist.",
|
|
1234
|
+
'Set verdict to "pass" only if every criterion is satisfied by concrete evidence.',
|
|
1235
|
+
'Set verdict to "fail" if any criterion is missing, contradicted, or unsupported.',
|
|
1236
|
+
"Each checklist entry must contain criterion, status (pass|fail|partial), and evidence.",
|
|
1237
|
+
"",
|
|
1238
|
+
`Thread title: ${thread.title}`,
|
|
1239
|
+
`Original request: ${thread.originalRequest}`,
|
|
1240
|
+
`Verifier job: ${job.title}`,
|
|
1241
|
+
`Acceptance criteria:
|
|
1242
|
+
${thread.acceptanceCriteria.map((criterion, index) => `${index + 1}. ${criterion}`).join(`
|
|
1243
|
+
`)}`,
|
|
1244
|
+
"",
|
|
1245
|
+
`Thread evidence:
|
|
1246
|
+
${summarizeThreadEvidence(thread) || "No evidence recorded."}`
|
|
1247
|
+
].join(`
|
|
1248
|
+
`);
|
|
1249
|
+
const raw = await withTrajectoryContext(runtime, {
|
|
1250
|
+
source: "orchestrator",
|
|
1251
|
+
decisionType: "acceptance-verifier",
|
|
1252
|
+
threadId: thread.id,
|
|
1253
|
+
verifierJobId: job.id
|
|
1254
|
+
}, () => runtime.useModel(ModelType4.TEXT_SMALL, {
|
|
1255
|
+
prompt,
|
|
1256
|
+
temperature: 0,
|
|
1257
|
+
stream: false
|
|
1258
|
+
}));
|
|
1259
|
+
const parsed = parseAcceptanceEvaluation(raw);
|
|
1260
|
+
if (parsed) {
|
|
1261
|
+
return parsed;
|
|
1262
|
+
}
|
|
1263
|
+
return {
|
|
1264
|
+
verdict: "fail",
|
|
1265
|
+
summary: "Acceptance verifier returned an invalid response, so the thread could not be proven complete.",
|
|
1266
|
+
checklist: thread.acceptanceCriteria.map((criterion) => ({
|
|
1267
|
+
criterion,
|
|
1268
|
+
status: "partial",
|
|
1269
|
+
evidence: "Verifier response was invalid JSON."
|
|
1270
|
+
}))
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
async function writeAcceptanceReport(thread, job, evaluation) {
|
|
1274
|
+
const dir = path5.join(getVerifierRootDir(), thread.id);
|
|
1275
|
+
await mkdir2(dir, { recursive: true });
|
|
1276
|
+
const reportPath = path5.join(dir, `${job.id}.json`);
|
|
1277
|
+
const report = {
|
|
1278
|
+
threadId: thread.id,
|
|
1279
|
+
verifierJobId: job.id,
|
|
1280
|
+
title: job.title,
|
|
1281
|
+
originalRequest: thread.originalRequest,
|
|
1282
|
+
acceptanceCriteria: thread.acceptanceCriteria,
|
|
1283
|
+
evaluation,
|
|
1284
|
+
generatedAt: new Date().toISOString()
|
|
1285
|
+
};
|
|
1286
|
+
const serialized = JSON.stringify(report, null, 2);
|
|
1287
|
+
await writeFile3(reportPath, serialized, "utf8");
|
|
1288
|
+
return {
|
|
1289
|
+
reportPath,
|
|
1290
|
+
sha256: createHash2("sha256").update(serialized).digest("hex")
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
async function finalizeAcceptanceJob(taskRegistry, thread, job, evaluation, reportPath, sha256) {
|
|
1294
|
+
await taskRegistry.updateTaskVerifierJob(job.id, {
|
|
1295
|
+
status: evaluation.verdict === "pass" ? "passed" : "failed",
|
|
1296
|
+
completedAt: new Date().toISOString(),
|
|
1297
|
+
metadata: {
|
|
1298
|
+
verdict: evaluation.verdict,
|
|
1299
|
+
summary: evaluation.summary,
|
|
1300
|
+
reportPath,
|
|
1301
|
+
sha256,
|
|
1302
|
+
checklist: evaluation.checklist
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
await taskRegistry.recordArtifact({
|
|
1306
|
+
threadId: thread.id,
|
|
1307
|
+
sessionId: thread.latestSessionId ?? null,
|
|
1308
|
+
artifactType: "acceptance_report",
|
|
1309
|
+
title: `${job.title} report`,
|
|
1310
|
+
path: reportPath,
|
|
1311
|
+
mimeType: "application/json",
|
|
1312
|
+
metadata: {
|
|
1313
|
+
verifierJobId: job.id,
|
|
1314
|
+
verdict: evaluation.verdict,
|
|
1315
|
+
sha256
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
await taskRegistry.recordTaskEvidence({
|
|
1319
|
+
threadId: thread.id,
|
|
1320
|
+
nodeId: job.nodeId,
|
|
1321
|
+
sessionId: thread.latestSessionId ?? null,
|
|
1322
|
+
verifierJobId: job.id,
|
|
1323
|
+
evidenceType: "acceptance_report",
|
|
1324
|
+
title: job.title,
|
|
1325
|
+
summary: evaluation.summary,
|
|
1326
|
+
path: reportPath,
|
|
1327
|
+
content: {
|
|
1328
|
+
checklist: evaluation.checklist,
|
|
1329
|
+
verdict: evaluation.verdict
|
|
1330
|
+
},
|
|
1331
|
+
metadata: {
|
|
1332
|
+
sha256
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
await taskRegistry.appendEvent({
|
|
1336
|
+
threadId: thread.id,
|
|
1337
|
+
sessionId: thread.latestSessionId ?? null,
|
|
1338
|
+
eventType: "verifier_job_completed",
|
|
1339
|
+
summary: `${job.title} ${evaluation.verdict}`,
|
|
1340
|
+
data: {
|
|
1341
|
+
verifierJobId: job.id,
|
|
1342
|
+
verdict: evaluation.verdict,
|
|
1343
|
+
reportPath
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
if (job.nodeId) {
|
|
1347
|
+
const node = thread.nodes.find((entry) => entry.id === job.nodeId);
|
|
1348
|
+
const patch = evaluation.verdict === "pass" ? {
|
|
1349
|
+
metadata: {
|
|
1350
|
+
acceptanceVerifierJobId: job.id,
|
|
1351
|
+
acceptanceSummary: evaluation.summary
|
|
1352
|
+
}
|
|
1353
|
+
} : {
|
|
1354
|
+
status: "failed",
|
|
1355
|
+
metadata: {
|
|
1356
|
+
acceptanceVerifierJobId: job.id,
|
|
1357
|
+
acceptanceSummary: evaluation.summary
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
if (node) {
|
|
1361
|
+
await taskRegistry.updateTaskNode(node.id, patch);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
async function runReadyTaskVerifiers(runtime, taskRegistry, threadId) {
|
|
1366
|
+
let thread = await taskRegistry.getThread(threadId);
|
|
1367
|
+
if (!thread) {
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const readyJobs = thread.verifierJobs.filter((job) => isVerifierReady(thread, job));
|
|
1371
|
+
for (const job of readyJobs) {
|
|
1372
|
+
if (activeVerifierRuns.has(job.id)) {
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
activeVerifierRuns.add(job.id);
|
|
1376
|
+
try {
|
|
1377
|
+
await taskRegistry.updateTaskVerifierJob(job.id, {
|
|
1378
|
+
status: "running",
|
|
1379
|
+
startedAt: new Date().toISOString(),
|
|
1380
|
+
metadata: {
|
|
1381
|
+
source: "acceptance-runner"
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
await taskRegistry.appendEvent({
|
|
1385
|
+
threadId,
|
|
1386
|
+
sessionId: thread.latestSessionId ?? null,
|
|
1387
|
+
eventType: "verifier_job_started",
|
|
1388
|
+
summary: `Running ${job.title}`,
|
|
1389
|
+
data: {
|
|
1390
|
+
verifierJobId: job.id,
|
|
1391
|
+
verifierType: job.verifierType
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
thread = await taskRegistry.getThread(threadId) ?? thread;
|
|
1395
|
+
const deterministicFailure = buildDeterministicFailureEvaluation(thread, collectAcceptancePrerequisiteFailures(thread));
|
|
1396
|
+
const evaluation = deterministicFailure ?? await evaluateAcceptanceCriteria(runtime, thread, job);
|
|
1397
|
+
const report = await writeAcceptanceReport(thread, job, evaluation);
|
|
1398
|
+
await finalizeAcceptanceJob(taskRegistry, thread, job, evaluation, report.reportPath, report.sha256);
|
|
1399
|
+
thread = await taskRegistry.getThread(threadId) ?? thread;
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1402
|
+
await taskRegistry.updateTaskVerifierJob(job.id, {
|
|
1403
|
+
status: "failed",
|
|
1404
|
+
completedAt: new Date().toISOString(),
|
|
1405
|
+
metadata: {
|
|
1406
|
+
source: "acceptance-runner",
|
|
1407
|
+
error: message
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
await taskRegistry.appendEvent({
|
|
1411
|
+
threadId,
|
|
1412
|
+
sessionId: thread.latestSessionId ?? null,
|
|
1413
|
+
eventType: "verifier_job_failed",
|
|
1414
|
+
summary: `${job.title} failed`,
|
|
1415
|
+
data: {
|
|
1416
|
+
verifierJobId: job.id,
|
|
1417
|
+
error: message
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
if (job.nodeId) {
|
|
1421
|
+
await taskRegistry.updateTaskNode(job.nodeId, {
|
|
1422
|
+
status: "failed",
|
|
1423
|
+
metadata: {
|
|
1424
|
+
acceptanceVerifierJobId: job.id,
|
|
1425
|
+
acceptanceSummary: message
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
} finally {
|
|
1430
|
+
activeVerifierRuns.delete(job.id);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
var terminalNodeStates, activeVerifierRuns;
|
|
1435
|
+
var init_task_verifier_runner = __esm(() => {
|
|
1436
|
+
terminalNodeStates = new Set([
|
|
1437
|
+
"completed",
|
|
1438
|
+
"failed",
|
|
1439
|
+
"canceled",
|
|
1440
|
+
"interrupted"
|
|
1441
|
+
]);
|
|
1442
|
+
activeVerifierRuns = new Set;
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1050
1445
|
// src/services/swarm-decision-loop.ts
|
|
1051
1446
|
var exports_swarm_decision_loop = {};
|
|
1052
1447
|
__export(exports_swarm_decision_loop, {
|
|
@@ -1061,8 +1456,8 @@ __export(exports_swarm_decision_loop, {
|
|
|
1061
1456
|
checkAllTasksComplete: () => checkAllTasksComplete,
|
|
1062
1457
|
POST_SEND_COOLDOWN_MS: () => POST_SEND_COOLDOWN_MS
|
|
1063
1458
|
});
|
|
1064
|
-
import * as
|
|
1065
|
-
import { ModelType as
|
|
1459
|
+
import * as path6 from "node:path";
|
|
1460
|
+
import { ModelType as ModelType5 } from "@elizaos/core";
|
|
1066
1461
|
function withTimeout(promise, ms, label) {
|
|
1067
1462
|
return new Promise((resolve2, reject) => {
|
|
1068
1463
|
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
@@ -1195,6 +1590,27 @@ function formatDecisionResponse(decision) {
|
|
|
1195
1590
|
return;
|
|
1196
1591
|
return decision.useKeys ? `keys:${decision.keys?.join(",")}` : decision.response;
|
|
1197
1592
|
}
|
|
1593
|
+
function truncateForUser(text, max = 140) {
|
|
1594
|
+
const trimmed = text.trim();
|
|
1595
|
+
if (trimmed.length <= max) {
|
|
1596
|
+
return trimmed;
|
|
1597
|
+
}
|
|
1598
|
+
return `${trimmed.slice(0, max)}...`;
|
|
1599
|
+
}
|
|
1600
|
+
function formatSuggestedAction(decision) {
|
|
1601
|
+
if (!decision) {
|
|
1602
|
+
return "Needs human review with no automatic suggestion.";
|
|
1603
|
+
}
|
|
1604
|
+
if (decision.action === "respond") {
|
|
1605
|
+
if (decision.useKeys && decision.keys?.length) {
|
|
1606
|
+
return `Suggested action: send keys ${decision.keys.join(", ")}.`;
|
|
1607
|
+
}
|
|
1608
|
+
if (decision.response?.trim()) {
|
|
1609
|
+
return `Suggested action: reply "${truncateForUser(decision.response, 80)}".`;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return `Suggested action: ${decision.action}.`;
|
|
1613
|
+
}
|
|
1198
1614
|
function decisionFromSuggestedResponse(suggestedResponse, reasoning = "Used adapter-provided auto-response for a routine blocking prompt.") {
|
|
1199
1615
|
if (suggestedResponse.startsWith("keys:")) {
|
|
1200
1616
|
return {
|
|
@@ -1219,8 +1635,8 @@ function inferRoutinePromptResponse(promptText, promptType) {
|
|
|
1219
1635
|
}
|
|
1220
1636
|
if (promptType === "config" && /claude (?:dialog awaiting navigation|menu navigation required)/i.test(promptText)) {
|
|
1221
1637
|
return {
|
|
1222
|
-
suggestedResponse: "keys:
|
|
1223
|
-
reasoning: "
|
|
1638
|
+
suggestedResponse: "keys:enter",
|
|
1639
|
+
reasoning: "Accepted Claude's default dialog action so the replacement session can continue without exiting the CLI."
|
|
1224
1640
|
};
|
|
1225
1641
|
}
|
|
1226
1642
|
if (promptType && promptType !== "unknown") {
|
|
@@ -1252,10 +1668,10 @@ function isOutOfScopeAccess(promptText, workdir) {
|
|
|
1252
1668
|
];
|
|
1253
1669
|
if (matches.length === 0)
|
|
1254
1670
|
return false;
|
|
1255
|
-
const resolvedWorkdir =
|
|
1671
|
+
const resolvedWorkdir = path6.resolve(workdir);
|
|
1256
1672
|
return matches.some((p) => {
|
|
1257
|
-
const resolved =
|
|
1258
|
-
return !resolved.startsWith(resolvedWorkdir +
|
|
1673
|
+
const resolved = path6.resolve(p);
|
|
1674
|
+
return !resolved.startsWith(resolvedWorkdir + path6.sep) && resolved !== resolvedWorkdir;
|
|
1259
1675
|
});
|
|
1260
1676
|
}
|
|
1261
1677
|
function checkAllTasksComplete(ctx) {
|
|
@@ -1273,20 +1689,18 @@ async function checkAllTasksCompleteAsync(ctx) {
|
|
|
1273
1689
|
return;
|
|
1274
1690
|
}
|
|
1275
1691
|
const threadIds = [...new Set(tasks.map((task) => task.threadId))];
|
|
1692
|
+
const failingThreads = [];
|
|
1276
1693
|
for (const threadId of threadIds) {
|
|
1694
|
+
await runReadyTaskVerifiers(ctx.runtime, ctx.taskRegistry, threadId);
|
|
1277
1695
|
const thread = await ctx.taskRegistry.getThread(threadId);
|
|
1278
1696
|
if (!thread || thread.nodes.length === 0) {
|
|
1279
1697
|
continue;
|
|
1280
1698
|
}
|
|
1281
|
-
const terminalNodeStates = new Set([
|
|
1282
|
-
"completed",
|
|
1283
|
-
"failed",
|
|
1284
|
-
"canceled",
|
|
1285
|
-
"interrupted"
|
|
1286
|
-
]);
|
|
1287
1699
|
const goalNodes = thread.nodes.filter((node) => node.kind === "goal");
|
|
1288
|
-
|
|
1289
|
-
|
|
1700
|
+
const failedGoals = goalNodes.filter((node) => node.status === "failed" || node.status === "canceled" || node.status === "interrupted").map((node) => `${node.title}=${node.status}`);
|
|
1701
|
+
const incompleteGoals = goalNodes.filter((node) => node.status !== "completed" && node.status !== "failed" && node.status !== "canceled" && node.status !== "interrupted").map((node) => `${node.title}=${node.status}`);
|
|
1702
|
+
if (incompleteGoals.length > 0) {
|
|
1703
|
+
const pendingGoals = goalNodes.filter((node) => node.status !== "completed").map((node) => `${node.title}=${node.status}`).join(", ");
|
|
1290
1704
|
ctx.log(`checkAllTasksComplete: thread ${threadId} still has non-terminal goal nodes — ${pendingGoals}`);
|
|
1291
1705
|
return;
|
|
1292
1706
|
}
|
|
@@ -1295,6 +1709,43 @@ async function checkAllTasksCompleteAsync(ctx) {
|
|
|
1295
1709
|
ctx.log(`checkAllTasksComplete: thread ${threadId} still has running verifier jobs`);
|
|
1296
1710
|
return;
|
|
1297
1711
|
}
|
|
1712
|
+
const pendingVerifiers = thread.verifierJobs.filter((job) => job.status === "pending");
|
|
1713
|
+
if (pendingVerifiers.length > 0) {
|
|
1714
|
+
ctx.log(`checkAllTasksComplete: thread ${threadId} still has pending verifier jobs`);
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
const failedVerifiers = thread.verifierJobs.filter((job) => job.status === "failed").map((job) => `${job.title}=failed`);
|
|
1718
|
+
if (failedGoals.length > 0 || failedVerifiers.length > 0) {
|
|
1719
|
+
failingThreads.push({
|
|
1720
|
+
threadId,
|
|
1721
|
+
failedGoals,
|
|
1722
|
+
failedVerifiers
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if (failingThreads.length > 0) {
|
|
1727
|
+
if (ctx.swarmCompleteNotified) {
|
|
1728
|
+
ctx.log("checkAllTasksComplete: failure notification already sent — skipping");
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
ctx.swarmCompleteNotified = true;
|
|
1732
|
+
const summary = failingThreads.map((thread) => [
|
|
1733
|
+
`thread ${thread.threadId}`,
|
|
1734
|
+
thread.failedGoals.length > 0 ? `failed goals: ${thread.failedGoals.join(", ")}` : "",
|
|
1735
|
+
thread.failedVerifiers.length > 0 ? `failed verifiers: ${thread.failedVerifiers.join(", ")}` : ""
|
|
1736
|
+
].filter(Boolean).join(" | ")).join("; ");
|
|
1737
|
+
ctx.log(`checkAllTasksComplete: sessions are terminal but acceptance failed — ${summary}`);
|
|
1738
|
+
ctx.broadcast({
|
|
1739
|
+
type: "swarm_attention_required",
|
|
1740
|
+
sessionId: "",
|
|
1741
|
+
timestamp: Date.now(),
|
|
1742
|
+
data: {
|
|
1743
|
+
summary,
|
|
1744
|
+
threads: failingThreads
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
ctx.sendChatMessage(`Task agents finished running, but the coordinator could not prove completion. ${summary}`, "task-agent");
|
|
1748
|
+
return;
|
|
1298
1749
|
}
|
|
1299
1750
|
if (ctx.swarmCompleteNotified) {
|
|
1300
1751
|
ctx.log("checkAllTasksComplete: already notified — skipping");
|
|
@@ -1384,7 +1835,7 @@ async function makeCoordinationDecision(ctx, taskCtx, promptText, recentOutput)
|
|
|
1384
1835
|
repo: taskCtx.repo,
|
|
1385
1836
|
workdir: taskCtx.workdir,
|
|
1386
1837
|
originalTask: taskCtx.originalTask
|
|
1387
|
-
}, () => ctx.runtime.useModel(
|
|
1838
|
+
}, () => ctx.runtime.useModel(ModelType5.TEXT_SMALL, { prompt }));
|
|
1388
1839
|
return parseCoordinationResponse(result);
|
|
1389
1840
|
} catch (err) {
|
|
1390
1841
|
ctx.log(`LLM coordination call failed: ${err}`);
|
|
@@ -1803,6 +2254,7 @@ async function handleBlocked(ctx, sessionId, taskCtx, data) {
|
|
|
1803
2254
|
reason: "max_auto_responses_exceeded"
|
|
1804
2255
|
}
|
|
1805
2256
|
});
|
|
2257
|
+
ctx.sendChatMessage(`[${taskCtx.label}] Paused for your attention after ${MAX_AUTO_RESPONSES} consecutive automatic approvals. Prompt: ${truncateForUser(promptText, 180)}`, "coding-agent");
|
|
1806
2258
|
return;
|
|
1807
2259
|
}
|
|
1808
2260
|
switch (ctx.getSupervisionLevel()) {
|
|
@@ -1820,6 +2272,7 @@ async function handleBlocked(ctx, sessionId, taskCtx, data) {
|
|
|
1820
2272
|
decision: "escalate",
|
|
1821
2273
|
reasoning: "Supervision level is notify — broadcasting only"
|
|
1822
2274
|
});
|
|
2275
|
+
ctx.sendChatMessage(`[${taskCtx.label}] Waiting on a blocked prompt: ${truncateForUser(promptText, 180)}`, "coding-agent");
|
|
1823
2276
|
break;
|
|
1824
2277
|
}
|
|
1825
2278
|
}
|
|
@@ -1909,7 +2362,7 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
|
|
|
1909
2362
|
repo: taskCtx.repo,
|
|
1910
2363
|
workdir: taskCtx.workdir,
|
|
1911
2364
|
originalTask: taskCtx.originalTask
|
|
1912
|
-
}, () => ctx.runtime.useModel(
|
|
2365
|
+
}, () => ctx.runtime.useModel(ModelType5.TEXT_SMALL, { prompt }));
|
|
1913
2366
|
decision = parseCoordinationResponse(result);
|
|
1914
2367
|
} catch (err) {
|
|
1915
2368
|
ctx.log(`Turn-complete LLM call failed: ${err}`);
|
|
@@ -1950,6 +2403,7 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
|
|
|
1950
2403
|
const instruction = decision.response ?? "";
|
|
1951
2404
|
const preview = instruction.length > 120 ? `${instruction.slice(0, 120)}...` : instruction;
|
|
1952
2405
|
ctx.log(`[${taskCtx.label}] Turn done, continuing: ${preview}`);
|
|
2406
|
+
ctx.sendChatMessage(`[${taskCtx.label}] Continuing work: ${preview || "sent follow-up instructions."}`, "coding-agent");
|
|
1953
2407
|
} else if (decision.action === "escalate") {
|
|
1954
2408
|
ctx.sendChatMessage(`[${taskCtx.label}] Turn finished — needs your attention: ${decision.reasoning}`, "coding-agent");
|
|
1955
2409
|
}
|
|
@@ -2017,6 +2471,7 @@ async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, rec
|
|
|
2017
2471
|
reason: "invalid_llm_response"
|
|
2018
2472
|
}
|
|
2019
2473
|
});
|
|
2474
|
+
ctx.sendChatMessage(`[${taskCtx.label}] Needs your attention: the coordinator could not decide how to handle "${truncateForUser(promptText, 160)}".`, "coding-agent");
|
|
2020
2475
|
return;
|
|
2021
2476
|
}
|
|
2022
2477
|
if (decision.action === "respond" && isOutOfScopeAccess(promptText, taskCtx.workdir)) {
|
|
@@ -2170,6 +2625,11 @@ async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recent
|
|
|
2170
2625
|
fromPipeline: decisionFromPipeline
|
|
2171
2626
|
}
|
|
2172
2627
|
});
|
|
2628
|
+
ctx.sendChatMessage([
|
|
2629
|
+
`[${taskCtx.label}] Waiting for your approval: ${truncateForUser(promptText, 180)}`,
|
|
2630
|
+
formatSuggestedAction(decision),
|
|
2631
|
+
decision?.reasoning ? `Reason: ${truncateForUser(decision.reasoning, 180)}` : ""
|
|
2632
|
+
].filter(Boolean).join(" "), "coding-agent");
|
|
2173
2633
|
} finally {
|
|
2174
2634
|
ctx.inFlightDecisions.delete(sessionId);
|
|
2175
2635
|
await drainPendingTurnComplete(ctx, sessionId);
|
|
@@ -2181,6 +2641,7 @@ var init_swarm_decision_loop = __esm(() => {
|
|
|
2181
2641
|
init_ansi_utils();
|
|
2182
2642
|
init_swarm_event_triage();
|
|
2183
2643
|
init_task_validation();
|
|
2644
|
+
init_task_verifier_runner();
|
|
2184
2645
|
deferredTurnCompleteTimers = new Map;
|
|
2185
2646
|
});
|
|
2186
2647
|
|
|
@@ -3145,7 +3606,7 @@ var init_query_promise = __esm(() => {
|
|
|
3145
3606
|
// node_modules/drizzle-orm/utils.js
|
|
3146
3607
|
function mapResultRow(columns, row, joinsNotNullableMap) {
|
|
3147
3608
|
const nullifyMap = {};
|
|
3148
|
-
const result = columns.reduce((result2, { path:
|
|
3609
|
+
const result = columns.reduce((result2, { path: path9, field }, columnIndex) => {
|
|
3149
3610
|
let decoder;
|
|
3150
3611
|
if (is(field, Column)) {
|
|
3151
3612
|
decoder = field;
|
|
@@ -3157,8 +3618,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
|
|
|
3157
3618
|
decoder = field.sql.decoder;
|
|
3158
3619
|
}
|
|
3159
3620
|
let node = result2;
|
|
3160
|
-
for (const [pathChunkIndex, pathChunk] of
|
|
3161
|
-
if (pathChunkIndex <
|
|
3621
|
+
for (const [pathChunkIndex, pathChunk] of path9.entries()) {
|
|
3622
|
+
if (pathChunkIndex < path9.length - 1) {
|
|
3162
3623
|
if (!(pathChunk in node)) {
|
|
3163
3624
|
node[pathChunk] = {};
|
|
3164
3625
|
}
|
|
@@ -3166,8 +3627,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
|
|
|
3166
3627
|
} else {
|
|
3167
3628
|
const rawValue = row[columnIndex];
|
|
3168
3629
|
const value = node[pathChunk] = rawValue === null ? null : decoder.mapFromDriverValue(rawValue);
|
|
3169
|
-
if (joinsNotNullableMap && is(field, Column) &&
|
|
3170
|
-
const objectName =
|
|
3630
|
+
if (joinsNotNullableMap && is(field, Column) && path9.length === 2) {
|
|
3631
|
+
const objectName = path9[0];
|
|
3171
3632
|
if (!(objectName in nullifyMap)) {
|
|
3172
3633
|
nullifyMap[objectName] = value === null ? getTableName(field.table) : false;
|
|
3173
3634
|
} else if (typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== getTableName(field.table)) {
|
|
@@ -3971,6 +4432,194 @@ var init_drizzle_orm = __esm(() => {
|
|
|
3971
4432
|
init_view_common();
|
|
3972
4433
|
});
|
|
3973
4434
|
|
|
4435
|
+
// src/services/task-policy.ts
|
|
4436
|
+
import fs from "node:fs";
|
|
4437
|
+
import path from "node:path";
|
|
4438
|
+
import { pathToFileURL } from "node:url";
|
|
4439
|
+
var ROLE_RANK = {
|
|
4440
|
+
GUEST: 0,
|
|
4441
|
+
USER: 1,
|
|
4442
|
+
ADMIN: 2,
|
|
4443
|
+
OWNER: 3
|
|
4444
|
+
};
|
|
4445
|
+
var DEFAULT_POLICY = {
|
|
4446
|
+
default: "GUEST",
|
|
4447
|
+
connectors: {
|
|
4448
|
+
discord: {
|
|
4449
|
+
create: "ADMIN",
|
|
4450
|
+
interact: "ADMIN"
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
};
|
|
4454
|
+
var LOCAL_ROLES_MODULE_CANDIDATES = [
|
|
4455
|
+
path.resolve(process.cwd(), "packages/plugin-roles/src/index.ts"),
|
|
4456
|
+
path.resolve(process.cwd(), "packages/plugin-roles/dist/index.js")
|
|
4457
|
+
];
|
|
4458
|
+
function normalizeRole(value) {
|
|
4459
|
+
const upper = typeof value === "string" ? value.trim().toUpperCase() : "";
|
|
4460
|
+
switch (upper) {
|
|
4461
|
+
case "OWNER":
|
|
4462
|
+
case "ADMIN":
|
|
4463
|
+
case "USER":
|
|
4464
|
+
return upper;
|
|
4465
|
+
default:
|
|
4466
|
+
return "GUEST";
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
function normalizeConnectorPolicy(value) {
|
|
4470
|
+
if (!value)
|
|
4471
|
+
return {};
|
|
4472
|
+
if (typeof value === "string") {
|
|
4473
|
+
const role = normalizeRole(value);
|
|
4474
|
+
return {
|
|
4475
|
+
create: role,
|
|
4476
|
+
interact: role
|
|
4477
|
+
};
|
|
4478
|
+
}
|
|
4479
|
+
return {
|
|
4480
|
+
...value.create ? { create: normalizeRole(value.create) } : {},
|
|
4481
|
+
...value.interact ? { interact: normalizeRole(value.interact) } : {}
|
|
4482
|
+
};
|
|
4483
|
+
}
|
|
4484
|
+
function parseTaskAgentPolicy(runtime) {
|
|
4485
|
+
if (typeof runtime.getSetting !== "function") {
|
|
4486
|
+
return DEFAULT_POLICY;
|
|
4487
|
+
}
|
|
4488
|
+
const configured = runtime.getSetting("TASK_AGENT_ROLE_POLICY") ?? runtime.getSetting("TASK_AGENT_CONNECTOR_ROLE_POLICY");
|
|
4489
|
+
if (!configured) {
|
|
4490
|
+
return DEFAULT_POLICY;
|
|
4491
|
+
}
|
|
4492
|
+
let parsed = configured;
|
|
4493
|
+
if (typeof configured === "string") {
|
|
4494
|
+
try {
|
|
4495
|
+
parsed = JSON.parse(configured);
|
|
4496
|
+
} catch {
|
|
4497
|
+
return DEFAULT_POLICY;
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4501
|
+
return DEFAULT_POLICY;
|
|
4502
|
+
}
|
|
4503
|
+
const record = parsed;
|
|
4504
|
+
const connectors = record.connectors && typeof record.connectors === "object" && !Array.isArray(record.connectors) ? Object.fromEntries(Object.entries(record.connectors).map(([connector, value]) => [
|
|
4505
|
+
connector,
|
|
4506
|
+
normalizeConnectorPolicy(value)
|
|
4507
|
+
])) : DEFAULT_POLICY.connectors;
|
|
4508
|
+
return {
|
|
4509
|
+
default: normalizeConnectorPolicy(record.default ?? DEFAULT_POLICY.default),
|
|
4510
|
+
connectors
|
|
4511
|
+
};
|
|
4512
|
+
}
|
|
4513
|
+
function getConnectorFromBridgeMetadata(message) {
|
|
4514
|
+
const metadata = message.content?.metadata;
|
|
4515
|
+
if (!metadata || typeof metadata !== "object")
|
|
4516
|
+
return null;
|
|
4517
|
+
const bridgeSender = metadata.bridgeSender;
|
|
4518
|
+
if (!bridgeSender || typeof bridgeSender !== "object")
|
|
4519
|
+
return null;
|
|
4520
|
+
const liveMetadata = bridgeSender.metadata;
|
|
4521
|
+
if (!liveMetadata || typeof liveMetadata !== "object")
|
|
4522
|
+
return null;
|
|
4523
|
+
for (const [connector, value] of Object.entries(liveMetadata)) {
|
|
4524
|
+
if (value && typeof value === "object") {
|
|
4525
|
+
return connector;
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
return null;
|
|
4529
|
+
}
|
|
4530
|
+
async function resolveConnectorSource(runtime, message) {
|
|
4531
|
+
const content = message.content;
|
|
4532
|
+
const directSource = typeof content?.source === "string" && content.source !== "client_chat" ? content.source : null;
|
|
4533
|
+
if (directSource)
|
|
4534
|
+
return directSource;
|
|
4535
|
+
const bridgeSource = getConnectorFromBridgeMetadata(message);
|
|
4536
|
+
if (bridgeSource)
|
|
4537
|
+
return bridgeSource;
|
|
4538
|
+
try {
|
|
4539
|
+
const room = await runtime.getRoom(message.roomId);
|
|
4540
|
+
if (typeof room?.source === "string" && room.source.trim().length > 0) {
|
|
4541
|
+
return room.source;
|
|
4542
|
+
}
|
|
4543
|
+
} catch {}
|
|
4544
|
+
return null;
|
|
4545
|
+
}
|
|
4546
|
+
async function resolveSenderRole(runtime, message) {
|
|
4547
|
+
if (process.env.MILADY_SKIP_LOCAL_PLUGIN_ROLES !== "1") {
|
|
4548
|
+
for (const candidate of LOCAL_ROLES_MODULE_CANDIDATES) {
|
|
4549
|
+
if (!fs.existsSync(candidate)) {
|
|
4550
|
+
continue;
|
|
4551
|
+
}
|
|
4552
|
+
try {
|
|
4553
|
+
const localRolesModule = await import(pathToFileURL(candidate).href);
|
|
4554
|
+
if (typeof localRolesModule.checkSenderRole === "function") {
|
|
4555
|
+
return await localRolesModule.checkSenderRole(runtime, message);
|
|
4556
|
+
}
|
|
4557
|
+
} catch {}
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
try {
|
|
4561
|
+
const rolesModuleSpecifier = "@miladyai/plugin-roles";
|
|
4562
|
+
const rolesModule = await import(rolesModuleSpecifier);
|
|
4563
|
+
if (typeof rolesModule.checkSenderRole !== "function") {
|
|
4564
|
+
return null;
|
|
4565
|
+
}
|
|
4566
|
+
return await rolesModule.checkSenderRole(runtime, message);
|
|
4567
|
+
} catch {
|
|
4568
|
+
return null;
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
async function requireTaskAgentAccess(runtime, message, ability) {
|
|
4572
|
+
const messageEntityId = typeof message.entityId === "string" && message.entityId.length > 0 ? message.entityId : null;
|
|
4573
|
+
const runtimeAgentId = typeof runtime.agentId === "string" && runtime.agentId.length > 0 ? runtime.agentId : null;
|
|
4574
|
+
if (messageEntityId && runtimeAgentId && messageEntityId === runtimeAgentId) {
|
|
4575
|
+
return {
|
|
4576
|
+
allowed: true,
|
|
4577
|
+
connector: null,
|
|
4578
|
+
requiredRole: "GUEST",
|
|
4579
|
+
actualRole: "OWNER"
|
|
4580
|
+
};
|
|
4581
|
+
}
|
|
4582
|
+
const connector = await resolveConnectorSource(runtime, message);
|
|
4583
|
+
const policy = parseTaskAgentPolicy(runtime);
|
|
4584
|
+
const connectorPolicy = connector ? normalizeConnectorPolicy(policy.connectors?.[connector]) : {};
|
|
4585
|
+
const defaultPolicy = normalizeConnectorPolicy(policy.default);
|
|
4586
|
+
const requiredRole = connectorPolicy[ability] ?? defaultPolicy[ability] ?? "GUEST";
|
|
4587
|
+
if (requiredRole === "GUEST") {
|
|
4588
|
+
return {
|
|
4589
|
+
allowed: true,
|
|
4590
|
+
connector,
|
|
4591
|
+
requiredRole,
|
|
4592
|
+
actualRole: "GUEST"
|
|
4593
|
+
};
|
|
4594
|
+
}
|
|
4595
|
+
const roleCheck = await resolveSenderRole(runtime, message);
|
|
4596
|
+
if (!roleCheck) {
|
|
4597
|
+
return {
|
|
4598
|
+
allowed: false,
|
|
4599
|
+
connector,
|
|
4600
|
+
requiredRole,
|
|
4601
|
+
actualRole: "GUEST",
|
|
4602
|
+
reason: connector === "discord" ? "Task-agent access in Discord requires a verified OWNER or ADMIN role." : "Task-agent access requires a verified role, but role context is unavailable."
|
|
4603
|
+
};
|
|
4604
|
+
}
|
|
4605
|
+
const actualRole = normalizeRole(roleCheck.role);
|
|
4606
|
+
if (ROLE_RANK[actualRole] < ROLE_RANK[requiredRole]) {
|
|
4607
|
+
return {
|
|
4608
|
+
allowed: false,
|
|
4609
|
+
connector,
|
|
4610
|
+
requiredRole,
|
|
4611
|
+
actualRole,
|
|
4612
|
+
reason: connector === "discord" ? `Task-agent access in Discord requires ${requiredRole} or higher. Current role: ${actualRole}.` : `Task-agent access requires ${requiredRole} or higher. Current role: ${actualRole}.`
|
|
4613
|
+
};
|
|
4614
|
+
}
|
|
4615
|
+
return {
|
|
4616
|
+
allowed: true,
|
|
4617
|
+
connector,
|
|
4618
|
+
requiredRole,
|
|
4619
|
+
actualRole
|
|
4620
|
+
};
|
|
4621
|
+
}
|
|
4622
|
+
|
|
3974
4623
|
// src/actions/finalize-workspace.ts
|
|
3975
4624
|
var finalizeWorkspaceAction = {
|
|
3976
4625
|
name: "FINALIZE_WORKSPACE",
|
|
@@ -4009,6 +4658,15 @@ var finalizeWorkspaceAction = {
|
|
|
4009
4658
|
return workspaceService != null;
|
|
4010
4659
|
},
|
|
4011
4660
|
handler: async (runtime, message, state, _options, callback) => {
|
|
4661
|
+
const access = await requireTaskAgentAccess(runtime, message, "interact");
|
|
4662
|
+
if (!access.allowed) {
|
|
4663
|
+
if (callback) {
|
|
4664
|
+
await callback({
|
|
4665
|
+
text: access.reason
|
|
4666
|
+
});
|
|
4667
|
+
}
|
|
4668
|
+
return { success: false, error: "FORBIDDEN", text: access.reason };
|
|
4669
|
+
}
|
|
4012
4670
|
const workspaceService = runtime.getService("CODING_WORKSPACE_SERVICE");
|
|
4013
4671
|
if (!workspaceService) {
|
|
4014
4672
|
if (callback) {
|
|
@@ -4165,7 +4823,7 @@ Automated changes generated by Milady task agent.
|
|
|
4165
4823
|
};
|
|
4166
4824
|
|
|
4167
4825
|
// src/services/pty-service.ts
|
|
4168
|
-
import { appendFile as appendFile2, mkdir as
|
|
4826
|
+
import { appendFile as appendFile2, mkdir as mkdir4, readFile as readFile3, writeFile as writeFile5 } from "node:fs/promises";
|
|
4169
4827
|
import { dirname as dirname2, join as join4 } from "node:path";
|
|
4170
4828
|
import { logger as logger4 } from "@elizaos/core";
|
|
4171
4829
|
import {
|
|
@@ -4186,6 +4844,7 @@ class AgentMetricsTracker {
|
|
|
4186
4844
|
completed: 0,
|
|
4187
4845
|
completedViaFastPath: 0,
|
|
4188
4846
|
completedViaClassifier: 0,
|
|
4847
|
+
completedViaOutputReconcile: 0,
|
|
4189
4848
|
stallCount: 0,
|
|
4190
4849
|
avgCompletionMs: 0,
|
|
4191
4850
|
totalCompletionMs: 0
|
|
@@ -4199,8 +4858,10 @@ class AgentMetricsTracker {
|
|
|
4199
4858
|
m.completed++;
|
|
4200
4859
|
if (method === "fast-path")
|
|
4201
4860
|
m.completedViaFastPath++;
|
|
4202
|
-
else
|
|
4861
|
+
else if (method === "classifier")
|
|
4203
4862
|
m.completedViaClassifier++;
|
|
4863
|
+
else
|
|
4864
|
+
m.completedViaOutputReconcile++;
|
|
4204
4865
|
m.totalCompletionMs += durationMs;
|
|
4205
4866
|
m.avgCompletionMs = Math.round(m.totalCompletionMs / m.completed);
|
|
4206
4867
|
}
|
|
@@ -4220,10 +4881,10 @@ class AgentMetricsTracker {
|
|
|
4220
4881
|
// src/services/config-env.ts
|
|
4221
4882
|
import { readFileSync } from "node:fs";
|
|
4222
4883
|
import * as os from "node:os";
|
|
4223
|
-
import * as
|
|
4884
|
+
import * as path2 from "node:path";
|
|
4224
4885
|
function readConfig() {
|
|
4225
4886
|
try {
|
|
4226
|
-
const configPath =
|
|
4887
|
+
const configPath = path2.join(process.env.MILADY_STATE_DIR ?? process.env.ELIZA_STATE_DIR ?? path2.join(os.homedir(), ".milady"), process.env.ELIZA_NAMESPACE === "milady" || !process.env.ELIZA_NAMESPACE ? "milady.json" : `${process.env.ELIZA_NAMESPACE}.json`);
|
|
4227
4888
|
const raw = readFileSync(configPath, "utf-8");
|
|
4228
4889
|
return JSON.parse(raw);
|
|
4229
4890
|
} catch {
|
|
@@ -4402,7 +5063,10 @@ async function handleGeminiAuth(ctx, sessionId, sendKeysToSession) {
|
|
|
4402
5063
|
|
|
4403
5064
|
// src/services/pty-init.ts
|
|
4404
5065
|
init_ansi_utils();
|
|
5066
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
4405
5067
|
import { createRequire as createRequire2 } from "node:module";
|
|
5068
|
+
import os2 from "node:os";
|
|
5069
|
+
import path3 from "node:path";
|
|
4406
5070
|
import { createAllAdapters } from "coding-agent-adapters";
|
|
4407
5071
|
import {
|
|
4408
5072
|
BunCompatiblePTYManager,
|
|
@@ -4415,13 +5079,44 @@ var resolvedAdapterModule = "coding-agent-adapters";
|
|
|
4415
5079
|
try {
|
|
4416
5080
|
resolvedAdapterModule = _require.resolve("coding-agent-adapters");
|
|
4417
5081
|
} catch {}
|
|
5082
|
+
function resolveNodeWorkerPath() {
|
|
5083
|
+
const explicitCandidates = [
|
|
5084
|
+
process.env.NODE,
|
|
5085
|
+
process.env.NODE_BINARY,
|
|
5086
|
+
"/opt/homebrew/bin/node",
|
|
5087
|
+
"/usr/local/bin/node"
|
|
5088
|
+
].filter((value) => Boolean(value?.trim()));
|
|
5089
|
+
for (const candidate of explicitCandidates) {
|
|
5090
|
+
if (existsSync(candidate)) {
|
|
5091
|
+
return candidate;
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
const nvmVersionsDir = path3.join(os2.homedir(), ".nvm", "versions", "node");
|
|
5095
|
+
if (existsSync(nvmVersionsDir)) {
|
|
5096
|
+
const versions = readdirSync(nvmVersionsDir).filter((entry) => entry.startsWith("v")).sort((a, b) => b.localeCompare(a, undefined, {
|
|
5097
|
+
numeric: true,
|
|
5098
|
+
sensitivity: "base"
|
|
5099
|
+
}));
|
|
5100
|
+
for (const version of versions) {
|
|
5101
|
+
const candidate = path3.join(nvmVersionsDir, version, "bin", "node");
|
|
5102
|
+
if (existsSync(candidate)) {
|
|
5103
|
+
return candidate;
|
|
5104
|
+
}
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
5107
|
+
return "node";
|
|
5108
|
+
}
|
|
4418
5109
|
function forwardReadyAsTaskComplete(ctx, session) {
|
|
4419
5110
|
if (!ctx.hasActiveTask?.(session.id) || !ctx.hasTaskActivity?.(session.id)) {
|
|
4420
5111
|
return;
|
|
4421
5112
|
}
|
|
4422
5113
|
const response = ctx.taskResponseMarkers.has(session.id) ? captureTaskResponse(session.id, ctx.sessionOutputBuffers, ctx.taskResponseMarkers) : "";
|
|
4423
5114
|
ctx.log(`session_ready for active task ${session.id} — forwarding as task_complete (stall classifier path, response: ${response.length} chars)`);
|
|
4424
|
-
ctx.emitEvent(session.id, "task_complete", {
|
|
5115
|
+
ctx.emitEvent(session.id, "task_complete", {
|
|
5116
|
+
session,
|
|
5117
|
+
response,
|
|
5118
|
+
source: "session_ready_forward"
|
|
5119
|
+
});
|
|
4425
5120
|
}
|
|
4426
5121
|
async function initializePTYManager(ctx) {
|
|
4427
5122
|
const usingBunWorker = isBun();
|
|
@@ -4430,6 +5125,7 @@ async function initializePTYManager(ctx) {
|
|
|
4430
5125
|
ctx.log(`Resolved adapter module: ${resolvedAdapterModule}`);
|
|
4431
5126
|
const bunManager = new BunCompatiblePTYManager({
|
|
4432
5127
|
adapterModules: [resolvedAdapterModule],
|
|
5128
|
+
nodePath: resolveNodeWorkerPath(),
|
|
4433
5129
|
stallDetectionEnabled: true,
|
|
4434
5130
|
stallTimeoutMs: 4000,
|
|
4435
5131
|
onStallClassify: async (sessionId, recentOutput, _stallDurationMs) => {
|
|
@@ -4438,40 +5134,62 @@ async function initializePTYManager(ctx) {
|
|
|
4438
5134
|
});
|
|
4439
5135
|
bunManager.on("session_ready", (session) => {
|
|
4440
5136
|
ctx.log(`session_ready event received for ${session.id} (type: ${session.type}, status: ${session.status})`);
|
|
4441
|
-
ctx.emitEvent(session.id, "ready", { session });
|
|
5137
|
+
ctx.emitEvent(session.id, "ready", { session, source: "pty_manager" });
|
|
4442
5138
|
forwardReadyAsTaskComplete(ctx, session);
|
|
4443
5139
|
ctx.markTaskDelivered?.(session.id);
|
|
4444
5140
|
});
|
|
4445
5141
|
bunManager.on("session_exit", (id, code) => {
|
|
4446
|
-
ctx.emitEvent(id, "stopped", {
|
|
5142
|
+
ctx.emitEvent(id, "stopped", {
|
|
5143
|
+
reason: `exit code ${code}`,
|
|
5144
|
+
source: "pty_manager"
|
|
5145
|
+
});
|
|
4447
5146
|
});
|
|
4448
5147
|
bunManager.on("session_error", (id, error) => {
|
|
4449
|
-
ctx.emitEvent(id, "error", { message: error });
|
|
5148
|
+
ctx.emitEvent(id, "error", { message: error, source: "pty_manager" });
|
|
4450
5149
|
});
|
|
4451
5150
|
bunManager.on("blocking_prompt", (session, promptInfo, autoResponded) => {
|
|
4452
5151
|
const info = promptInfo;
|
|
4453
5152
|
ctx.log(`blocking_prompt for ${session.id}: type=${info?.type}, autoResponded=${autoResponded}, prompt="${(info?.prompt ?? "").slice(0, 80)}"`);
|
|
4454
|
-
ctx.emitEvent(session.id, "blocked", {
|
|
5153
|
+
ctx.emitEvent(session.id, "blocked", {
|
|
5154
|
+
promptInfo,
|
|
5155
|
+
autoResponded,
|
|
5156
|
+
source: "pty_manager"
|
|
5157
|
+
});
|
|
4455
5158
|
});
|
|
4456
5159
|
bunManager.on("login_required", (session, instructions, url) => {
|
|
4457
5160
|
if (session.type === "gemini") {
|
|
4458
5161
|
ctx.handleGeminiAuth(session.id);
|
|
4459
5162
|
}
|
|
4460
|
-
ctx.emitEvent(session.id, "login_required", {
|
|
5163
|
+
ctx.emitEvent(session.id, "login_required", {
|
|
5164
|
+
instructions,
|
|
5165
|
+
url,
|
|
5166
|
+
source: "pty_manager"
|
|
5167
|
+
});
|
|
4461
5168
|
});
|
|
4462
5169
|
bunManager.on("task_complete", (session) => {
|
|
4463
5170
|
const response = captureTaskResponse(session.id, ctx.sessionOutputBuffers, ctx.taskResponseMarkers);
|
|
4464
5171
|
const durationMs = session.startedAt ? Date.now() - new Date(session.startedAt).getTime() : 0;
|
|
4465
5172
|
ctx.metricsTracker.recordCompletion(session.type, "fast-path", durationMs);
|
|
4466
5173
|
ctx.log(`Task complete for ${session.id} (adapter fast-path), response: ${response.length} chars`);
|
|
4467
|
-
ctx.emitEvent(session.id, "task_complete", {
|
|
5174
|
+
ctx.emitEvent(session.id, "task_complete", {
|
|
5175
|
+
session,
|
|
5176
|
+
response,
|
|
5177
|
+
source: "adapter_fast_path"
|
|
5178
|
+
});
|
|
4468
5179
|
});
|
|
4469
5180
|
bunManager.on("tool_running", (session, info) => {
|
|
4470
5181
|
ctx.log(`tool_running for ${session.id}: ${info.toolName}${info.description ? ` — ${info.description}` : ""}`);
|
|
4471
|
-
ctx.emitEvent(session.id, "tool_running", {
|
|
5182
|
+
ctx.emitEvent(session.id, "tool_running", {
|
|
5183
|
+
session,
|
|
5184
|
+
...info,
|
|
5185
|
+
source: "pty_manager"
|
|
5186
|
+
});
|
|
4472
5187
|
});
|
|
4473
5188
|
bunManager.on("message", (message) => {
|
|
4474
|
-
ctx.emitEvent(message.sessionId, "message",
|
|
5189
|
+
ctx.emitEvent(message.sessionId, "message", {
|
|
5190
|
+
...message,
|
|
5191
|
+
source: "pty_manager"
|
|
5192
|
+
});
|
|
4475
5193
|
});
|
|
4476
5194
|
bunManager.on("worker_error", (err) => {
|
|
4477
5195
|
const raw = typeof err === "string" ? err : String(err);
|
|
@@ -4519,38 +5237,60 @@ async function initializePTYManager(ctx) {
|
|
|
4519
5237
|
}
|
|
4520
5238
|
}
|
|
4521
5239
|
nodeManager.on("session_ready", (session) => {
|
|
4522
|
-
ctx.emitEvent(session.id, "ready", { session });
|
|
5240
|
+
ctx.emitEvent(session.id, "ready", { session, source: "pty_manager" });
|
|
4523
5241
|
forwardReadyAsTaskComplete(ctx, session);
|
|
4524
5242
|
ctx.markTaskDelivered?.(session.id);
|
|
4525
5243
|
});
|
|
4526
5244
|
nodeManager.on("blocking_prompt", (session, promptInfo, autoResponded) => {
|
|
4527
|
-
ctx.emitEvent(session.id, "blocked", {
|
|
5245
|
+
ctx.emitEvent(session.id, "blocked", {
|
|
5246
|
+
promptInfo,
|
|
5247
|
+
autoResponded,
|
|
5248
|
+
source: "pty_manager"
|
|
5249
|
+
});
|
|
4528
5250
|
});
|
|
4529
5251
|
nodeManager.on("login_required", (session, instructions, url) => {
|
|
4530
5252
|
if (session.type === "gemini") {
|
|
4531
5253
|
ctx.handleGeminiAuth(session.id);
|
|
4532
5254
|
}
|
|
4533
|
-
ctx.emitEvent(session.id, "login_required", {
|
|
5255
|
+
ctx.emitEvent(session.id, "login_required", {
|
|
5256
|
+
instructions,
|
|
5257
|
+
url,
|
|
5258
|
+
source: "pty_manager"
|
|
5259
|
+
});
|
|
4534
5260
|
});
|
|
4535
5261
|
nodeManager.on("task_complete", (session) => {
|
|
4536
5262
|
const response = captureTaskResponse(session.id, ctx.sessionOutputBuffers, ctx.taskResponseMarkers);
|
|
4537
5263
|
const durationMs = session.startedAt ? Date.now() - new Date(session.startedAt).getTime() : 0;
|
|
4538
5264
|
ctx.metricsTracker.recordCompletion(session.type, "fast-path", durationMs);
|
|
4539
5265
|
ctx.log(`Task complete for ${session.id} (adapter fast-path), response: ${response.length} chars`);
|
|
4540
|
-
ctx.emitEvent(session.id, "task_complete", {
|
|
5266
|
+
ctx.emitEvent(session.id, "task_complete", {
|
|
5267
|
+
session,
|
|
5268
|
+
response,
|
|
5269
|
+
source: "adapter_fast_path"
|
|
5270
|
+
});
|
|
4541
5271
|
});
|
|
4542
5272
|
nodeManager.on("tool_running", (session, info) => {
|
|
4543
5273
|
ctx.log(`tool_running for ${session.id}: ${info.toolName}${info.description ? ` — ${info.description}` : ""}`);
|
|
4544
|
-
ctx.emitEvent(session.id, "tool_running", {
|
|
5274
|
+
ctx.emitEvent(session.id, "tool_running", {
|
|
5275
|
+
session,
|
|
5276
|
+
...info,
|
|
5277
|
+
source: "pty_manager"
|
|
5278
|
+
});
|
|
4545
5279
|
});
|
|
4546
5280
|
nodeManager.on("session_stopped", (session, reason) => {
|
|
4547
|
-
ctx.emitEvent(session.id, "stopped", { reason });
|
|
5281
|
+
ctx.emitEvent(session.id, "stopped", { reason, source: "pty_manager" });
|
|
4548
5282
|
});
|
|
4549
5283
|
nodeManager.on("session_error", (session, error) => {
|
|
4550
|
-
ctx.emitEvent(session.id, "error", {
|
|
5284
|
+
ctx.emitEvent(session.id, "error", {
|
|
5285
|
+
message: error,
|
|
5286
|
+
source: "pty_manager"
|
|
5287
|
+
});
|
|
4551
5288
|
});
|
|
4552
5289
|
nodeManager.on("message", (message) => {
|
|
4553
|
-
ctx.emitEvent(message.sessionId, "message",
|
|
5290
|
+
ctx.emitEvent(message.sessionId, "message", {
|
|
5291
|
+
...message,
|
|
5292
|
+
source: "pty_manager"
|
|
5293
|
+
});
|
|
4554
5294
|
});
|
|
4555
5295
|
return { manager: nodeManager, usingBunWorker: false };
|
|
4556
5296
|
}
|
|
@@ -4681,7 +5421,11 @@ async function getSessionOutput(ctx, sessionId, lines) {
|
|
|
4681
5421
|
`);
|
|
4682
5422
|
}
|
|
4683
5423
|
|
|
5424
|
+
// src/services/pty-service.ts
|
|
5425
|
+
init_ansi_utils();
|
|
5426
|
+
|
|
4684
5427
|
// src/services/pty-spawn.ts
|
|
5428
|
+
init_ansi_utils();
|
|
4685
5429
|
var ENV_ALLOWLIST = [
|
|
4686
5430
|
"PATH",
|
|
4687
5431
|
"HOME",
|
|
@@ -4696,7 +5440,9 @@ var ENV_ALLOWLIST = [
|
|
|
4696
5440
|
"TMPDIR",
|
|
4697
5441
|
"XDG_RUNTIME_DIR",
|
|
4698
5442
|
"NODE_OPTIONS",
|
|
4699
|
-
"BUN_INSTALL"
|
|
5443
|
+
"BUN_INSTALL",
|
|
5444
|
+
"ANTHROPIC_MODEL",
|
|
5445
|
+
"ANTHROPIC_SMALL_FAST_MODEL"
|
|
4700
5446
|
];
|
|
4701
5447
|
function buildSanitizedBaseEnv() {
|
|
4702
5448
|
const env = {};
|
|
@@ -4758,6 +5504,9 @@ function setupDeferredTaskDelivery(ctx, session, task, agentType) {
|
|
|
4758
5504
|
const VERIFY_DELAY_MS = 5000;
|
|
4759
5505
|
const MAX_RETRIES = 2;
|
|
4760
5506
|
const minNewLines = MIN_NEW_LINES_BY_AGENT[agentType] ?? 15;
|
|
5507
|
+
const READY_PROBE_INTERVAL_MS = 500;
|
|
5508
|
+
const isAdapterBackedAgent = agentType === "claude" || agentType === "gemini" || agentType === "codex" || agentType === "aider";
|
|
5509
|
+
const adapter = isAdapterBackedAgent ? ctx.getAdapter(agentType) : null;
|
|
4761
5510
|
const sendTaskWithRetry = (attempt) => {
|
|
4762
5511
|
const buffer = ctx.sessionOutputBuffers.get(sid);
|
|
4763
5512
|
const baselineLength = buffer?.length ?? 0;
|
|
@@ -4767,7 +5516,10 @@ function setupDeferredTaskDelivery(ctx, session, task, agentType) {
|
|
|
4767
5516
|
setTimeout(() => {
|
|
4768
5517
|
const currentLength = buffer?.length ?? 0;
|
|
4769
5518
|
const newLines = currentLength - baselineLength;
|
|
4770
|
-
|
|
5519
|
+
const newOutput = buffer?.slice(baselineLength).join(`
|
|
5520
|
+
`) ?? "";
|
|
5521
|
+
const accepted = newLines > 0 || newLines >= minNewLines || (adapter?.detectLoading?.(newOutput) ?? false) || cleanForChat(newOutput).length >= 32;
|
|
5522
|
+
if (!accepted) {
|
|
4771
5523
|
ctx.log(`Session ${sid} — task may not have been accepted (only ${newLines} new lines after ${VERIFY_DELAY_MS}ms). Retrying (attempt ${attempt + 2}/${MAX_RETRIES + 1})`);
|
|
4772
5524
|
sendTaskWithRetry(attempt + 1);
|
|
4773
5525
|
} else {
|
|
@@ -4778,15 +5530,31 @@ function setupDeferredTaskDelivery(ctx, session, task, agentType) {
|
|
|
4778
5530
|
};
|
|
4779
5531
|
const READY_TIMEOUT_MS = 30000;
|
|
4780
5532
|
let taskSent = false;
|
|
5533
|
+
let taskDeliveredMarked = false;
|
|
4781
5534
|
let readyTimeout;
|
|
5535
|
+
let readyProbe;
|
|
5536
|
+
const clearPendingReadyWait = () => {
|
|
5537
|
+
if (readyTimeout) {
|
|
5538
|
+
clearTimeout(readyTimeout);
|
|
5539
|
+
readyTimeout = undefined;
|
|
5540
|
+
}
|
|
5541
|
+
if (readyProbe) {
|
|
5542
|
+
clearInterval(readyProbe);
|
|
5543
|
+
readyProbe = undefined;
|
|
5544
|
+
}
|
|
5545
|
+
};
|
|
4782
5546
|
const sendTask = () => {
|
|
4783
5547
|
if (taskSent)
|
|
4784
5548
|
return;
|
|
4785
5549
|
taskSent = true;
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
5550
|
+
clearPendingReadyWait();
|
|
5551
|
+
setTimeout(() => {
|
|
5552
|
+
if (!taskDeliveredMarked) {
|
|
5553
|
+
ctx.markTaskDelivered(sid);
|
|
5554
|
+
taskDeliveredMarked = true;
|
|
5555
|
+
}
|
|
5556
|
+
sendTaskWithRetry(0);
|
|
5557
|
+
}, settleMs);
|
|
4790
5558
|
if (ctx.usingBunWorker) {
|
|
4791
5559
|
ctx.manager.removeListener("session_ready", onReady);
|
|
4792
5560
|
} else {
|
|
@@ -4812,6 +5580,29 @@ function setupDeferredTaskDelivery(ctx, session, task, agentType) {
|
|
|
4812
5580
|
sendTask();
|
|
4813
5581
|
}
|
|
4814
5582
|
}, READY_TIMEOUT_MS);
|
|
5583
|
+
if (ctx.usingBunWorker && isAdapterBackedAgent && adapter) {
|
|
5584
|
+
readyProbe = setInterval(() => {
|
|
5585
|
+
if (taskSent)
|
|
5586
|
+
return;
|
|
5587
|
+
const buffer = ctx.sessionOutputBuffers.get(sid);
|
|
5588
|
+
if (!buffer || buffer.length === 0)
|
|
5589
|
+
return;
|
|
5590
|
+
const output = buffer.join(`
|
|
5591
|
+
`);
|
|
5592
|
+
const cleanedOutput = cleanForChat(output);
|
|
5593
|
+
if (adapter.detectLoading?.(output))
|
|
5594
|
+
return;
|
|
5595
|
+
if (adapter.detectLogin(output).required)
|
|
5596
|
+
return;
|
|
5597
|
+
if (adapter.detectBlockingPrompt(output).detected)
|
|
5598
|
+
return;
|
|
5599
|
+
const promptVisible = adapter.detectReady(output) || agentType === "codex" && /›\s+(?:Ask Codex to do anything|\S.*)/.test(cleanedOutput);
|
|
5600
|
+
if (!promptVisible)
|
|
5601
|
+
return;
|
|
5602
|
+
ctx.log(`Session ${sid} — detected ready prompt from buffered output, delivering task before timeout`);
|
|
5603
|
+
sendTask();
|
|
5604
|
+
}, READY_PROBE_INTERVAL_MS);
|
|
5605
|
+
}
|
|
4815
5606
|
}
|
|
4816
5607
|
}
|
|
4817
5608
|
function buildSpawnConfig(sessionId, options, workdir) {
|
|
@@ -4900,6 +5691,71 @@ import {
|
|
|
4900
5691
|
buildTaskCompletionTimeline,
|
|
4901
5692
|
extractTaskCompletionTraceRecords
|
|
4902
5693
|
} from "pty-manager";
|
|
5694
|
+
var STATUS_NOISE_LINE = /messages to be submitted after next tool call|working \(\d+s .*esc to interrupt\)|\b\d+% left\b|context left|use \/skills to list available skills/i;
|
|
5695
|
+
var STATUS_PATH_LINE = /(\/private\/|\/var\/folders\/|\/Users\/|\/tmp\/)/;
|
|
5696
|
+
var SPINNER_FRAGMENT_TOKEN = /^(?:w|wo|wor|work|worki|workin|working|orking|rking|king|ing|ng|g|\d+|[•·])$/i;
|
|
5697
|
+
function normalizeForComparison(value) {
|
|
5698
|
+
return stripAnsi(value).replace(/\s+/g, " ").trim().toLowerCase();
|
|
5699
|
+
}
|
|
5700
|
+
function looksLikeSpinnerFragments(line) {
|
|
5701
|
+
const tokens = line.replace(/[^\w/%@.:\-/ ]+/g, " ").split(/\s+/).filter(Boolean);
|
|
5702
|
+
if (tokens.length === 0)
|
|
5703
|
+
return false;
|
|
5704
|
+
const fragmentTokens = tokens.filter((token) => SPINNER_FRAGMENT_TOKEN.test(token));
|
|
5705
|
+
return fragmentTokens.length >= 4 && fragmentTokens.length >= Math.ceil(tokens.length * 0.6);
|
|
5706
|
+
}
|
|
5707
|
+
function isStatusNoiseLine(line) {
|
|
5708
|
+
const compact = line.replace(/\s+/g, " ").trim();
|
|
5709
|
+
if (!compact)
|
|
5710
|
+
return true;
|
|
5711
|
+
if (compact.startsWith("› "))
|
|
5712
|
+
return true;
|
|
5713
|
+
if (STATUS_NOISE_LINE.test(compact))
|
|
5714
|
+
return true;
|
|
5715
|
+
if (looksLikeSpinnerFragments(compact))
|
|
5716
|
+
return true;
|
|
5717
|
+
if (STATUS_PATH_LINE.test(compact) && /\b\d+% left\b/i.test(compact))
|
|
5718
|
+
return true;
|
|
5719
|
+
if (STATUS_PATH_LINE.test(compact) && looksLikeSpinnerFragments(compact))
|
|
5720
|
+
return true;
|
|
5721
|
+
return false;
|
|
5722
|
+
}
|
|
5723
|
+
function sanitizeOutputForClassification(output, lastSentInput) {
|
|
5724
|
+
const normalizedInput = lastSentInput ? normalizeForComparison(lastSentInput) : "";
|
|
5725
|
+
let removedEchoLines = 0;
|
|
5726
|
+
let removedStatusLines = 0;
|
|
5727
|
+
const sanitized = stripAnsi(output).split(`
|
|
5728
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter((line) => {
|
|
5729
|
+
if (!line)
|
|
5730
|
+
return false;
|
|
5731
|
+
const normalizedLine = line.toLowerCase();
|
|
5732
|
+
if (normalizedInput && normalizedLine.length >= 12 && normalizedInput.includes(normalizedLine)) {
|
|
5733
|
+
removedEchoLines += 1;
|
|
5734
|
+
return false;
|
|
5735
|
+
}
|
|
5736
|
+
if (isStatusNoiseLine(line)) {
|
|
5737
|
+
removedStatusLines += 1;
|
|
5738
|
+
return false;
|
|
5739
|
+
}
|
|
5740
|
+
return true;
|
|
5741
|
+
}).join(`
|
|
5742
|
+
`).trim();
|
|
5743
|
+
return { sanitized, removedEchoLines, removedStatusLines };
|
|
5744
|
+
}
|
|
5745
|
+
function promptLooksLikeFalseBlockedNoise(prompt, lastSentInput) {
|
|
5746
|
+
if (!prompt)
|
|
5747
|
+
return false;
|
|
5748
|
+
const normalizedPrompt = normalizeForComparison(prompt);
|
|
5749
|
+
if (!normalizedPrompt)
|
|
5750
|
+
return false;
|
|
5751
|
+
if (lastSentInput) {
|
|
5752
|
+
const normalizedInput = normalizeForComparison(lastSentInput);
|
|
5753
|
+
if (normalizedPrompt.length >= 12 && normalizedInput.includes(normalizedPrompt)) {
|
|
5754
|
+
return true;
|
|
5755
|
+
}
|
|
5756
|
+
}
|
|
5757
|
+
return isStatusNoiseLine(prompt) || looksLikeSpinnerFragments(prompt);
|
|
5758
|
+
}
|
|
4903
5759
|
function buildStallClassificationPrompt(agentType, sessionId, output) {
|
|
4904
5760
|
return `You are Milady, an AI orchestrator managing task-agent sessions. ` + `A ${agentType} task agent (session: ${sessionId}) appears to have stalled — ` + `it has stopped producing output while in a busy state.
|
|
4905
5761
|
|
|
@@ -4920,7 +5776,7 @@ ${output.slice(-1500)}
|
|
|
4920
5776
|
|
|
4921
5777
|
` + `5. "tool_running" — The agent is using an external tool (browser automation, ` + `MCP tool, etc.). Indicators: "Claude in Chrome", "javascript_tool", ` + `"computer_tool", "screenshot", "navigate", tool execution output. ` + `The agent is actively working but the terminal may be quiet.
|
|
4922
5778
|
|
|
4923
|
-
` + `IMPORTANT: If you see BOTH completed work output AND an idle prompt (❯), choose "task_complete". ` + `Only choose "waiting_for_input" if the agent is clearly asking a question mid-task.
|
|
5779
|
+
` + `IMPORTANT: If you see BOTH completed work output AND an idle prompt (❯), choose "task_complete". ` + `Only choose "waiting_for_input" if the agent is clearly asking a question mid-task. ` + `Ignore echoed user input, copied prior transcripts, spinner fragments, and status rows like ` + `"Working (12s • esc to interrupt)" or "97% left" — those mean the agent is still working, not blocked.
|
|
4924
5780
|
|
|
4925
5781
|
` + `If "waiting_for_input", also provide:
|
|
4926
5782
|
` + `- "prompt": the text of what it's asking
|
|
@@ -4931,11 +5787,11 @@ ${output.slice(-1500)}
|
|
|
4931
5787
|
}
|
|
4932
5788
|
async function writeStallSnapshot(sessionId, agentType, recentOutput, effectiveOutput, buffers, traceEntries, log) {
|
|
4933
5789
|
try {
|
|
4934
|
-
const
|
|
4935
|
-
const
|
|
4936
|
-
const
|
|
4937
|
-
const snapshotDir =
|
|
4938
|
-
|
|
5790
|
+
const fs2 = await import("node:fs");
|
|
5791
|
+
const os3 = await import("node:os");
|
|
5792
|
+
const path4 = await import("node:path");
|
|
5793
|
+
const snapshotDir = path4.join(os3.homedir(), ".milady", "debug");
|
|
5794
|
+
fs2.mkdirSync(snapshotDir, { recursive: true });
|
|
4939
5795
|
const ourBuffer = buffers.get(sessionId);
|
|
4940
5796
|
const ourTail = ourBuffer ? ourBuffer.slice(-100).join(`
|
|
4941
5797
|
`) : "(no buffer)";
|
|
@@ -4966,8 +5822,8 @@ async function writeStallSnapshot(sessionId, agentType, recentOutput, effectiveO
|
|
|
4966
5822
|
``
|
|
4967
5823
|
].join(`
|
|
4968
5824
|
`);
|
|
4969
|
-
const snapshotPath =
|
|
4970
|
-
|
|
5825
|
+
const snapshotPath = path4.join(snapshotDir, `stall-snapshot-${sessionId}.txt`);
|
|
5826
|
+
fs2.writeFileSync(snapshotPath, snapshot);
|
|
4971
5827
|
log(`Stall snapshot → ${snapshotPath}`);
|
|
4972
5828
|
} catch (_) {}
|
|
4973
5829
|
}
|
|
@@ -4997,7 +5853,19 @@ async function classifyStallOutput(ctx) {
|
|
|
4997
5853
|
}
|
|
4998
5854
|
}
|
|
4999
5855
|
}
|
|
5000
|
-
const
|
|
5856
|
+
const {
|
|
5857
|
+
sanitized: sanitizedOutput,
|
|
5858
|
+
removedEchoLines,
|
|
5859
|
+
removedStatusLines
|
|
5860
|
+
} = sanitizeOutputForClassification(effectiveOutput, ctx.lastSentInput);
|
|
5861
|
+
if (removedEchoLines > 0 || removedStatusLines > 0) {
|
|
5862
|
+
log(`Sanitized stall output for ${sessionId}: removed ${removedEchoLines} echoed lines and ${removedStatusLines} status lines`);
|
|
5863
|
+
}
|
|
5864
|
+
if (!sanitizedOutput && removedEchoLines + removedStatusLines > 0) {
|
|
5865
|
+
log(`Stall classification short-circuit for ${sessionId}: only echoed input / status noise remained`);
|
|
5866
|
+
return { state: "still_working" };
|
|
5867
|
+
}
|
|
5868
|
+
const systemPrompt = buildStallClassificationPrompt(agentType, sessionId, sanitizedOutput || effectiveOutput);
|
|
5001
5869
|
if (ctx.debugSnapshots) {
|
|
5002
5870
|
await writeStallSnapshot(sessionId, agentType, recentOutput, effectiveOutput, buffers, traceEntries, log);
|
|
5003
5871
|
}
|
|
@@ -5031,6 +5899,10 @@ async function classifyStallOutput(ctx) {
|
|
|
5031
5899
|
prompt: parsed.prompt,
|
|
5032
5900
|
suggestedResponse: parsed.suggestedResponse
|
|
5033
5901
|
};
|
|
5902
|
+
if (classification.state === "waiting_for_input" && promptLooksLikeFalseBlockedNoise(classification.prompt, ctx.lastSentInput)) {
|
|
5903
|
+
log(`Stall classification override for ${sessionId}: prompt looked like echoed input / status noise`);
|
|
5904
|
+
return { state: "still_working" };
|
|
5905
|
+
}
|
|
5034
5906
|
log(`Stall classification for ${sessionId}: ${classification.state}${classification.suggestedResponse ? ` → "${classification.suggestedResponse}"` : ""}`);
|
|
5035
5907
|
if (classification.state === "task_complete") {
|
|
5036
5908
|
const session = manager?.get(sessionId);
|
|
@@ -5074,6 +5946,8 @@ Classification states:
|
|
|
5074
5946
|
|
|
5075
5947
|
` + `5. "tool_running" — The agent is using an external tool (browser automation, MCP tool, etc.).
|
|
5076
5948
|
|
|
5949
|
+
` + `Ignore echoed user input, copied prior transcripts, spinner fragments, and status rows like ` + `"Working (12s • esc to interrupt)" or "97% left" — those indicate active work, not a live prompt.
|
|
5950
|
+
|
|
5077
5951
|
` + `If "waiting_for_input", you must also decide how to respond. Guidelines:
|
|
5078
5952
|
- IMPORTANT: If the prompt asks to approve access to files or directories OUTSIDE the working directory (${taskContext.workdir}), DECLINE the request. Respond with "n" and tell the agent: "That path is outside your workspace. Use ${taskContext.workdir} instead."
|
|
5079
5953
|
- For tool approval prompts (file writes, shell commands), respond "y" or use "keys:enter".
|
|
@@ -5113,7 +5987,19 @@ async function classifyAndDecideForCoordinator(ctx) {
|
|
|
5113
5987
|
}
|
|
5114
5988
|
}
|
|
5115
5989
|
}
|
|
5116
|
-
const
|
|
5990
|
+
const {
|
|
5991
|
+
sanitized: sanitizedOutput,
|
|
5992
|
+
removedEchoLines,
|
|
5993
|
+
removedStatusLines
|
|
5994
|
+
} = sanitizeOutputForClassification(effectiveOutput, ctx.lastSentInput);
|
|
5995
|
+
if (removedEchoLines > 0 || removedStatusLines > 0) {
|
|
5996
|
+
log(`Sanitized combined stall output for ${sessionId}: removed ${removedEchoLines} echoed lines and ${removedStatusLines} status lines`);
|
|
5997
|
+
}
|
|
5998
|
+
if (!sanitizedOutput && removedEchoLines + removedStatusLines > 0) {
|
|
5999
|
+
log(`Combined classify+decide short-circuit for ${sessionId}: only echoed input / status noise remained`);
|
|
6000
|
+
return { state: "still_working" };
|
|
6001
|
+
}
|
|
6002
|
+
const systemPrompt = buildCombinedClassifyDecidePrompt(agentType, sessionId, sanitizedOutput || effectiveOutput, taskContext, decisionHistory);
|
|
5117
6003
|
if (ctx.debugSnapshots) {
|
|
5118
6004
|
await writeStallSnapshot(sessionId, agentType, recentOutput, effectiveOutput, buffers, traceEntries, log);
|
|
5119
6005
|
}
|
|
@@ -5161,6 +6047,10 @@ async function classifyAndDecideForCoordinator(ctx) {
|
|
|
5161
6047
|
prompt: parsed.prompt,
|
|
5162
6048
|
suggestedResponse: parsed.suggestedResponse
|
|
5163
6049
|
};
|
|
6050
|
+
if (classification.state === "waiting_for_input" && promptLooksLikeFalseBlockedNoise(classification.prompt, ctx.lastSentInput)) {
|
|
6051
|
+
log(`Combined classify+decide override for ${sessionId}: prompt looked like echoed input / status noise`);
|
|
6052
|
+
return { state: "still_working" };
|
|
6053
|
+
}
|
|
5164
6054
|
log(`Combined classify+decide for ${sessionId}: ${classification.state}${classification.suggestedResponse ? ` → "${classification.suggestedResponse}"` : ""}`);
|
|
5165
6055
|
if (classification.state === "task_complete") {
|
|
5166
6056
|
const session = manager?.get(sessionId);
|
|
@@ -5191,6 +6081,20 @@ function buildCodexCloudProviderToml(baseUrl) {
|
|
|
5191
6081
|
` + `supports_websockets = false
|
|
5192
6082
|
`;
|
|
5193
6083
|
}
|
|
6084
|
+
function compactCredentials(credentials) {
|
|
6085
|
+
return Object.fromEntries(Object.entries(credentials).filter(([, value]) => value !== undefined));
|
|
6086
|
+
}
|
|
6087
|
+
function isAnthropicOAuthToken(value) {
|
|
6088
|
+
return typeof value === "string" && value.startsWith("sk-ant-oat");
|
|
6089
|
+
}
|
|
6090
|
+
function sanitizeCustomCredentials(customCredentials, blockedValues = []) {
|
|
6091
|
+
if (!customCredentials) {
|
|
6092
|
+
return;
|
|
6093
|
+
}
|
|
6094
|
+
const blocked = new Set(blockedValues.filter(Boolean));
|
|
6095
|
+
const filtered = Object.entries(customCredentials).filter(([, value]) => !blocked.has(value));
|
|
6096
|
+
return filtered.length > 0 ? Object.fromEntries(filtered) : undefined;
|
|
6097
|
+
}
|
|
5194
6098
|
function buildAgentCredentials(runtime) {
|
|
5195
6099
|
const llmProvider = readConfigEnvKey("PARALLAX_LLM_PROVIDER") || "subscription";
|
|
5196
6100
|
if (llmProvider === "cloud") {
|
|
@@ -5198,7 +6102,7 @@ function buildAgentCredentials(runtime) {
|
|
|
5198
6102
|
if (!cloudKey) {
|
|
5199
6103
|
throw new Error("Eliza Cloud is selected as the LLM provider but no cloud.apiKey is paired. Pair your account in the Cloud settings section first.");
|
|
5200
6104
|
}
|
|
5201
|
-
const cloudCredentials = {
|
|
6105
|
+
const cloudCredentials = compactCredentials({
|
|
5202
6106
|
anthropicKey: cloudKey,
|
|
5203
6107
|
openaiKey: cloudKey,
|
|
5204
6108
|
googleKey: undefined,
|
|
@@ -5206,28 +6110,163 @@ function buildAgentCredentials(runtime) {
|
|
|
5206
6110
|
openaiBaseUrl: ELIZA_CLOUD_OPENAI_BASE,
|
|
5207
6111
|
githubToken: runtime.getSetting("GITHUB_TOKEN"),
|
|
5208
6112
|
extraConfigToml: buildCodexCloudProviderToml(ELIZA_CLOUD_OPENAI_BASE)
|
|
5209
|
-
};
|
|
6113
|
+
});
|
|
5210
6114
|
return cloudCredentials;
|
|
5211
6115
|
}
|
|
5212
|
-
const
|
|
5213
|
-
|
|
6116
|
+
const subscriptionMode = llmProvider === "subscription";
|
|
6117
|
+
const rawAnthropicKey = runtime.getSetting("ANTHROPIC_API_KEY");
|
|
6118
|
+
const anthropicKey = isAnthropicOAuthToken(rawAnthropicKey) ? undefined : rawAnthropicKey;
|
|
6119
|
+
const directCredentials = compactCredentials({
|
|
6120
|
+
anthropicKey: subscriptionMode ? undefined : anthropicKey,
|
|
5214
6121
|
openaiKey: runtime.getSetting("OPENAI_API_KEY"),
|
|
5215
6122
|
googleKey: runtime.getSetting("GOOGLE_GENERATIVE_AI_API_KEY"),
|
|
5216
6123
|
githubToken: runtime.getSetting("GITHUB_TOKEN"),
|
|
5217
|
-
anthropicBaseUrl: runtime.getSetting("ANTHROPIC_BASE_URL"),
|
|
6124
|
+
anthropicBaseUrl: subscriptionMode ? undefined : anthropicKey ? runtime.getSetting("ANTHROPIC_BASE_URL") : undefined,
|
|
5218
6125
|
openaiBaseUrl: runtime.getSetting("OPENAI_BASE_URL")
|
|
5219
|
-
};
|
|
6126
|
+
});
|
|
5220
6127
|
return directCredentials;
|
|
5221
6128
|
}
|
|
5222
6129
|
|
|
5223
6130
|
// src/services/swarm-coordinator.ts
|
|
5224
6131
|
init_ansi_utils();
|
|
6132
|
+
|
|
6133
|
+
// src/services/coordinator-event-normalizer.ts
|
|
6134
|
+
function normalizeSource(data) {
|
|
6135
|
+
const source = typeof data?.source === "string" ? data.source : "";
|
|
6136
|
+
switch (source) {
|
|
6137
|
+
case "pty_manager":
|
|
6138
|
+
case "adapter_fast_path":
|
|
6139
|
+
case "session_ready_forward":
|
|
6140
|
+
case "hook":
|
|
6141
|
+
return source;
|
|
6142
|
+
default:
|
|
6143
|
+
return "unknown";
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
6146
|
+
function normalizeSessionSnapshot(data) {
|
|
6147
|
+
const session = data?.session;
|
|
6148
|
+
if (!session || typeof session !== "object" || Array.isArray(session)) {
|
|
6149
|
+
return;
|
|
6150
|
+
}
|
|
6151
|
+
const record = session;
|
|
6152
|
+
const id = typeof record.id === "string" ? record.id : undefined;
|
|
6153
|
+
if (!id)
|
|
6154
|
+
return;
|
|
6155
|
+
return {
|
|
6156
|
+
id,
|
|
6157
|
+
...typeof record.type === "string" ? { type: record.type } : {},
|
|
6158
|
+
...typeof record.status === "string" ? { status: record.status } : {}
|
|
6159
|
+
};
|
|
6160
|
+
}
|
|
6161
|
+
function normalizeCoordinatorEvent(sessionId, event, data) {
|
|
6162
|
+
const timestamp = typeof data?.timestamp === "number" ? data.timestamp : Date.now();
|
|
6163
|
+
const source = normalizeSource(data);
|
|
6164
|
+
const session = normalizeSessionSnapshot(data);
|
|
6165
|
+
switch (event) {
|
|
6166
|
+
case "ready":
|
|
6167
|
+
return {
|
|
6168
|
+
sessionId,
|
|
6169
|
+
name: "ready",
|
|
6170
|
+
source,
|
|
6171
|
+
timestamp,
|
|
6172
|
+
rawData: data,
|
|
6173
|
+
...session ? { session } : {}
|
|
6174
|
+
};
|
|
6175
|
+
case "blocked": {
|
|
6176
|
+
const promptInfo = data?.promptInfo;
|
|
6177
|
+
const promptRecord = promptInfo && typeof promptInfo === "object" && !Array.isArray(promptInfo) ? promptInfo : undefined;
|
|
6178
|
+
const promptText = typeof promptRecord?.prompt === "string" && promptRecord.prompt || typeof promptRecord?.instructions === "string" && promptRecord.instructions || "";
|
|
6179
|
+
return {
|
|
6180
|
+
sessionId,
|
|
6181
|
+
name: "blocked",
|
|
6182
|
+
source,
|
|
6183
|
+
timestamp,
|
|
6184
|
+
rawData: data,
|
|
6185
|
+
...session ? { session } : {},
|
|
6186
|
+
promptText,
|
|
6187
|
+
...typeof promptRecord?.type === "string" ? { promptType: promptRecord.type } : {},
|
|
6188
|
+
...promptRecord ? { promptInfo: promptRecord } : {},
|
|
6189
|
+
autoResponded: data?.autoResponded === true
|
|
6190
|
+
};
|
|
6191
|
+
}
|
|
6192
|
+
case "login_required":
|
|
6193
|
+
return {
|
|
6194
|
+
sessionId,
|
|
6195
|
+
name: "login_required",
|
|
6196
|
+
source,
|
|
6197
|
+
timestamp,
|
|
6198
|
+
rawData: data,
|
|
6199
|
+
...session ? { session } : {},
|
|
6200
|
+
...typeof data?.instructions === "string" ? {
|
|
6201
|
+
instructions: data.instructions
|
|
6202
|
+
} : {},
|
|
6203
|
+
...typeof data?.url === "string" ? {
|
|
6204
|
+
url: data.url
|
|
6205
|
+
} : {}
|
|
6206
|
+
};
|
|
6207
|
+
case "task_complete":
|
|
6208
|
+
return {
|
|
6209
|
+
sessionId,
|
|
6210
|
+
name: "task_complete",
|
|
6211
|
+
source,
|
|
6212
|
+
timestamp,
|
|
6213
|
+
rawData: data,
|
|
6214
|
+
...session ? { session } : {},
|
|
6215
|
+
response: typeof data?.response === "string" ? data.response : ""
|
|
6216
|
+
};
|
|
6217
|
+
case "tool_running":
|
|
6218
|
+
return {
|
|
6219
|
+
sessionId,
|
|
6220
|
+
name: "tool_running",
|
|
6221
|
+
source,
|
|
6222
|
+
timestamp,
|
|
6223
|
+
rawData: data,
|
|
6224
|
+
...session ? { session } : {},
|
|
6225
|
+
...typeof data?.toolName === "string" ? { toolName: data.toolName } : {},
|
|
6226
|
+
...typeof data?.description === "string" ? { description: data.description } : {}
|
|
6227
|
+
};
|
|
6228
|
+
case "stopped":
|
|
6229
|
+
return {
|
|
6230
|
+
sessionId,
|
|
6231
|
+
name: "stopped",
|
|
6232
|
+
source,
|
|
6233
|
+
timestamp,
|
|
6234
|
+
rawData: data,
|
|
6235
|
+
...session ? { session } : {},
|
|
6236
|
+
...typeof data?.reason === "string" ? { reason: data.reason } : {}
|
|
6237
|
+
};
|
|
6238
|
+
case "error":
|
|
6239
|
+
return {
|
|
6240
|
+
sessionId,
|
|
6241
|
+
name: "error",
|
|
6242
|
+
source,
|
|
6243
|
+
timestamp,
|
|
6244
|
+
rawData: data,
|
|
6245
|
+
...session ? { session } : {},
|
|
6246
|
+
message: typeof data?.message === "string" ? data.message : "unknown error"
|
|
6247
|
+
};
|
|
6248
|
+
case "message":
|
|
6249
|
+
return {
|
|
6250
|
+
sessionId,
|
|
6251
|
+
name: "message",
|
|
6252
|
+
source,
|
|
6253
|
+
timestamp,
|
|
6254
|
+
rawData: data,
|
|
6255
|
+
...session ? { session } : {},
|
|
6256
|
+
...typeof data?.content === "string" ? { content: data.content } : {}
|
|
6257
|
+
};
|
|
6258
|
+
default:
|
|
6259
|
+
return null;
|
|
6260
|
+
}
|
|
6261
|
+
}
|
|
6262
|
+
|
|
6263
|
+
// src/services/swarm-coordinator.ts
|
|
5225
6264
|
init_swarm_decision_loop();
|
|
5226
6265
|
|
|
5227
6266
|
// src/services/swarm-history.ts
|
|
5228
|
-
import * as
|
|
5229
|
-
import * as
|
|
5230
|
-
import * as
|
|
6267
|
+
import * as fs2 from "fs/promises";
|
|
6268
|
+
import * as os3 from "os";
|
|
6269
|
+
import * as path7 from "path";
|
|
5231
6270
|
var MAX_ENTRIES = 150;
|
|
5232
6271
|
var TRUNCATE_TO = 100;
|
|
5233
6272
|
var MAX_FILE_SIZE_BYTES = 1048576;
|
|
@@ -5259,26 +6298,26 @@ class SwarmHistory {
|
|
|
5259
6298
|
appendCount = 0;
|
|
5260
6299
|
mutex = new WriteMutex;
|
|
5261
6300
|
constructor(stateDir) {
|
|
5262
|
-
const dir = stateDir || process.env.MILADY_STATE_DIR || process.env.ELIZA_STATE_DIR ||
|
|
5263
|
-
this.filePath =
|
|
6301
|
+
const dir = stateDir || process.env.MILADY_STATE_DIR || process.env.ELIZA_STATE_DIR || path7.join(os3.homedir(), ".milady");
|
|
6302
|
+
this.filePath = path7.join(dir, "swarm-history.jsonl");
|
|
5264
6303
|
}
|
|
5265
6304
|
async append(entry) {
|
|
5266
6305
|
await this.mutex.acquire();
|
|
5267
6306
|
try {
|
|
5268
|
-
const dir =
|
|
5269
|
-
await
|
|
5270
|
-
await
|
|
6307
|
+
const dir = path7.dirname(this.filePath);
|
|
6308
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
6309
|
+
await fs2.appendFile(this.filePath, `${JSON.stringify(entry)}
|
|
5271
6310
|
`, "utf-8");
|
|
5272
6311
|
this.appendCount++;
|
|
5273
6312
|
try {
|
|
5274
|
-
const stat3 = await
|
|
6313
|
+
const stat3 = await fs2.stat(this.filePath);
|
|
5275
6314
|
if (stat3.size > MAX_FILE_SIZE_BYTES) {
|
|
5276
6315
|
await this.truncateInner(TRUNCATE_TO);
|
|
5277
6316
|
return;
|
|
5278
6317
|
}
|
|
5279
6318
|
} catch {}
|
|
5280
6319
|
if (this.appendCount >= MAX_ENTRIES - TRUNCATE_TO) {
|
|
5281
|
-
const content = await
|
|
6320
|
+
const content = await fs2.readFile(this.filePath, "utf-8");
|
|
5282
6321
|
const lineCount = content.split(`
|
|
5283
6322
|
`).filter((l) => l.trim() !== "").length;
|
|
5284
6323
|
if (lineCount > MAX_ENTRIES) {
|
|
@@ -5294,7 +6333,7 @@ class SwarmHistory {
|
|
|
5294
6333
|
}
|
|
5295
6334
|
async readAll() {
|
|
5296
6335
|
try {
|
|
5297
|
-
const content = await
|
|
6336
|
+
const content = await fs2.readFile(this.filePath, "utf-8");
|
|
5298
6337
|
const entries = [];
|
|
5299
6338
|
const lines = content.split(`
|
|
5300
6339
|
`);
|
|
@@ -5329,7 +6368,7 @@ class SwarmHistory {
|
|
|
5329
6368
|
const entries = await this.readAll();
|
|
5330
6369
|
if (entries.length === 0) {
|
|
5331
6370
|
try {
|
|
5332
|
-
await
|
|
6371
|
+
await fs2.stat(this.filePath);
|
|
5333
6372
|
console.error("[swarm-history] truncate aborted: file exists but readAll returned empty");
|
|
5334
6373
|
return;
|
|
5335
6374
|
} catch {
|
|
@@ -5346,7 +6385,7 @@ class SwarmHistory {
|
|
|
5346
6385
|
`) + `
|
|
5347
6386
|
`;
|
|
5348
6387
|
}
|
|
5349
|
-
await
|
|
6388
|
+
await fs2.writeFile(this.filePath, content, "utf-8");
|
|
5350
6389
|
this.appendCount = 0;
|
|
5351
6390
|
}
|
|
5352
6391
|
}
|
|
@@ -5354,7 +6393,7 @@ class SwarmHistory {
|
|
|
5354
6393
|
// src/services/swarm-idle-watchdog.ts
|
|
5355
6394
|
init_ansi_utils();
|
|
5356
6395
|
init_swarm_decision_loop();
|
|
5357
|
-
import { ModelType as
|
|
6396
|
+
import { ModelType as ModelType6 } from "@elizaos/core";
|
|
5358
6397
|
var IDLE_THRESHOLD_MS = 5 * 60 * 1000;
|
|
5359
6398
|
var MAX_IDLE_CHECKS = 4;
|
|
5360
6399
|
async function scanIdleSessions(ctx) {
|
|
@@ -5510,7 +6549,7 @@ async function handleIdleCheck(ctx, taskCtx, idleMinutes) {
|
|
|
5510
6549
|
repo: taskCtx.repo,
|
|
5511
6550
|
workdir: taskCtx.workdir,
|
|
5512
6551
|
originalTask: taskCtx.originalTask
|
|
5513
|
-
}, () => ctx.runtime.useModel(
|
|
6552
|
+
}, () => ctx.runtime.useModel(ModelType6.TEXT_SMALL, { prompt }));
|
|
5514
6553
|
decision = parseCoordinationResponse(result);
|
|
5515
6554
|
} catch (err) {
|
|
5516
6555
|
ctx.log(`Idle check LLM call failed: ${err}`);
|
|
@@ -5554,7 +6593,7 @@ async function handleIdleCheck(ctx, taskCtx, idleMinutes) {
|
|
|
5554
6593
|
}
|
|
5555
6594
|
|
|
5556
6595
|
// src/services/task-acceptance.ts
|
|
5557
|
-
import { ModelType as
|
|
6596
|
+
import { ModelType as ModelType7 } from "@elizaos/core";
|
|
5558
6597
|
var MAX_CRITERIA = 7;
|
|
5559
6598
|
function trimCriterion(value) {
|
|
5560
6599
|
return value.replace(/^[\s*-]+/, "").replace(/\s+/g, " ").trim();
|
|
@@ -5641,7 +6680,7 @@ async function deriveTaskAcceptanceCriteria(runtime, input) {
|
|
|
5641
6680
|
};
|
|
5642
6681
|
}
|
|
5643
6682
|
try {
|
|
5644
|
-
const raw = await runtime.useModel(
|
|
6683
|
+
const raw = await runtime.useModel(ModelType7.TEXT_SMALL, {
|
|
5645
6684
|
prompt: buildAcceptancePrompt(input),
|
|
5646
6685
|
temperature: 0.1,
|
|
5647
6686
|
stream: false
|
|
@@ -5664,9 +6703,69 @@ async function deriveTaskAcceptanceCriteria(runtime, input) {
|
|
|
5664
6703
|
|
|
5665
6704
|
// src/services/task-agent-frameworks.ts
|
|
5666
6705
|
import { execFileSync } from "node:child_process";
|
|
5667
|
-
import
|
|
5668
|
-
import
|
|
5669
|
-
import
|
|
6706
|
+
import fs3 from "node:fs";
|
|
6707
|
+
import os4 from "node:os";
|
|
6708
|
+
import path8 from "node:path";
|
|
6709
|
+
var RESEARCH_SIGNAL_RE = /\b(research|investigate|analy[sz]e|analysis|compare|evaluate|review|study|summari[sz]e|deep research|look into|explore)\b/i;
|
|
6710
|
+
var PLANNING_SIGNAL_RE = /\b(plan|planning|roadmap|strategy|spec|architecture|design|scope|milestone|sequence|timeline)\b/i;
|
|
6711
|
+
var OPS_SIGNAL_RE = /\b(deploy|release|ship|rollback|monitor|incident|infra|infrastructure|configure|setup|docker|kubernetes|ci|cd|runbook)\b/i;
|
|
6712
|
+
var IMPLEMENTATION_SIGNAL_RE = /\b(code|coding|implement|fix|debug|refactor|write|build|patch|feature|server|api|component|function|typescrip?t|javascript|react)\b/i;
|
|
6713
|
+
var VERIFICATION_SIGNAL_RE = /\b(test|tests|verify|validation|prove|acceptance|check|regression|benchmark|lint|typecheck|qa)\b/i;
|
|
6714
|
+
var COORDINATION_SIGNAL_RE = /\b(parallel|delegate|subagent|sub-agent|swarm|coordinate|coordination|handoff|mailbox|scheduler|orchestrate)\b/i;
|
|
6715
|
+
var REPO_SIGNAL_RE = /\b(repo|repository|branch|commit|pull request|pr|diff|workspace|file|directory|codebase)\b/i;
|
|
6716
|
+
var FAST_ITERATION_SIGNAL_RE = /\b(fix|debug|patch|flaky|quick|fast|iterate|loop|unblock|repair)\b/i;
|
|
6717
|
+
var FRAMEWORK_CAPABILITY_PROFILES = {
|
|
6718
|
+
claude: {
|
|
6719
|
+
implementation: 0.95,
|
|
6720
|
+
research: 0.95,
|
|
6721
|
+
planning: 1,
|
|
6722
|
+
ops: 0.8,
|
|
6723
|
+
verification: 0.85,
|
|
6724
|
+
coordination: 1,
|
|
6725
|
+
repoWork: 0.9,
|
|
6726
|
+
fastIteration: 0.75
|
|
6727
|
+
},
|
|
6728
|
+
codex: {
|
|
6729
|
+
implementation: 1,
|
|
6730
|
+
research: 0.8,
|
|
6731
|
+
planning: 0.75,
|
|
6732
|
+
ops: 0.85,
|
|
6733
|
+
verification: 1,
|
|
6734
|
+
coordination: 0.9,
|
|
6735
|
+
repoWork: 1,
|
|
6736
|
+
fastIteration: 0.95
|
|
6737
|
+
},
|
|
6738
|
+
gemini: {
|
|
6739
|
+
implementation: 0.7,
|
|
6740
|
+
research: 1,
|
|
6741
|
+
planning: 0.95,
|
|
6742
|
+
ops: 0.7,
|
|
6743
|
+
verification: 0.6,
|
|
6744
|
+
coordination: 0.7,
|
|
6745
|
+
repoWork: 0.65,
|
|
6746
|
+
fastIteration: 0.7
|
|
6747
|
+
},
|
|
6748
|
+
aider: {
|
|
6749
|
+
implementation: 0.9,
|
|
6750
|
+
research: 0.45,
|
|
6751
|
+
planning: 0.45,
|
|
6752
|
+
ops: 0.75,
|
|
6753
|
+
verification: 0.85,
|
|
6754
|
+
coordination: 0.35,
|
|
6755
|
+
repoWork: 0.95,
|
|
6756
|
+
fastIteration: 1
|
|
6757
|
+
},
|
|
6758
|
+
pi: {
|
|
6759
|
+
implementation: 0.55,
|
|
6760
|
+
research: 0.5,
|
|
6761
|
+
planning: 0.55,
|
|
6762
|
+
ops: 0.5,
|
|
6763
|
+
verification: 0.5,
|
|
6764
|
+
coordination: 0.35,
|
|
6765
|
+
repoWork: 0.5,
|
|
6766
|
+
fastIteration: 0.5
|
|
6767
|
+
}
|
|
6768
|
+
};
|
|
5670
6769
|
var FRAMEWORK_LABELS = {
|
|
5671
6770
|
claude: "Claude Code",
|
|
5672
6771
|
codex: "Codex",
|
|
@@ -5718,11 +6817,11 @@ function safeGetSetting(runtime, key) {
|
|
|
5718
6817
|
}
|
|
5719
6818
|
}
|
|
5720
6819
|
function getUserHomeDir() {
|
|
5721
|
-
return process.env.HOME?.trim() || process.env.USERPROFILE?.trim() ||
|
|
6820
|
+
return process.env.HOME?.trim() || process.env.USERPROFILE?.trim() || os4.homedir();
|
|
5722
6821
|
}
|
|
5723
6822
|
function readJsonFile(filePath) {
|
|
5724
6823
|
try {
|
|
5725
|
-
return JSON.parse(
|
|
6824
|
+
return JSON.parse(fs3.readFileSync(filePath, "utf8"));
|
|
5726
6825
|
} catch {
|
|
5727
6826
|
return null;
|
|
5728
6827
|
}
|
|
@@ -5746,10 +6845,10 @@ function resolveMiladyConfigPath() {
|
|
|
5746
6845
|
const explicit = process.env.MILADY_CONFIG_PATH?.trim() || process.env.ELIZA_CONFIG_PATH?.trim();
|
|
5747
6846
|
if (explicit)
|
|
5748
6847
|
return explicit;
|
|
5749
|
-
const stateDir = process.env.MILADY_STATE_DIR?.trim() || process.env.ELIZA_STATE_DIR?.trim() ||
|
|
6848
|
+
const stateDir = process.env.MILADY_STATE_DIR?.trim() || process.env.ELIZA_STATE_DIR?.trim() || path8.join(getUserHomeDir(), ".milady");
|
|
5750
6849
|
const namespace = process.env.ELIZA_NAMESPACE?.trim();
|
|
5751
6850
|
const filename = !namespace || namespace === "milady" ? "milady.json" : `${namespace}.json`;
|
|
5752
|
-
return
|
|
6851
|
+
return path8.join(stateDir, filename);
|
|
5753
6852
|
}
|
|
5754
6853
|
function readConfiguredSubscriptionProvider() {
|
|
5755
6854
|
const config = readJsonFile(resolveMiladyConfigPath());
|
|
@@ -5765,7 +6864,7 @@ function readConfiguredSubscriptionProvider() {
|
|
|
5765
6864
|
return typeof provider === "string" && provider.trim() ? provider.trim() : undefined;
|
|
5766
6865
|
}
|
|
5767
6866
|
function hasClaudeSubscriptionAuth() {
|
|
5768
|
-
const credentialsPath =
|
|
6867
|
+
const credentialsPath = path8.join(getUserHomeDir(), ".claude", ".credentials.json");
|
|
5769
6868
|
const fileToken = extractOauthAccessToken(readJsonFile(credentialsPath));
|
|
5770
6869
|
if (fileToken)
|
|
5771
6870
|
return true;
|
|
@@ -5784,7 +6883,7 @@ function hasClaudeApiKey(runtime) {
|
|
|
5784
6883
|
return Boolean(process.env.ANTHROPIC_API_KEY?.trim() || safeGetSetting(runtime, "ANTHROPIC_API_KEY"));
|
|
5785
6884
|
}
|
|
5786
6885
|
function hasCodexSubscriptionAuth() {
|
|
5787
|
-
const authPath =
|
|
6886
|
+
const authPath = path8.join(getUserHomeDir(), ".codex", "auth.json");
|
|
5788
6887
|
const auth = readJsonFile(authPath);
|
|
5789
6888
|
if (!auth || typeof auth !== "object" || Array.isArray(auth))
|
|
5790
6889
|
return false;
|
|
@@ -5801,8 +6900,11 @@ function hasElizaCloudApiKey() {
|
|
|
5801
6900
|
return Boolean(readConfigCloudKey("apiKey"));
|
|
5802
6901
|
}
|
|
5803
6902
|
function hasPiBinary() {
|
|
6903
|
+
return hasBinaryOnPath("pi");
|
|
6904
|
+
}
|
|
6905
|
+
function hasBinaryOnPath(binaryName) {
|
|
5804
6906
|
const command = process.platform === "win32" ? "where" : "which";
|
|
5805
|
-
const args =
|
|
6907
|
+
const args = [binaryName];
|
|
5806
6908
|
try {
|
|
5807
6909
|
execFileSync(command, args, {
|
|
5808
6910
|
encoding: "utf8",
|
|
@@ -5814,6 +6916,18 @@ function hasPiBinary() {
|
|
|
5814
6916
|
return false;
|
|
5815
6917
|
}
|
|
5816
6918
|
}
|
|
6919
|
+
function hasFrameworkBinary(id) {
|
|
6920
|
+
switch (id) {
|
|
6921
|
+
case "claude":
|
|
6922
|
+
return hasBinaryOnPath("claude");
|
|
6923
|
+
case "codex":
|
|
6924
|
+
return hasBinaryOnPath("codex");
|
|
6925
|
+
case "gemini":
|
|
6926
|
+
return hasBinaryOnPath("gemini");
|
|
6927
|
+
case "aider":
|
|
6928
|
+
return hasBinaryOnPath("aider");
|
|
6929
|
+
}
|
|
6930
|
+
}
|
|
5817
6931
|
function getFrameworkCooldown(id) {
|
|
5818
6932
|
const cooldown = frameworkCooldowns.get(id);
|
|
5819
6933
|
if (!cooldown)
|
|
@@ -5824,7 +6938,7 @@ function getFrameworkCooldown(id) {
|
|
|
5824
6938
|
}
|
|
5825
6939
|
return cooldown;
|
|
5826
6940
|
}
|
|
5827
|
-
async function computeTaskAgentFrameworkState(runtime, probe) {
|
|
6941
|
+
async function computeTaskAgentFrameworkState(runtime, probe, profileInput) {
|
|
5828
6942
|
const configuredSubscriptionProvider = readConfiguredSubscriptionProvider();
|
|
5829
6943
|
const preflightByAdapter = new Map;
|
|
5830
6944
|
if (probe?.checkAvailableAgents) {
|
|
@@ -5848,10 +6962,10 @@ async function computeTaskAgentFrameworkState(runtime, probe) {
|
|
|
5848
6962
|
const piReady = hasPiBinary();
|
|
5849
6963
|
const providerPrefersClaude = configuredSubscriptionProvider === "anthropic-subscription";
|
|
5850
6964
|
const providerPrefersCodex = configuredSubscriptionProvider === "openai-codex" || configuredSubscriptionProvider === "openai-subscription";
|
|
5851
|
-
const
|
|
6965
|
+
const inventory = STANDARD_FRAMEWORKS.map((id) => {
|
|
5852
6966
|
const preflight = preflightByAdapter.get(id);
|
|
5853
6967
|
const cooldown = getFrameworkCooldown(id);
|
|
5854
|
-
const installed = preflight?.installed === true;
|
|
6968
|
+
const installed = preflight?.installed === true || hasFrameworkBinary(id);
|
|
5855
6969
|
const subscriptionReady = id === "claude" ? claudeSubscriptionReady : id === "codex" ? codexSubscriptionReady : false;
|
|
5856
6970
|
const authReady = id === "claude" ? claudeAuthReady : id === "codex" ? codexAuthReady : id === "gemini" ? geminiAuthReady : claudeAuthReady || codexAuthReady || geminiAuthReady;
|
|
5857
6971
|
const reason = id === "claude" && subscriptionReady ? "ready to use the user's Claude subscription" : id === "codex" && subscriptionReady ? "ready to use the user's OpenAI subscription" : installed ? authReady ? "installed with credentials available" : "installed but credentials were not detected" : "CLI not detected";
|
|
@@ -5870,7 +6984,7 @@ async function computeTaskAgentFrameworkState(runtime, probe) {
|
|
|
5870
6984
|
docsUrl: preflight?.docsUrl
|
|
5871
6985
|
};
|
|
5872
6986
|
});
|
|
5873
|
-
|
|
6987
|
+
inventory.push({
|
|
5874
6988
|
id: "pi",
|
|
5875
6989
|
label: FRAMEWORK_LABELS.pi,
|
|
5876
6990
|
installed: piReady,
|
|
@@ -5880,59 +6994,53 @@ async function computeTaskAgentFrameworkState(runtime, probe) {
|
|
|
5880
6994
|
recommended: false,
|
|
5881
6995
|
reason: piReady ? "CLI detected" : "CLI not detected"
|
|
5882
6996
|
});
|
|
5883
|
-
const
|
|
5884
|
-
|
|
6997
|
+
const frameworks = inventory.map((framework) => ({
|
|
6998
|
+
...framework,
|
|
6999
|
+
recommended: false
|
|
7000
|
+
}));
|
|
7001
|
+
const metrics = probe?.getAgentMetrics?.() ?? {};
|
|
7002
|
+
const profile = buildTaskAgentTaskProfile(profileInput);
|
|
5885
7003
|
const explicitDefault = safeGetSetting(runtime, "PARALLAX_DEFAULT_AGENT_TYPE")?.toLowerCase().trim();
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
reason: "configured OpenAI subscription should drive Codex first"
|
|
5901
|
-
};
|
|
5902
|
-
} else if (byId.get("claude")?.installed && claudeSubscriptionReady && isSelectable("claude")) {
|
|
5903
|
-
preferred = {
|
|
5904
|
-
id: "claude",
|
|
5905
|
-
reason: "Claude Code is installed and the user is logged in"
|
|
5906
|
-
};
|
|
5907
|
-
} else if (byId.get("codex")?.installed && codexSubscriptionReady && isSelectable("codex")) {
|
|
5908
|
-
preferred = {
|
|
5909
|
-
id: "codex",
|
|
5910
|
-
reason: "Codex is installed and the user is logged in"
|
|
5911
|
-
};
|
|
5912
|
-
} else if (byId.get("claude")?.installed && claudeAuthReady && isSelectable("claude")) {
|
|
5913
|
-
preferred = {
|
|
5914
|
-
id: "claude",
|
|
5915
|
-
reason: "Claude Code is installed and credentials are available"
|
|
7004
|
+
const selectable = frameworks.filter((framework) => framework.installed && !framework.temporarilyDisabled);
|
|
7005
|
+
const candidates = selectable.length > 0 ? selectable : frameworks.filter((framework) => framework.installed);
|
|
7006
|
+
const scoredCandidates = candidates.map((framework) => {
|
|
7007
|
+
const explicitOverride = explicitDefault === framework.id ? framework.installed && !framework.temporarilyDisabled ? 40 : 0 : 0;
|
|
7008
|
+
const providerPreference = providerPrefersClaude && framework.id === "claude" ? framework.subscriptionReady ? 18 : 6 : providerPrefersCodex && framework.id === "codex" ? framework.subscriptionReady ? 18 : 6 : 0;
|
|
7009
|
+
const availabilityScore = (framework.installed ? 40 : -100) + (framework.authReady ? 18 : -25) + (framework.subscriptionReady ? 8 : 0) + (framework.temporarilyDisabled ? -80 : 0);
|
|
7010
|
+
const profileScore = computeProfileFitScore(framework.id, profile);
|
|
7011
|
+
const metricsScore = computeMetricsScore(metrics[framework.id], profile.signals.fastIteration);
|
|
7012
|
+
const selectionSignals = {
|
|
7013
|
+
availability: availabilityScore,
|
|
7014
|
+
profile: profileScore,
|
|
7015
|
+
provider: providerPreference,
|
|
7016
|
+
metrics: metricsScore,
|
|
7017
|
+
explicitOverride
|
|
5916
7018
|
};
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
};
|
|
5922
|
-
} else if (byId.get("gemini")?.installed && geminiAuthReady && isSelectable("gemini")) {
|
|
5923
|
-
preferred = {
|
|
5924
|
-
id: "gemini",
|
|
5925
|
-
reason: "Gemini CLI is installed and credentials are available"
|
|
5926
|
-
};
|
|
5927
|
-
} else {
|
|
5928
|
-
const fallback = frameworks.find((framework) => framework.installed && !framework.temporarilyDisabled) ?? frameworks.find((framework) => framework.installed) ?? frameworks[0];
|
|
5929
|
-
preferred = {
|
|
5930
|
-
id: fallback.id,
|
|
5931
|
-
reason: fallback.installed ? "best available installed task-agent framework" : "default fallback while no task-agent CLI is installed"
|
|
7019
|
+
return {
|
|
7020
|
+
framework,
|
|
7021
|
+
score: Object.values(selectionSignals).reduce((sum, value) => sum + value, 0),
|
|
7022
|
+
selectionSignals
|
|
5932
7023
|
};
|
|
5933
|
-
}
|
|
7024
|
+
});
|
|
7025
|
+
const fallback = candidates[0] ?? frameworks.find((framework) => framework.installed) ?? frameworks[0];
|
|
7026
|
+
const preferredCandidate = scoredCandidates.sort((left, right) => {
|
|
7027
|
+
if (right.score !== left.score) {
|
|
7028
|
+
return right.score - left.score;
|
|
7029
|
+
}
|
|
7030
|
+
return left.framework.id.localeCompare(right.framework.id);
|
|
7031
|
+
})[0]?.framework ?? fallback;
|
|
7032
|
+
const preferredSignals = scoredCandidates.find((entry) => entry.framework.id === preferredCandidate.id)?.selectionSignals ?? {};
|
|
7033
|
+
const preferred = {
|
|
7034
|
+
id: preferredCandidate.id,
|
|
7035
|
+
reason: buildPreferredReason(preferredCandidate, profile, preferredSignals, explicitDefault, configuredSubscriptionProvider)
|
|
7036
|
+
};
|
|
5934
7037
|
for (const framework of frameworks) {
|
|
5935
7038
|
framework.recommended = framework.id === preferred.id;
|
|
7039
|
+
const scored = scoredCandidates.find((entry) => entry.framework.id === framework.id);
|
|
7040
|
+
if (scored) {
|
|
7041
|
+
framework.selectionScore = scored.score;
|
|
7042
|
+
framework.selectionSignals = scored.selectionSignals;
|
|
7043
|
+
}
|
|
5936
7044
|
}
|
|
5937
7045
|
return {
|
|
5938
7046
|
configuredSubscriptionProvider,
|
|
@@ -5940,16 +7048,169 @@ async function computeTaskAgentFrameworkState(runtime, probe) {
|
|
|
5940
7048
|
preferred
|
|
5941
7049
|
};
|
|
5942
7050
|
}
|
|
5943
|
-
async function getTaskAgentFrameworkState(runtime, probe) {
|
|
7051
|
+
async function getTaskAgentFrameworkState(runtime, probe, profileInput) {
|
|
5944
7052
|
if (frameworkStateCache && frameworkStateCache.expiresAt > Date.now()) {
|
|
5945
|
-
return frameworkStateCache.value;
|
|
7053
|
+
return computeTaskAgentFrameworkStateFromInventory(runtime, frameworkStateCache.value, probe, profileInput);
|
|
7054
|
+
}
|
|
7055
|
+
const value = await computeTaskAgentFrameworkState(runtime, probe, profileInput);
|
|
7056
|
+
if (!profileInput) {
|
|
7057
|
+
frameworkStateCache = {
|
|
7058
|
+
expiresAt: Date.now() + 15000,
|
|
7059
|
+
value: {
|
|
7060
|
+
configuredSubscriptionProvider: value.configuredSubscriptionProvider,
|
|
7061
|
+
frameworks: value.frameworks.map((framework) => ({
|
|
7062
|
+
...framework,
|
|
7063
|
+
recommended: false,
|
|
7064
|
+
selectionScore: undefined,
|
|
7065
|
+
selectionSignals: undefined
|
|
7066
|
+
}))
|
|
7067
|
+
}
|
|
7068
|
+
};
|
|
5946
7069
|
}
|
|
5947
|
-
|
|
7070
|
+
return value;
|
|
7071
|
+
}
|
|
7072
|
+
function computeTaskAgentFrameworkStateFromInventory(runtime, inventory, probe, profileInput) {
|
|
7073
|
+
const clonedProbe = {
|
|
7074
|
+
...probe,
|
|
7075
|
+
checkAvailableAgents: undefined
|
|
7076
|
+
};
|
|
5948
7077
|
frameworkStateCache = {
|
|
5949
7078
|
expiresAt: Date.now() + 15000,
|
|
5950
|
-
value
|
|
7079
|
+
value: inventory
|
|
5951
7080
|
};
|
|
5952
|
-
return
|
|
7081
|
+
return {
|
|
7082
|
+
...computeTaskAgentFrameworkStateFromCachedInventory(runtime, inventory, clonedProbe, profileInput)
|
|
7083
|
+
};
|
|
7084
|
+
}
|
|
7085
|
+
function computeTaskAgentFrameworkStateFromCachedInventory(runtime, inventory, probe, profileInput) {
|
|
7086
|
+
const metrics = probe?.getAgentMetrics?.() ?? {};
|
|
7087
|
+
const frameworks = inventory.frameworks.map((framework) => ({
|
|
7088
|
+
...framework,
|
|
7089
|
+
recommended: false
|
|
7090
|
+
}));
|
|
7091
|
+
const profile = buildTaskAgentTaskProfile(profileInput);
|
|
7092
|
+
const configuredSubscriptionProvider = inventory.configuredSubscriptionProvider;
|
|
7093
|
+
const providerPrefersClaude = configuredSubscriptionProvider === "anthropic-subscription";
|
|
7094
|
+
const providerPrefersCodex = configuredSubscriptionProvider === "openai-codex" || configuredSubscriptionProvider === "openai-subscription";
|
|
7095
|
+
const explicitDefault = safeGetSetting(runtime, "PARALLAX_DEFAULT_AGENT_TYPE")?.toLowerCase().trim();
|
|
7096
|
+
const candidates = frameworks.filter((framework) => framework.installed && !framework.temporarilyDisabled).length > 0 ? frameworks.filter((framework) => framework.installed && !framework.temporarilyDisabled) : frameworks.filter((framework) => framework.installed);
|
|
7097
|
+
const scoredCandidates = candidates.map((framework) => {
|
|
7098
|
+
const explicitOverride = explicitDefault === framework.id ? framework.installed && !framework.temporarilyDisabled ? 40 : 0 : 0;
|
|
7099
|
+
const providerPreference = providerPrefersClaude && framework.id === "claude" ? framework.subscriptionReady ? 18 : 6 : providerPrefersCodex && framework.id === "codex" ? framework.subscriptionReady ? 18 : 6 : 0;
|
|
7100
|
+
const availabilityScore = (framework.installed ? 40 : -100) + (framework.authReady ? 18 : -25) + (framework.subscriptionReady ? 8 : 0) + (framework.temporarilyDisabled ? -80 : 0);
|
|
7101
|
+
const profileScore = computeProfileFitScore(framework.id, profile);
|
|
7102
|
+
const metricsScore = computeMetricsScore(metrics[framework.id], profile.signals.fastIteration);
|
|
7103
|
+
const selectionSignals = {
|
|
7104
|
+
availability: availabilityScore,
|
|
7105
|
+
profile: profileScore,
|
|
7106
|
+
provider: providerPreference,
|
|
7107
|
+
metrics: metricsScore,
|
|
7108
|
+
explicitOverride
|
|
7109
|
+
};
|
|
7110
|
+
return {
|
|
7111
|
+
framework,
|
|
7112
|
+
score: Object.values(selectionSignals).reduce((sum, value) => sum + value, 0),
|
|
7113
|
+
selectionSignals
|
|
7114
|
+
};
|
|
7115
|
+
});
|
|
7116
|
+
const fallback = candidates[0] ?? frameworks.find((framework) => framework.installed) ?? frameworks[0];
|
|
7117
|
+
const preferredCandidate = scoredCandidates.sort((left, right) => {
|
|
7118
|
+
if (right.score !== left.score) {
|
|
7119
|
+
return right.score - left.score;
|
|
7120
|
+
}
|
|
7121
|
+
return left.framework.id.localeCompare(right.framework.id);
|
|
7122
|
+
})[0]?.framework ?? fallback;
|
|
7123
|
+
const preferredSignals = scoredCandidates.find((entry) => entry.framework.id === preferredCandidate.id)?.selectionSignals ?? {};
|
|
7124
|
+
const preferred = {
|
|
7125
|
+
id: preferredCandidate.id,
|
|
7126
|
+
reason: buildPreferredReason(preferredCandidate, profile, preferredSignals, explicitDefault, configuredSubscriptionProvider)
|
|
7127
|
+
};
|
|
7128
|
+
for (const framework of frameworks) {
|
|
7129
|
+
framework.recommended = framework.id === preferred.id;
|
|
7130
|
+
const scored = scoredCandidates.find((entry) => entry.framework.id === framework.id);
|
|
7131
|
+
if (scored) {
|
|
7132
|
+
framework.selectionScore = scored.score;
|
|
7133
|
+
framework.selectionSignals = scored.selectionSignals;
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
7136
|
+
frameworkStateCache = {
|
|
7137
|
+
expiresAt: Date.now() + 15000,
|
|
7138
|
+
value: inventory
|
|
7139
|
+
};
|
|
7140
|
+
return {
|
|
7141
|
+
configuredSubscriptionProvider,
|
|
7142
|
+
frameworks,
|
|
7143
|
+
preferred
|
|
7144
|
+
};
|
|
7145
|
+
}
|
|
7146
|
+
function clampSignal(value) {
|
|
7147
|
+
return Math.max(0, Math.min(1, value));
|
|
7148
|
+
}
|
|
7149
|
+
function kindBoost(kind, target) {
|
|
7150
|
+
if (kind === "mixed")
|
|
7151
|
+
return 0.25;
|
|
7152
|
+
return kind === target ? 0.4 : 0;
|
|
7153
|
+
}
|
|
7154
|
+
function buildTaskAgentTaskProfile(input) {
|
|
7155
|
+
const text = [
|
|
7156
|
+
input?.task?.trim(),
|
|
7157
|
+
input?.repo?.trim(),
|
|
7158
|
+
...(input?.acceptanceCriteria ?? []).map((value) => value.trim())
|
|
7159
|
+
].filter((value) => Boolean(value)).join(`
|
|
7160
|
+
`);
|
|
7161
|
+
const inferredKind = input?.threadKind ?? (OPS_SIGNAL_RE.test(text) ? "ops" : PLANNING_SIGNAL_RE.test(text) ? "planning" : RESEARCH_SIGNAL_RE.test(text) && !IMPLEMENTATION_SIGNAL_RE.test(text) ? "research" : IMPLEMENTATION_SIGNAL_RE.test(text) ? "coding" : RESEARCH_SIGNAL_RE.test(text) ? "mixed" : "coding");
|
|
7162
|
+
const repoPresent = Boolean(input?.repo?.trim() || input?.workdir?.trim());
|
|
7163
|
+
const subtaskCount = Math.max(1, input?.subtaskCount ?? 1);
|
|
7164
|
+
const signals = {
|
|
7165
|
+
implementation: clampSignal((IMPLEMENTATION_SIGNAL_RE.test(text) ? 0.7 : 0.2) + (repoPresent ? 0.15 : 0) + kindBoost(inferredKind, "coding")),
|
|
7166
|
+
research: clampSignal((RESEARCH_SIGNAL_RE.test(text) ? 0.7 : 0.1) + kindBoost(inferredKind, "research")),
|
|
7167
|
+
planning: clampSignal((PLANNING_SIGNAL_RE.test(text) ? 0.75 : 0.1) + kindBoost(inferredKind, "planning")),
|
|
7168
|
+
ops: clampSignal((OPS_SIGNAL_RE.test(text) ? 0.75 : 0.05) + kindBoost(inferredKind, "ops")),
|
|
7169
|
+
verification: clampSignal((VERIFICATION_SIGNAL_RE.test(text) ? 0.8 : 0.15) + ((input?.acceptanceCriteria?.length ?? 0) > 0 ? 0.15 : 0)),
|
|
7170
|
+
coordination: clampSignal((COORDINATION_SIGNAL_RE.test(text) ? 0.7 : 0.05) + (subtaskCount > 1 ? 0.25 : 0)),
|
|
7171
|
+
repoWork: clampSignal((REPO_SIGNAL_RE.test(text) ? 0.7 : 0.1) + (repoPresent ? 0.25 : 0)),
|
|
7172
|
+
fastIteration: clampSignal((FAST_ITERATION_SIGNAL_RE.test(text) ? 0.75 : 0.15) + (inferredKind === "coding" ? 0.1 : 0))
|
|
7173
|
+
};
|
|
7174
|
+
return {
|
|
7175
|
+
text,
|
|
7176
|
+
kind: inferredKind,
|
|
7177
|
+
subtaskCount,
|
|
7178
|
+
repoPresent,
|
|
7179
|
+
signals
|
|
7180
|
+
};
|
|
7181
|
+
}
|
|
7182
|
+
function computeProfileFitScore(frameworkId, profile) {
|
|
7183
|
+
const capability = FRAMEWORK_CAPABILITY_PROFILES[frameworkId];
|
|
7184
|
+
const weightedSum = profile.signals.implementation * capability.implementation * 18 + profile.signals.research * capability.research * 16 + profile.signals.planning * capability.planning * 14 + profile.signals.ops * capability.ops * 12 + profile.signals.verification * capability.verification * 14 + profile.signals.coordination * capability.coordination * 14 + profile.signals.repoWork * capability.repoWork * 10 + profile.signals.fastIteration * capability.fastIteration * 10;
|
|
7185
|
+
return Math.round(weightedSum);
|
|
7186
|
+
}
|
|
7187
|
+
function computeMetricsScore(metrics, fastIterationSignal) {
|
|
7188
|
+
if (!metrics || metrics.spawned === 0) {
|
|
7189
|
+
return 0;
|
|
7190
|
+
}
|
|
7191
|
+
const successRate = metrics.spawned > 0 ? metrics.completed / metrics.spawned : 0;
|
|
7192
|
+
const stallRate = metrics.spawned > 0 ? metrics.stallCount / metrics.spawned : 0;
|
|
7193
|
+
const durationBonus = metrics.completed > 0 ? Math.max(-8, Math.min(8, (120000 - metrics.avgCompletionMs) / 120000 * (4 + fastIterationSignal * 4))) : 0;
|
|
7194
|
+
return Math.round(successRate * 14 - stallRate * 12 + durationBonus);
|
|
7195
|
+
}
|
|
7196
|
+
function buildPreferredReason(framework, profile, selectionSignals, explicitDefault, configuredSubscriptionProvider) {
|
|
7197
|
+
const dominantSignals = Object.entries(profile.signals).sort((left, right) => right[1] - left[1]).slice(0, 2).map(([key]) => key);
|
|
7198
|
+
if (explicitDefault === framework.id && selectionSignals.explicitOverride > 0) {
|
|
7199
|
+
return `explicit PARALLAX_DEFAULT_AGENT_TYPE override, with ${FRAMEWORK_LABELS[framework.id]} still scoring well for ${dominantSignals.join(" + ")} work`;
|
|
7200
|
+
}
|
|
7201
|
+
if (configuredSubscriptionProvider === "anthropic-subscription" && framework.id === "claude" && framework.subscriptionReady) {
|
|
7202
|
+
return `best fit for ${dominantSignals.join(" + ")} work while honoring the configured Claude subscription`;
|
|
7203
|
+
}
|
|
7204
|
+
if ((configuredSubscriptionProvider === "openai-codex" || configuredSubscriptionProvider === "openai-subscription") && framework.id === "codex" && framework.subscriptionReady) {
|
|
7205
|
+
return `best fit for ${dominantSignals.join(" + ")} work while honoring the configured OpenAI subscription`;
|
|
7206
|
+
}
|
|
7207
|
+
if (framework.subscriptionReady) {
|
|
7208
|
+
return `best overall score for ${dominantSignals.join(" + ")} work with subscription-backed auth already available`;
|
|
7209
|
+
}
|
|
7210
|
+
if (framework.authReady) {
|
|
7211
|
+
return `best overall score for ${dominantSignals.join(" + ")} work with credentials already available`;
|
|
7212
|
+
}
|
|
7213
|
+
return `selected as the highest-scoring installed framework for ${dominantSignals.join(" + ")} work`;
|
|
5953
7214
|
}
|
|
5954
7215
|
function clearTaskAgentFrameworkStateCache() {
|
|
5955
7216
|
frameworkStateCache = undefined;
|
|
@@ -8147,6 +9408,8 @@ var PAUSE_TIMEOUT_MS = 30000;
|
|
|
8147
9408
|
var MAX_PRE_BRIDGE_BUFFER = 100;
|
|
8148
9409
|
var STOPPED_RECOVERY_WINDOW_MS = 90000;
|
|
8149
9410
|
var FAILOVER_OUTPUT_MAX_CHARS = 4000;
|
|
9411
|
+
var MAX_AUTOMATIC_ERROR_RECOVERIES = 2;
|
|
9412
|
+
var ALTERNATE_FRAMEWORK_ERROR_RE = /\b(auth|login|credential|401|403|unauthorized|forbidden|token|api key|not found|enoent|missing executable|command not found)\b/i;
|
|
8150
9413
|
function inferProviderSource(framework) {
|
|
8151
9414
|
if (framework.subscriptionReady) {
|
|
8152
9415
|
return "subscription";
|
|
@@ -8264,8 +9527,8 @@ class SwarmCoordinator {
|
|
|
8264
9527
|
await this.taskRegistry.recoverInterruptedTasks();
|
|
8265
9528
|
await this.rehydratePendingDecisions();
|
|
8266
9529
|
this.ptyService = ptyService;
|
|
8267
|
-
this.unsubscribeEvents = ptyService.
|
|
8268
|
-
this.
|
|
9530
|
+
this.unsubscribeEvents = ptyService.onNormalizedSessionEvent((normalized) => {
|
|
9531
|
+
this.handleNormalizedSessionEvent(normalized).catch((err) => {
|
|
8269
9532
|
this.log(`Error handling event: ${err}`);
|
|
8270
9533
|
});
|
|
8271
9534
|
});
|
|
@@ -8450,7 +9713,7 @@ class SwarmCoordinator {
|
|
|
8450
9713
|
const buffered = [...this.pauseBuffer];
|
|
8451
9714
|
this.pauseBuffer = [];
|
|
8452
9715
|
for (const entry of buffered) {
|
|
8453
|
-
this.
|
|
9716
|
+
this.handleNormalizedSessionEvent(entry).catch((err) => {
|
|
8454
9717
|
this.log(`Error replaying buffered event: ${err}`);
|
|
8455
9718
|
});
|
|
8456
9719
|
}
|
|
@@ -8498,7 +9761,9 @@ class SwarmCoordinator {
|
|
|
8498
9761
|
repo: context.repo,
|
|
8499
9762
|
workdir: context.workdir,
|
|
8500
9763
|
originalTask: context.originalTask
|
|
8501
|
-
}).catch(() => {
|
|
9764
|
+
}).catch((err) => {
|
|
9765
|
+
this.log(`Failed to append task registration history for ${sessionId}: ${err}`);
|
|
9766
|
+
});
|
|
8502
9767
|
const taskCtx = this.tasks.get(sessionId);
|
|
8503
9768
|
const persistPromise = taskCtx ? (async () => {
|
|
8504
9769
|
const existingThread = await this.taskRegistry.getThreadRecord(threadId);
|
|
@@ -8619,7 +9884,7 @@ class SwarmCoordinator {
|
|
|
8619
9884
|
if (buffered) {
|
|
8620
9885
|
this.unregisteredBuffer.delete(sessionId);
|
|
8621
9886
|
for (const entry of buffered) {
|
|
8622
|
-
this.
|
|
9887
|
+
this.handleNormalizedSessionEvent(entry.normalized).catch((err) => {
|
|
8623
9888
|
this.log(`Error replaying buffered event: ${err}`);
|
|
8624
9889
|
});
|
|
8625
9890
|
}
|
|
@@ -9206,8 +10471,40 @@ ${transcriptExcerpt}` : "",
|
|
|
9206
10471
|
const remainder = frameworks.filter((framework) => framework.id !== preferredFrameworkId);
|
|
9207
10472
|
return [preferred, ...remainder].filter((framework) => Boolean(framework && framework.id !== failedFramework && framework.installed && framework.authReady && !framework.temporarilyDisabled));
|
|
9208
10473
|
}
|
|
10474
|
+
getRecoveryCandidates(frameworks, currentFramework, preferredFrameworkId, preferAlternative) {
|
|
10475
|
+
const healthy = frameworks.filter((framework) => framework.installed && framework.authReady && !framework.temporarilyDisabled);
|
|
10476
|
+
const byId = new Map(healthy.map((framework) => [framework.id, framework]));
|
|
10477
|
+
const orderedIds = [];
|
|
10478
|
+
if (!preferAlternative) {
|
|
10479
|
+
orderedIds.push(currentFramework);
|
|
10480
|
+
}
|
|
10481
|
+
orderedIds.push(preferredFrameworkId);
|
|
10482
|
+
for (const framework of healthy) {
|
|
10483
|
+
orderedIds.push(framework.id);
|
|
10484
|
+
}
|
|
10485
|
+
const seen = new Set;
|
|
10486
|
+
const candidates = [];
|
|
10487
|
+
for (const id of orderedIds) {
|
|
10488
|
+
if (seen.has(id)) {
|
|
10489
|
+
continue;
|
|
10490
|
+
}
|
|
10491
|
+
seen.add(id);
|
|
10492
|
+
if (preferAlternative && id === currentFramework) {
|
|
10493
|
+
continue;
|
|
10494
|
+
}
|
|
10495
|
+
const framework = byId.get(id);
|
|
10496
|
+
if (framework) {
|
|
10497
|
+
candidates.push(framework);
|
|
10498
|
+
}
|
|
10499
|
+
}
|
|
10500
|
+
return candidates;
|
|
10501
|
+
}
|
|
10502
|
+
shouldPreferAlternativeFrameworkForError(reason) {
|
|
10503
|
+
return ALTERNATE_FRAMEWORK_ERROR_RE.test(reason);
|
|
10504
|
+
}
|
|
9209
10505
|
formatFailoverPrompt(taskCtx, failedFramework, reason, recentOutput) {
|
|
9210
|
-
const
|
|
10506
|
+
const cleanedOutput = cleanForFailoverContext(recentOutput, taskCtx.workdir);
|
|
10507
|
+
const trimmedOutput = cleanedOutput.trim();
|
|
9211
10508
|
const clippedOutput = trimmedOutput.length > FAILOVER_OUTPUT_MAX_CHARS ? trimmedOutput.slice(-FAILOVER_OUTPUT_MAX_CHARS) : trimmedOutput;
|
|
9212
10509
|
const recentDecisions = taskCtx.decisions.slice(-5).map((decision, index) => `${index + 1}. ${decision.event}: ${decision.reasoning}${decision.response ? ` (response: ${decision.response})` : ""}`).join(`
|
|
9213
10510
|
`);
|
|
@@ -9228,6 +10525,32 @@ ${clippedOutput}
|
|
|
9228
10525
|
` : "",
|
|
9229
10526
|
"Use the existing workspace state instead of starting from scratch. Inspect the files, continue the task, run the needed validation, and then report what changed and how you verified it."
|
|
9230
10527
|
].filter(Boolean).join(`
|
|
10528
|
+
`);
|
|
10529
|
+
}
|
|
10530
|
+
formatErrorRecoveryPrompt(taskCtx, recoveryFramework, reason, recentOutput) {
|
|
10531
|
+
const cleanedOutput = cleanForFailoverContext(recentOutput, taskCtx.workdir);
|
|
10532
|
+
const trimmedOutput = cleanedOutput.trim();
|
|
10533
|
+
const clippedOutput = trimmedOutput.length > FAILOVER_OUTPUT_MAX_CHARS ? trimmedOutput.slice(-FAILOVER_OUTPUT_MAX_CHARS) : trimmedOutput;
|
|
10534
|
+
const recentDecisions = taskCtx.decisions.slice(-5).map((decision, index) => `${index + 1}. ${decision.event}: ${decision.reasoning}${decision.response ? ` (response: ${decision.response})` : ""}`).join(`
|
|
10535
|
+
`);
|
|
10536
|
+
const recoveryMode = recoveryFramework === taskCtx.agentType ? `a fresh ${recoveryFramework} session` : `a ${recoveryFramework} recovery session`;
|
|
10537
|
+
return [
|
|
10538
|
+
`Continue an in-progress task after the previous session terminated unexpectedly. Milady started ${recoveryMode} for recovery.`,
|
|
10539
|
+
"",
|
|
10540
|
+
"Original task:",
|
|
10541
|
+
taskCtx.originalTask,
|
|
10542
|
+
"",
|
|
10543
|
+
`Failure reason: ${reason}`,
|
|
10544
|
+
`Workspace: ${taskCtx.workdir}`,
|
|
10545
|
+
"",
|
|
10546
|
+
recentDecisions ? `Recent coordinator decisions:
|
|
10547
|
+
${recentDecisions}
|
|
10548
|
+
` : "",
|
|
10549
|
+
clippedOutput ? `Recent terminal output from the failed session:
|
|
10550
|
+
${clippedOutput}
|
|
10551
|
+
` : "",
|
|
10552
|
+
"Use the existing workspace state instead of starting over. Inspect the current files, recover from the failure, continue the task, run the needed validation, and then report exactly what changed and how you verified it."
|
|
10553
|
+
].filter(Boolean).join(`
|
|
9231
10554
|
`);
|
|
9232
10555
|
}
|
|
9233
10556
|
async handleFrameworkDepletion(taskCtx, sessionId, reason) {
|
|
@@ -9325,6 +10648,95 @@ ${clippedOutput}
|
|
|
9325
10648
|
replacementLabel
|
|
9326
10649
|
};
|
|
9327
10650
|
}
|
|
10651
|
+
async attemptTaskRecovery(taskCtx, errorMsg) {
|
|
10652
|
+
if (!this.ptyService) {
|
|
10653
|
+
return null;
|
|
10654
|
+
}
|
|
10655
|
+
const failedSession = this.ptyService.getSession(taskCtx.sessionId);
|
|
10656
|
+
const priorMetadata = failedSession?.metadata && typeof failedSession.metadata === "object" && !Array.isArray(failedSession.metadata) ? failedSession.metadata : {};
|
|
10657
|
+
const recoveryOrdinal = typeof priorMetadata.recoveryOrdinal === "number" ? priorMetadata.recoveryOrdinal + 1 : 1;
|
|
10658
|
+
if (recoveryOrdinal > MAX_AUTOMATIC_ERROR_RECOVERIES) {
|
|
10659
|
+
return null;
|
|
10660
|
+
}
|
|
10661
|
+
let recoveryFramework = taskCtx.agentType;
|
|
10662
|
+
let recoveryAvailability = null;
|
|
10663
|
+
if (this.isAutomaticFailoverFramework(taskCtx.agentType)) {
|
|
10664
|
+
const frameworkState = await this.ptyService.getFrameworkState();
|
|
10665
|
+
const candidates = this.getRecoveryCandidates(frameworkState.frameworks, taskCtx.agentType, frameworkState.preferred.id, this.shouldPreferAlternativeFrameworkForError(errorMsg));
|
|
10666
|
+
const selected = candidates[0];
|
|
10667
|
+
if (!selected) {
|
|
10668
|
+
return null;
|
|
10669
|
+
}
|
|
10670
|
+
recoveryFramework = selected.id;
|
|
10671
|
+
recoveryAvailability = selected;
|
|
10672
|
+
}
|
|
10673
|
+
const priorOutput = await Promise.race([
|
|
10674
|
+
this.ptyService.getSessionOutput(taskCtx.sessionId, 200),
|
|
10675
|
+
new Promise((resolve2) => setTimeout(() => resolve2(""), 5000))
|
|
10676
|
+
]);
|
|
10677
|
+
const replacementLabel = `${taskCtx.label} (${recoveryFramework} recovery ${recoveryOrdinal})`;
|
|
10678
|
+
const replacementSession = await this.ptyService.spawnSession({
|
|
10679
|
+
name: failedSession?.name ?? `task-recovery-${Date.now()}-${recoveryFramework}`,
|
|
10680
|
+
agentType: recoveryFramework,
|
|
10681
|
+
workdir: taskCtx.workdir,
|
|
10682
|
+
initialTask: this.formatErrorRecoveryPrompt(taskCtx, recoveryFramework, errorMsg, priorOutput),
|
|
10683
|
+
credentials: buildAgentCredentials(this.runtime),
|
|
10684
|
+
approvalPreset: this.ptyService.defaultApprovalPreset,
|
|
10685
|
+
skipAdapterAutoResponse: true,
|
|
10686
|
+
metadata: {
|
|
10687
|
+
...priorMetadata,
|
|
10688
|
+
threadId: taskCtx.threadId,
|
|
10689
|
+
requestedType: recoveryFramework,
|
|
10690
|
+
label: replacementLabel,
|
|
10691
|
+
recoveryOrdinal,
|
|
10692
|
+
recoveredFromFramework: taskCtx.agentType,
|
|
10693
|
+
recoveredFromSessionId: taskCtx.sessionId,
|
|
10694
|
+
recoveryReason: errorMsg,
|
|
10695
|
+
recoveryAt: Date.now()
|
|
10696
|
+
}
|
|
10697
|
+
});
|
|
10698
|
+
await this.registerTask(replacementSession.id, {
|
|
10699
|
+
threadId: taskCtx.threadId,
|
|
10700
|
+
taskNodeId: taskCtx.taskNodeId,
|
|
10701
|
+
agentType: recoveryFramework,
|
|
10702
|
+
label: replacementLabel,
|
|
10703
|
+
originalTask: taskCtx.originalTask,
|
|
10704
|
+
workdir: taskCtx.workdir,
|
|
10705
|
+
repo: taskCtx.repo,
|
|
10706
|
+
providerSource: recoveryAvailability ? inferProviderSource(recoveryAvailability) : null,
|
|
10707
|
+
metadata: replacementSession.metadata && typeof replacementSession.metadata === "object" && !Array.isArray(replacementSession.metadata) ? replacementSession.metadata : undefined
|
|
10708
|
+
});
|
|
10709
|
+
await this.taskRegistry.appendEvent({
|
|
10710
|
+
threadId: taskCtx.threadId,
|
|
10711
|
+
sessionId: replacementSession.id,
|
|
10712
|
+
eventType: "task_error_recovery_started",
|
|
10713
|
+
summary: `Continuing "${taskCtx.label}" after an agent error`,
|
|
10714
|
+
data: {
|
|
10715
|
+
fromFramework: taskCtx.agentType,
|
|
10716
|
+
fromSessionId: taskCtx.sessionId,
|
|
10717
|
+
toFramework: recoveryFramework,
|
|
10718
|
+
toSessionId: replacementSession.id,
|
|
10719
|
+
reason: errorMsg,
|
|
10720
|
+
recoveryOrdinal
|
|
10721
|
+
}
|
|
10722
|
+
});
|
|
10723
|
+
this.broadcast({
|
|
10724
|
+
type: "task_recovery_started",
|
|
10725
|
+
sessionId: replacementSession.id,
|
|
10726
|
+
timestamp: Date.now(),
|
|
10727
|
+
data: {
|
|
10728
|
+
fromSessionId: taskCtx.sessionId,
|
|
10729
|
+
fromFramework: taskCtx.agentType,
|
|
10730
|
+
toFramework: recoveryFramework,
|
|
10731
|
+
reason: errorMsg
|
|
10732
|
+
}
|
|
10733
|
+
});
|
|
10734
|
+
return {
|
|
10735
|
+
replacementSessionId: replacementSession.id,
|
|
10736
|
+
replacementFramework: recoveryFramework,
|
|
10737
|
+
replacementLabel
|
|
10738
|
+
};
|
|
10739
|
+
}
|
|
9328
10740
|
async recordDecision(taskCtx, decision) {
|
|
9329
10741
|
taskCtx.decisions.push(decision);
|
|
9330
10742
|
await this.taskRegistry.recordDecision({
|
|
@@ -9357,7 +10769,9 @@ ${clippedOutput}
|
|
|
9357
10769
|
if (ctx) {
|
|
9358
10770
|
this.unregisteredBuffer.delete(sessionId);
|
|
9359
10771
|
for (const entry of stillBuffered) {
|
|
9360
|
-
this.
|
|
10772
|
+
this.handleNormalizedSessionEvent(entry.normalized).catch((err) => {
|
|
10773
|
+
this.log(`Failed to replay buffered event for ${sessionId}: ${err}`);
|
|
10774
|
+
});
|
|
9361
10775
|
}
|
|
9362
10776
|
return;
|
|
9363
10777
|
}
|
|
@@ -9418,6 +10832,22 @@ ${clippedOutput}
|
|
|
9418
10832
|
} catch {}
|
|
9419
10833
|
}
|
|
9420
10834
|
async handleSessionEvent(sessionId, event, data) {
|
|
10835
|
+
const normalized = normalizeCoordinatorEvent(sessionId, event, data);
|
|
10836
|
+
if (!normalized) {
|
|
10837
|
+
this.broadcast({
|
|
10838
|
+
type: event,
|
|
10839
|
+
sessionId,
|
|
10840
|
+
timestamp: Date.now(),
|
|
10841
|
+
data
|
|
10842
|
+
});
|
|
10843
|
+
return;
|
|
10844
|
+
}
|
|
10845
|
+
await this.handleNormalizedSessionEvent(normalized);
|
|
10846
|
+
}
|
|
10847
|
+
async handleNormalizedSessionEvent(normalized) {
|
|
10848
|
+
const sessionId = normalized.sessionId;
|
|
10849
|
+
const event = normalized.name;
|
|
10850
|
+
const data = normalized.rawData;
|
|
9421
10851
|
if (!this.scratchDecisionWired) {
|
|
9422
10852
|
this.wireScratchDecisionCallback();
|
|
9423
10853
|
}
|
|
@@ -9436,7 +10866,7 @@ ${clippedOutput}
|
|
|
9436
10866
|
buffer = [];
|
|
9437
10867
|
this.unregisteredBuffer.set(sessionId, buffer);
|
|
9438
10868
|
}
|
|
9439
|
-
buffer.push({
|
|
10869
|
+
buffer.push({ normalized, receivedAt: Date.now() });
|
|
9440
10870
|
if (!this.unregisteredRetryTimers.has(sessionId)) {
|
|
9441
10871
|
this.scheduleUnregisteredRetry(sessionId, 0);
|
|
9442
10872
|
}
|
|
@@ -9466,22 +10896,23 @@ ${clippedOutput}
|
|
|
9466
10896
|
taskCtx.lastActivityAt = Date.now();
|
|
9467
10897
|
taskCtx.idleCheckCount = 0;
|
|
9468
10898
|
if (this._paused && (event === "blocked" || event === "task_complete")) {
|
|
9469
|
-
const
|
|
9470
|
-
if (!
|
|
10899
|
+
const blockedAutoResponded = event === "blocked" && normalized.autoResponded === true;
|
|
10900
|
+
if (!blockedAutoResponded) {
|
|
9471
10901
|
this.broadcast({
|
|
9472
10902
|
type: event === "blocked" ? "blocked_buffered" : "turn_complete_buffered",
|
|
9473
10903
|
sessionId,
|
|
9474
10904
|
timestamp: Date.now(),
|
|
9475
10905
|
data
|
|
9476
10906
|
});
|
|
9477
|
-
this.pauseBuffer.push(
|
|
10907
|
+
this.pauseBuffer.push(normalized);
|
|
9478
10908
|
this.log(`Buffered "${event}" for ${taskCtx.label} (coordinator paused)`);
|
|
9479
10909
|
return;
|
|
9480
10910
|
}
|
|
9481
10911
|
}
|
|
9482
10912
|
switch (event) {
|
|
9483
10913
|
case "blocked": {
|
|
9484
|
-
const
|
|
10914
|
+
const blockedEvent = normalized;
|
|
10915
|
+
const blockedPrompt = blockedEvent.promptText;
|
|
9485
10916
|
if (this.isAutomaticFailoverFramework(taskCtx.agentType) && isUsageExhaustedTaskAgentError(blockedPrompt)) {
|
|
9486
10917
|
const failoverResult = await this.handleFrameworkDepletion(taskCtx, sessionId, blockedPrompt);
|
|
9487
10918
|
taskCtx.status = "error";
|
|
@@ -9549,8 +10980,18 @@ ${clippedOutput}
|
|
|
9549
10980
|
});
|
|
9550
10981
|
const errorMsg = data.message ?? "unknown error";
|
|
9551
10982
|
const failoverResult = await this.handleFrameworkDepletion(taskCtx, sessionId, errorMsg);
|
|
10983
|
+
let recoveryResult = null;
|
|
9552
10984
|
if (!failoverResult) {
|
|
9553
|
-
|
|
10985
|
+
try {
|
|
10986
|
+
recoveryResult = await this.attemptTaskRecovery(taskCtx, errorMsg);
|
|
10987
|
+
} catch (recoveryError) {
|
|
10988
|
+
this.log(`Automatic error recovery failed for "${taskCtx.label}": ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
|
|
10989
|
+
}
|
|
10990
|
+
}
|
|
10991
|
+
if (recoveryResult) {
|
|
10992
|
+
this.sendChatMessage(`"${taskCtx.label}" hit an error: ${errorMsg}. Milady is continuing the same task on ${recoveryResult.replacementFramework}.`, "coding-agent");
|
|
10993
|
+
} else if (!failoverResult) {
|
|
10994
|
+
this.sendChatMessage(`"${taskCtx.label}" hit an error and needs your attention: ${errorMsg}`, "coding-agent");
|
|
9554
10995
|
}
|
|
9555
10996
|
taskCtx.status = "error";
|
|
9556
10997
|
await this.taskRegistry.appendEvent({
|
|
@@ -9560,10 +11001,13 @@ ${clippedOutput}
|
|
|
9560
11001
|
summary: `Task "${taskCtx.label}" errored`,
|
|
9561
11002
|
data: { status: "error", message: errorMsg }
|
|
9562
11003
|
});
|
|
9563
|
-
|
|
11004
|
+
if (!failoverResult && !recoveryResult) {
|
|
11005
|
+
checkAllTasksComplete(this);
|
|
11006
|
+
}
|
|
9564
11007
|
break;
|
|
9565
11008
|
}
|
|
9566
|
-
case "stopped":
|
|
11009
|
+
case "stopped": {
|
|
11010
|
+
const alreadyTerminal = taskCtx.status === "completed" || taskCtx.status === "error";
|
|
9567
11011
|
if (taskCtx.status !== "completed" && taskCtx.status !== "error") {
|
|
9568
11012
|
taskCtx.status = "stopped";
|
|
9569
11013
|
taskCtx.stoppedAt = Date.now();
|
|
@@ -9582,8 +11026,12 @@ ${clippedOutput}
|
|
|
9582
11026
|
summary: `Task "${taskCtx.label}" stopped`,
|
|
9583
11027
|
data: { status: taskCtx.status }
|
|
9584
11028
|
});
|
|
11029
|
+
if (!alreadyTerminal) {
|
|
11030
|
+
this.sendChatMessage(`"${taskCtx.label}" stopped before completion.`, "coding-agent");
|
|
11031
|
+
}
|
|
9585
11032
|
checkAllTasksComplete(this);
|
|
9586
11033
|
break;
|
|
11034
|
+
}
|
|
9587
11035
|
case "ready":
|
|
9588
11036
|
taskCtx.status = "active";
|
|
9589
11037
|
if (taskCtx.agentType === "claude" || taskCtx.agentType === "codex" || taskCtx.agentType === "gemini" || taskCtx.agentType === "aider") {
|
|
@@ -9603,6 +11051,35 @@ ${clippedOutput}
|
|
|
9603
11051
|
data: { status: "ready" }
|
|
9604
11052
|
});
|
|
9605
11053
|
break;
|
|
11054
|
+
case "login_required": {
|
|
11055
|
+
const loginEvent = normalized;
|
|
11056
|
+
taskCtx.status = "blocked";
|
|
11057
|
+
this.broadcast({
|
|
11058
|
+
type: "login_required",
|
|
11059
|
+
sessionId,
|
|
11060
|
+
timestamp: Date.now(),
|
|
11061
|
+
data
|
|
11062
|
+
});
|
|
11063
|
+
await this.taskRegistry.appendEvent({
|
|
11064
|
+
threadId: taskCtx.threadId,
|
|
11065
|
+
sessionId,
|
|
11066
|
+
eventType: "task_status_changed",
|
|
11067
|
+
summary: `Task "${taskCtx.label}" is waiting for login`,
|
|
11068
|
+
data: {
|
|
11069
|
+
status: "blocked",
|
|
11070
|
+
reason: "login_required",
|
|
11071
|
+
instructions: loginEvent.instructions ?? null,
|
|
11072
|
+
url: loginEvent.url ?? null
|
|
11073
|
+
}
|
|
11074
|
+
});
|
|
11075
|
+
const loginParts = [
|
|
11076
|
+
`"${taskCtx.label}" needs a provider login before it can continue.`,
|
|
11077
|
+
loginEvent.instructions?.trim() || "",
|
|
11078
|
+
loginEvent.url ? `Login link: ${loginEvent.url}` : ""
|
|
11079
|
+
].filter(Boolean);
|
|
11080
|
+
this.sendChatMessage(loginParts.join(" "), "coding-agent");
|
|
11081
|
+
break;
|
|
11082
|
+
}
|
|
9606
11083
|
case "tool_running": {
|
|
9607
11084
|
taskCtx.status = "tool_running";
|
|
9608
11085
|
taskCtx.lastActivityAt = Date.now();
|
|
@@ -9636,7 +11113,9 @@ ${clippedOutput}
|
|
|
9636
11113
|
}
|
|
9637
11114
|
} catch {}
|
|
9638
11115
|
}
|
|
9639
|
-
|
|
11116
|
+
const message = `[${taskCtx.label}] Running ${toolDesc}.${urlSuffix} The agent is working outside the terminal.`;
|
|
11117
|
+
this.log(message);
|
|
11118
|
+
this.sendChatMessage(message, "coding-agent");
|
|
9640
11119
|
}
|
|
9641
11120
|
break;
|
|
9642
11121
|
}
|
|
@@ -9701,6 +11180,7 @@ ${clippedOutput}
|
|
|
9701
11180
|
response: decision.action === "respond" ? decision.useKeys ? `keys:${decision.keys?.join(",")}` : decision.response : undefined,
|
|
9702
11181
|
reasoning: `Human-approved: ${decision.reasoning}`
|
|
9703
11182
|
});
|
|
11183
|
+
await this.syncTaskContext(taskCtx);
|
|
9704
11184
|
}
|
|
9705
11185
|
await this.executeDecision(sessionId, decision);
|
|
9706
11186
|
this.pendingDecisions.delete(sessionId);
|
|
@@ -9728,6 +11208,9 @@ ${clippedOutput}
|
|
|
9728
11208
|
keys: decision.keys
|
|
9729
11209
|
}
|
|
9730
11210
|
});
|
|
11211
|
+
if (taskCtx) {
|
|
11212
|
+
this.sendChatMessage(`"${taskCtx.label}" was approved. Milady is continuing the task now.`, "coding-agent");
|
|
11213
|
+
}
|
|
9731
11214
|
} else {
|
|
9732
11215
|
if (taskCtx) {
|
|
9733
11216
|
taskCtx.status = "blocked";
|
|
@@ -9738,6 +11221,7 @@ ${clippedOutput}
|
|
|
9738
11221
|
decision: "escalate",
|
|
9739
11222
|
reasoning: "Human rejected the suggested action"
|
|
9740
11223
|
});
|
|
11224
|
+
await this.syncTaskContext(taskCtx);
|
|
9741
11225
|
}
|
|
9742
11226
|
this.pendingDecisions.delete(sessionId);
|
|
9743
11227
|
await this.taskRegistry.deletePendingDecision(sessionId);
|
|
@@ -9756,6 +11240,7 @@ ${clippedOutput}
|
|
|
9756
11240
|
timestamp: Date.now(),
|
|
9757
11241
|
data: { prompt: pending.promptText }
|
|
9758
11242
|
});
|
|
11243
|
+
this.sendChatMessage(`"${pending.taskContext.label}" remains blocked after the suggested action was rejected. Prompt: ${pending.promptText}`, "coding-agent");
|
|
9759
11244
|
}
|
|
9760
11245
|
}
|
|
9761
11246
|
log(message) {
|
|
@@ -9765,6 +11250,21 @@ ${clippedOutput}
|
|
|
9765
11250
|
|
|
9766
11251
|
// src/services/pty-service.ts
|
|
9767
11252
|
init_swarm_decision_loop();
|
|
11253
|
+
function buildWorkspaceLockMemory(workdir) {
|
|
11254
|
+
return `# Workspace
|
|
11255
|
+
|
|
11256
|
+
Your working directory is \`${workdir}\`. Stay inside it: do not \`cd\` to \`/tmp\`, \`/\`, \`$HOME\`, or any other path outside the workspace. Create all files, run all builds, and start all servers from this directory. If you need scratch space, make a subdirectory here.`;
|
|
11257
|
+
}
|
|
11258
|
+
function prependWorkspaceLockToTask(task, workspaceLock) {
|
|
11259
|
+
if (!task?.trim()) {
|
|
11260
|
+
return;
|
|
11261
|
+
}
|
|
11262
|
+
return `${workspaceLock}
|
|
11263
|
+
|
|
11264
|
+
---
|
|
11265
|
+
|
|
11266
|
+
${task}`;
|
|
11267
|
+
}
|
|
9768
11268
|
function getCoordinator(runtime) {
|
|
9769
11269
|
const ptyService = runtime.getService("PTY_SERVICE");
|
|
9770
11270
|
return ptyService?.coordinator ?? undefined;
|
|
@@ -9781,9 +11281,12 @@ class PTYService {
|
|
|
9781
11281
|
sessionMetadata = new Map;
|
|
9782
11282
|
sessionWorkdirs = new Map;
|
|
9783
11283
|
eventCallbacks = [];
|
|
11284
|
+
normalizedEventCallbacks = [];
|
|
9784
11285
|
outputUnsubscribers = new Map;
|
|
9785
11286
|
transcriptUnsubscribers = new Map;
|
|
9786
11287
|
sessionOutputBuffers = new Map;
|
|
11288
|
+
completionReconcileTimers = new Map;
|
|
11289
|
+
completionSignalSince = new Map;
|
|
9787
11290
|
terminalSessionStates = new Map;
|
|
9788
11291
|
adapterCache = new Map;
|
|
9789
11292
|
taskResponseMarkers = new Map;
|
|
@@ -9898,6 +11401,11 @@ class PTYService {
|
|
|
9898
11401
|
unsubscribe();
|
|
9899
11402
|
}
|
|
9900
11403
|
this.transcriptUnsubscribers.clear();
|
|
11404
|
+
for (const timer of this.completionReconcileTimers.values()) {
|
|
11405
|
+
clearInterval(timer);
|
|
11406
|
+
}
|
|
11407
|
+
this.completionReconcileTimers.clear();
|
|
11408
|
+
this.completionSignalSince.clear();
|
|
9901
11409
|
if (this.manager) {
|
|
9902
11410
|
await this.manager.shutdown();
|
|
9903
11411
|
this.manager = null;
|
|
@@ -9926,7 +11434,6 @@ class PTYService {
|
|
|
9926
11434
|
}
|
|
9927
11435
|
const piRequested = isPiAgentType(options.agentType);
|
|
9928
11436
|
const resolvedAgentType = piRequested ? "shell" : options.agentType;
|
|
9929
|
-
const resolvedInitialTask = piRequested ? toPiCommand(options.initialTask) : options.initialTask;
|
|
9930
11437
|
const effectiveApprovalPreset = options.approvalPreset ?? (resolvedAgentType !== "shell" ? this.defaultApprovalPreset : undefined);
|
|
9931
11438
|
const maxSessions = this.serviceConfig.maxConcurrentSessions ?? 8;
|
|
9932
11439
|
const activeSessions = (await this.listSessions()).length;
|
|
@@ -9935,10 +11442,19 @@ class PTYService {
|
|
|
9935
11442
|
}
|
|
9936
11443
|
const sessionId = this.generateSessionId();
|
|
9937
11444
|
const workdir = options.workdir ?? process.cwd();
|
|
11445
|
+
const workspaceLock = buildWorkspaceLockMemory(workdir);
|
|
11446
|
+
const shouldWriteMemoryFile = resolvedAgentType !== "shell" && Boolean(options.memoryContent?.trim());
|
|
11447
|
+
const effectiveInitialTask = shouldWriteMemoryFile ? options.initialTask : prependWorkspaceLockToTask(options.initialTask, workspaceLock);
|
|
11448
|
+
const resolvedInitialTask = piRequested ? toPiCommand(effectiveInitialTask) : effectiveInitialTask;
|
|
9938
11449
|
this.sessionWorkdirs.set(sessionId, workdir);
|
|
9939
|
-
if (
|
|
11450
|
+
if (shouldWriteMemoryFile) {
|
|
11451
|
+
const fullMemory = options.memoryContent ? `${workspaceLock}
|
|
11452
|
+
|
|
11453
|
+
---
|
|
11454
|
+
|
|
11455
|
+
${options.memoryContent}` : workspaceLock;
|
|
9940
11456
|
try {
|
|
9941
|
-
const writtenPath = await this.writeMemoryFile(resolvedAgentType, workdir,
|
|
11457
|
+
const writtenPath = await this.writeMemoryFile(resolvedAgentType, workdir, fullMemory);
|
|
9942
11458
|
this.log(`Wrote memory file for ${resolvedAgentType}: ${writtenPath}`);
|
|
9943
11459
|
} catch (err) {
|
|
9944
11460
|
this.log(`Failed to write memory file for ${resolvedAgentType}: ${err}`);
|
|
@@ -9978,8 +11494,8 @@ class PTYService {
|
|
|
9978
11494
|
settings.hooks = { ...existingHooks, ...hookProtocol.settingsHooks };
|
|
9979
11495
|
this.log(`Injecting HTTP hooks for session ${sessionId}`);
|
|
9980
11496
|
}
|
|
9981
|
-
await
|
|
9982
|
-
await
|
|
11497
|
+
await mkdir4(dirname2(settingsPath), { recursive: true });
|
|
11498
|
+
await writeFile5(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
9983
11499
|
this.log(`Wrote allowedDirectories [${workdir}] to ${settingsPath}`);
|
|
9984
11500
|
} catch (err) {
|
|
9985
11501
|
this.log(`Failed to write Claude settings: ${err}`);
|
|
@@ -10002,8 +11518,8 @@ class PTYService {
|
|
|
10002
11518
|
settings.hooks = { ...existingHooks, ...hookProtocol.settingsHooks };
|
|
10003
11519
|
this.log(`Injecting Gemini CLI hooks for session ${sessionId}`);
|
|
10004
11520
|
}
|
|
10005
|
-
await
|
|
10006
|
-
await
|
|
11521
|
+
await mkdir4(dirname2(settingsPath), { recursive: true });
|
|
11522
|
+
await writeFile5(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
10007
11523
|
} catch (err) {
|
|
10008
11524
|
this.log(`Failed to write Gemini settings: ${err}`);
|
|
10009
11525
|
}
|
|
@@ -10017,20 +11533,6 @@ class PTYService {
|
|
|
10017
11533
|
initialTask: resolvedInitialTask,
|
|
10018
11534
|
approvalPreset: effectiveApprovalPreset
|
|
10019
11535
|
}, workdir);
|
|
10020
|
-
{
|
|
10021
|
-
const ac = spawnConfig.adapterConfig;
|
|
10022
|
-
const mask = (v) => typeof v === "string" && v.length > 12 ? `${v.slice(0, 8)}...${v.slice(-4)}` : String(v);
|
|
10023
|
-
const parts = [];
|
|
10024
|
-
if (ac?.anthropicKey)
|
|
10025
|
-
parts.push(`anthropicKey=${mask(ac.anthropicKey)}`);
|
|
10026
|
-
if (ac?.anthropicBaseUrl)
|
|
10027
|
-
parts.push(`anthropicBaseUrl=${ac.anthropicBaseUrl}`);
|
|
10028
|
-
if (ac?.openaiKey)
|
|
10029
|
-
parts.push(`openaiKey=${mask(ac.openaiKey)}`);
|
|
10030
|
-
if (ac?.openaiBaseUrl)
|
|
10031
|
-
parts.push(`openaiBaseUrl=${ac.openaiBaseUrl}`);
|
|
10032
|
-
this.log(`[DEBUG] PTY spawn ${resolvedAgentType} adapterConfig credentials: ${parts.join(", ") || "(none)"}`);
|
|
10033
|
-
}
|
|
10034
11536
|
const session = await this.manager.spawn(spawnConfig);
|
|
10035
11537
|
this.terminalSessionStates.delete(session.id);
|
|
10036
11538
|
this.sessionNames.set(session.id, options.name);
|
|
@@ -10120,7 +11622,13 @@ class PTYService {
|
|
|
10120
11622
|
throw new Error("PTYService not initialized");
|
|
10121
11623
|
captureFeed(sessionId, input, "stdin");
|
|
10122
11624
|
this.persistTranscript(sessionId, "stdin", input);
|
|
10123
|
-
|
|
11625
|
+
const metadata = this.sessionMetadata.get(sessionId);
|
|
11626
|
+
if (metadata) {
|
|
11627
|
+
metadata.lastSentInput = input;
|
|
11628
|
+
}
|
|
11629
|
+
const message = await sendToSession(this.ioContext(), sessionId, input);
|
|
11630
|
+
this.scheduleCompletionReconcile(sessionId);
|
|
11631
|
+
return message;
|
|
10124
11632
|
}
|
|
10125
11633
|
async sendKeysToSession(sessionId, keys) {
|
|
10126
11634
|
if (!this.manager)
|
|
@@ -10136,6 +11644,7 @@ class PTYService {
|
|
|
10136
11644
|
try {
|
|
10137
11645
|
return await stopSession(this.ioContext(), sessionId, this.sessionMetadata, this.sessionWorkdirs, (msg) => this.log(msg), force);
|
|
10138
11646
|
} finally {
|
|
11647
|
+
this.clearCompletionReconcile(sessionId);
|
|
10139
11648
|
this.clearTranscriptCapture(sessionId);
|
|
10140
11649
|
}
|
|
10141
11650
|
}
|
|
@@ -10154,19 +11663,36 @@ class PTYService {
|
|
|
10154
11663
|
return "fixed";
|
|
10155
11664
|
}
|
|
10156
11665
|
get defaultAgentType() {
|
|
11666
|
+
return this.explicitDefaultAgentType ?? "claude";
|
|
11667
|
+
}
|
|
11668
|
+
get explicitDefaultAgentType() {
|
|
10157
11669
|
const fromConfig = readConfigEnvKey("PARALLAX_DEFAULT_AGENT_TYPE");
|
|
10158
11670
|
const fromRuntimeOrEnv = fromConfig || this.runtime.getSetting("PARALLAX_DEFAULT_AGENT_TYPE");
|
|
10159
11671
|
if (fromRuntimeOrEnv && ["claude", "gemini", "codex", "aider"].includes(fromRuntimeOrEnv.toLowerCase())) {
|
|
10160
11672
|
return fromRuntimeOrEnv.toLowerCase();
|
|
10161
11673
|
}
|
|
10162
|
-
return
|
|
11674
|
+
return null;
|
|
10163
11675
|
}
|
|
10164
|
-
async resolveAgentType() {
|
|
10165
|
-
|
|
11676
|
+
async resolveAgentType(selection) {
|
|
11677
|
+
if (this.agentSelectionStrategy === "fixed" && this.explicitDefaultAgentType) {
|
|
11678
|
+
return this.explicitDefaultAgentType;
|
|
11679
|
+
}
|
|
11680
|
+
const frameworkState = await this.getFrameworkState(selection);
|
|
10166
11681
|
return frameworkState.preferred.id;
|
|
10167
11682
|
}
|
|
10168
|
-
async getFrameworkState() {
|
|
10169
|
-
|
|
11683
|
+
async getFrameworkState(selection) {
|
|
11684
|
+
const profile = selection ? buildTaskAgentTaskProfile(selection) : undefined;
|
|
11685
|
+
return getTaskAgentFrameworkState(this.runtime, {
|
|
11686
|
+
checkAvailableAgents: (types) => this.checkAvailableAgents(types),
|
|
11687
|
+
getAgentMetrics: () => this.metricsTracker.getAll()
|
|
11688
|
+
}, profile ? {
|
|
11689
|
+
task: selection?.task,
|
|
11690
|
+
repo: selection?.repo,
|
|
11691
|
+
workdir: selection?.workdir,
|
|
11692
|
+
threadKind: profile.kind,
|
|
11693
|
+
subtaskCount: profile.subtaskCount,
|
|
11694
|
+
acceptanceCriteria: selection?.acceptanceCriteria
|
|
11695
|
+
} : selection);
|
|
10170
11696
|
}
|
|
10171
11697
|
getSession(sessionId) {
|
|
10172
11698
|
if (!this.manager)
|
|
@@ -10180,7 +11706,10 @@ class PTYService {
|
|
|
10180
11706
|
if (!this.manager)
|
|
10181
11707
|
return [];
|
|
10182
11708
|
const sessions = this.usingBunWorker ? await this.manager.list() : this.manager.list(filter);
|
|
10183
|
-
const liveSessions = sessions.map((
|
|
11709
|
+
const liveSessions = sessions.map((session) => {
|
|
11710
|
+
const cached = this.manager?.get(session.id);
|
|
11711
|
+
return this.toSessionInfo(cached ?? session, this.sessionWorkdirs.get(session.id));
|
|
11712
|
+
});
|
|
10184
11713
|
const terminalSessions = Array.from(this.terminalSessionStates.keys()).filter((sessionId) => !sessions.some((session) => session.id === sessionId)).map((sessionId) => this.toTerminalSessionInfo(sessionId)).filter((session) => session !== undefined);
|
|
10185
11714
|
return [...liveSessions, ...terminalSessions];
|
|
10186
11715
|
}
|
|
@@ -10198,9 +11727,9 @@ class PTYService {
|
|
|
10198
11727
|
if (!this.manager)
|
|
10199
11728
|
return false;
|
|
10200
11729
|
if (this.usingBunWorker) {
|
|
10201
|
-
return this.manager.isSessionLoading(sessionId);
|
|
11730
|
+
return this.manager.isSessionLoading?.(sessionId) ?? false;
|
|
10202
11731
|
}
|
|
10203
|
-
return this.manager.isSessionLoading(sessionId);
|
|
11732
|
+
return this.manager.isSessionLoading?.(sessionId) ?? false;
|
|
10204
11733
|
}
|
|
10205
11734
|
clearTranscriptCapture(sessionId) {
|
|
10206
11735
|
const unsubscribe = this.transcriptUnsubscribers.get(sessionId);
|
|
@@ -10280,20 +11809,21 @@ class PTYService {
|
|
|
10280
11809
|
}
|
|
10281
11810
|
switch (event) {
|
|
10282
11811
|
case "tool_running":
|
|
10283
|
-
this.emitEvent(sessionId, "tool_running", data);
|
|
11812
|
+
this.emitEvent(sessionId, "tool_running", { ...data, source: "hook" });
|
|
10284
11813
|
break;
|
|
10285
11814
|
case "task_complete":
|
|
10286
|
-
this.emitEvent(sessionId, "task_complete", data);
|
|
11815
|
+
this.emitEvent(sessionId, "task_complete", { ...data, source: "hook" });
|
|
10287
11816
|
break;
|
|
10288
11817
|
case "permission_approved":
|
|
10289
11818
|
break;
|
|
10290
11819
|
case "notification":
|
|
10291
|
-
this.emitEvent(sessionId, "message", data);
|
|
11820
|
+
this.emitEvent(sessionId, "message", { ...data, source: "hook" });
|
|
10292
11821
|
break;
|
|
10293
11822
|
case "session_end":
|
|
10294
11823
|
this.emitEvent(sessionId, "stopped", {
|
|
10295
11824
|
...data,
|
|
10296
|
-
reason: "session_end"
|
|
11825
|
+
reason: "session_end",
|
|
11826
|
+
source: "hook"
|
|
10297
11827
|
});
|
|
10298
11828
|
break;
|
|
10299
11829
|
default:
|
|
@@ -10330,6 +11860,7 @@ class PTYService {
|
|
|
10330
11860
|
manager: this.manager,
|
|
10331
11861
|
metricsTracker: this.metricsTracker,
|
|
10332
11862
|
debugSnapshots: this.serviceConfig.debug === true,
|
|
11863
|
+
lastSentInput: typeof meta?.lastSentInput === "string" ? meta.lastSentInput : undefined,
|
|
10333
11864
|
log: (msg) => this.log(msg),
|
|
10334
11865
|
taskContext: {
|
|
10335
11866
|
sessionId: taskCtx.sessionId,
|
|
@@ -10359,6 +11890,7 @@ class PTYService {
|
|
|
10359
11890
|
manager: this.manager,
|
|
10360
11891
|
metricsTracker: this.metricsTracker,
|
|
10361
11892
|
debugSnapshots: this.serviceConfig.debug === true,
|
|
11893
|
+
lastSentInput: typeof meta?.lastSentInput === "string" ? meta.lastSentInput : undefined,
|
|
10362
11894
|
log: (msg) => this.log(msg)
|
|
10363
11895
|
});
|
|
10364
11896
|
if (classification && meta?.coordinatorManaged && classification.suggestedResponse) {
|
|
@@ -10422,7 +11954,7 @@ class PTYService {
|
|
|
10422
11954
|
];
|
|
10423
11955
|
try {
|
|
10424
11956
|
if (existing.length === 0) {
|
|
10425
|
-
await
|
|
11957
|
+
await writeFile5(gitignorePath, `${entries.join(`
|
|
10426
11958
|
`)}
|
|
10427
11959
|
`, "utf-8");
|
|
10428
11960
|
} else {
|
|
@@ -10445,6 +11977,14 @@ class PTYService {
|
|
|
10445
11977
|
this.eventCallbacks.splice(idx, 1);
|
|
10446
11978
|
};
|
|
10447
11979
|
}
|
|
11980
|
+
onNormalizedSessionEvent(callback) {
|
|
11981
|
+
this.normalizedEventCallbacks.push(callback);
|
|
11982
|
+
return () => {
|
|
11983
|
+
const idx = this.normalizedEventCallbacks.indexOf(callback);
|
|
11984
|
+
if (idx !== -1)
|
|
11985
|
+
this.normalizedEventCallbacks.splice(idx, 1);
|
|
11986
|
+
};
|
|
11987
|
+
}
|
|
10448
11988
|
registerAdapter(adapter) {
|
|
10449
11989
|
if (!this.manager) {
|
|
10450
11990
|
throw new Error("PTYService not initialized");
|
|
@@ -10491,6 +12031,12 @@ class PTYService {
|
|
|
10491
12031
|
};
|
|
10492
12032
|
}
|
|
10493
12033
|
emitEvent(sessionId, event, data) {
|
|
12034
|
+
if (event === "blocked" && this.shouldSuppressBlockedEvent(sessionId, data)) {
|
|
12035
|
+
return;
|
|
12036
|
+
}
|
|
12037
|
+
if (event === "ready" || event === "task_complete" || event === "stopped" || event === "error") {
|
|
12038
|
+
this.clearCompletionReconcile(sessionId);
|
|
12039
|
+
}
|
|
10494
12040
|
if (event === "stopped" || event === "error") {
|
|
10495
12041
|
const liveSession = this.manager?.get(sessionId);
|
|
10496
12042
|
const createdAt = liveSession?.startedAt instanceof Date ? liveSession.startedAt : liveSession?.startedAt ? new Date(liveSession.startedAt) : new Date;
|
|
@@ -10510,6 +12056,16 @@ class PTYService {
|
|
|
10510
12056
|
this.log(`Event callback error: ${err}`);
|
|
10511
12057
|
}
|
|
10512
12058
|
}
|
|
12059
|
+
const normalized = normalizeCoordinatorEvent(sessionId, event, data);
|
|
12060
|
+
if (!normalized)
|
|
12061
|
+
return;
|
|
12062
|
+
for (const callback of this.normalizedEventCallbacks) {
|
|
12063
|
+
try {
|
|
12064
|
+
callback(normalized);
|
|
12065
|
+
} catch (err) {
|
|
12066
|
+
this.log(`Normalized event callback error: ${err}`);
|
|
12067
|
+
}
|
|
12068
|
+
}
|
|
10513
12069
|
}
|
|
10514
12070
|
getAgentMetrics() {
|
|
10515
12071
|
return this.metricsTracker.getAll();
|
|
@@ -10525,17 +12081,146 @@ class PTYService {
|
|
|
10525
12081
|
if (trackedSessionIds.size === 0) {
|
|
10526
12082
|
return;
|
|
10527
12083
|
}
|
|
10528
|
-
const reason = info.signal ? `PTY worker exited unexpectedly (signal ${info.signal})` : `PTY worker exited unexpectedly (code ${info.code ?? "unknown"})`;
|
|
10529
|
-
for (const sessionId of trackedSessionIds) {
|
|
10530
|
-
const terminalState = this.terminalSessionStates.get(sessionId);
|
|
10531
|
-
if (terminalState?.status === "stopped" || terminalState?.status === "error") {
|
|
10532
|
-
continue;
|
|
10533
|
-
}
|
|
10534
|
-
this.emitEvent(sessionId, "error", {
|
|
10535
|
-
message: reason,
|
|
10536
|
-
workerExit: info
|
|
10537
|
-
|
|
12084
|
+
const reason = info.signal ? `PTY worker exited unexpectedly (signal ${info.signal})` : `PTY worker exited unexpectedly (code ${info.code ?? "unknown"})`;
|
|
12085
|
+
for (const sessionId of trackedSessionIds) {
|
|
12086
|
+
const terminalState = this.terminalSessionStates.get(sessionId);
|
|
12087
|
+
if (terminalState?.status === "stopped" || terminalState?.status === "error") {
|
|
12088
|
+
continue;
|
|
12089
|
+
}
|
|
12090
|
+
this.emitEvent(sessionId, "error", {
|
|
12091
|
+
message: reason,
|
|
12092
|
+
workerExit: info,
|
|
12093
|
+
source: "pty_manager"
|
|
12094
|
+
});
|
|
12095
|
+
}
|
|
12096
|
+
}
|
|
12097
|
+
clearCompletionReconcile(sessionId) {
|
|
12098
|
+
const timer = this.completionReconcileTimers.get(sessionId);
|
|
12099
|
+
if (timer) {
|
|
12100
|
+
clearInterval(timer);
|
|
12101
|
+
this.completionReconcileTimers.delete(sessionId);
|
|
12102
|
+
}
|
|
12103
|
+
this.completionSignalSince.delete(sessionId);
|
|
12104
|
+
}
|
|
12105
|
+
scheduleCompletionReconcile(sessionId) {
|
|
12106
|
+
this.clearCompletionReconcile(sessionId);
|
|
12107
|
+
const timer = setInterval(() => {
|
|
12108
|
+
this.reconcileBusySessionFromOutput(sessionId);
|
|
12109
|
+
}, 1000);
|
|
12110
|
+
this.completionReconcileTimers.set(sessionId, timer);
|
|
12111
|
+
this.reconcileBusySessionFromOutput(sessionId);
|
|
12112
|
+
}
|
|
12113
|
+
isAdapterBackedAgentType(value) {
|
|
12114
|
+
return value === "claude" || value === "gemini" || value === "codex" || value === "aider" || value === "hermes";
|
|
12115
|
+
}
|
|
12116
|
+
shouldSuppressBlockedEvent(sessionId, data) {
|
|
12117
|
+
const payload = data;
|
|
12118
|
+
if (payload?.source !== "pty_manager") {
|
|
12119
|
+
return false;
|
|
12120
|
+
}
|
|
12121
|
+
const promptInfo = payload.promptInfo && typeof payload.promptInfo === "object" && !Array.isArray(payload.promptInfo) ? payload.promptInfo : undefined;
|
|
12122
|
+
if (!promptInfo) {
|
|
12123
|
+
return false;
|
|
12124
|
+
}
|
|
12125
|
+
const promptType = typeof promptInfo.type === "string" ? promptInfo.type.toLowerCase() : "";
|
|
12126
|
+
if (promptType && promptType !== "unknown") {
|
|
12127
|
+
return false;
|
|
12128
|
+
}
|
|
12129
|
+
const promptText = typeof promptInfo.prompt === "string" ? cleanForChat(promptInfo.prompt) : "";
|
|
12130
|
+
if (!promptText) {
|
|
12131
|
+
return false;
|
|
12132
|
+
}
|
|
12133
|
+
const compactPrompt = promptText.replace(/\s+/g, " ").trim();
|
|
12134
|
+
const hasWorkspacePath = /(\/private\/|\/var\/folders\/)/.test(compactPrompt);
|
|
12135
|
+
const looksLikeWorkingStatus = /working \(\d+s .*esc to interrupt\)/i.test(compactPrompt) || /messages to be submitted after next tool call/i.test(compactPrompt) || /find and fix a bug in @filename/i.test(compactPrompt) || /use \/skills to list available skills/i.test(compactPrompt);
|
|
12136
|
+
const looksLikeSpinnerTail = /\b\d+% left\b/i.test(compactPrompt) && hasWorkspacePath;
|
|
12137
|
+
const looksLikeSpinnerFragments2 = hasWorkspacePath && /(?:\bW Wo\b|• Wor|• Work|Worki|Workin|Working)/i.test(compactPrompt);
|
|
12138
|
+
if (!looksLikeWorkingStatus && !looksLikeSpinnerTail && !looksLikeSpinnerFragments2) {
|
|
12139
|
+
return false;
|
|
12140
|
+
}
|
|
12141
|
+
this.log(`Suppressing false blocked prompt noise for ${sessionId}: ${compactPrompt.slice(0, 160)}`);
|
|
12142
|
+
return true;
|
|
12143
|
+
}
|
|
12144
|
+
responseLooksMeaningful(response, rawOutput) {
|
|
12145
|
+
if (extractCompletionSummary(rawOutput).trim().length > 0) {
|
|
12146
|
+
return true;
|
|
12147
|
+
}
|
|
12148
|
+
const cleaned = response.trim();
|
|
12149
|
+
if (!cleaned)
|
|
12150
|
+
return false;
|
|
12151
|
+
const substantiveLines = cleaned.split(`
|
|
12152
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => !line.startsWith("› ") && !/^Work(?:i|in|ing)?(?:\s+\d+)?$/i.test(line) && !/^\d+% left\b/i.test(line) && !/context left/i.test(line) && !/esc to interrupt/i.test(line) && !/Use \/skills/i.test(line) && !/Messages to be submitted after next tool call/i.test(line));
|
|
12153
|
+
if (substantiveLines.some((line) => /\b(Added|Created|Creating|Updated|Wrote|Deleted|Renamed|Verified|Completed|Finished|Saved|Ran|LIVE_)\b/i.test(line))) {
|
|
12154
|
+
return true;
|
|
12155
|
+
}
|
|
12156
|
+
return false;
|
|
12157
|
+
}
|
|
12158
|
+
async reconcileBusySessionFromOutput(sessionId) {
|
|
12159
|
+
if (!this.manager) {
|
|
12160
|
+
this.clearCompletionReconcile(sessionId);
|
|
12161
|
+
return;
|
|
12162
|
+
}
|
|
12163
|
+
const liveSession = this.manager.get(sessionId);
|
|
12164
|
+
if (!liveSession) {
|
|
12165
|
+
this.clearCompletionReconcile(sessionId);
|
|
12166
|
+
return;
|
|
12167
|
+
}
|
|
12168
|
+
if (liveSession.status !== "busy") {
|
|
12169
|
+
this.clearCompletionReconcile(sessionId);
|
|
12170
|
+
return;
|
|
12171
|
+
}
|
|
12172
|
+
const agentType = this.sessionMetadata.get(sessionId)?.agentType;
|
|
12173
|
+
if (!this.isAdapterBackedAgentType(agentType)) {
|
|
12174
|
+
this.clearCompletionReconcile(sessionId);
|
|
12175
|
+
return;
|
|
12176
|
+
}
|
|
12177
|
+
const adapter = this.getAdapter(agentType);
|
|
12178
|
+
const rawOutput = await this.getSessionOutput(sessionId);
|
|
12179
|
+
if (!rawOutput.trim()) {
|
|
12180
|
+
this.completionSignalSince.delete(sessionId);
|
|
12181
|
+
return;
|
|
12182
|
+
}
|
|
12183
|
+
if (adapter.detectLoading?.(rawOutput)) {
|
|
12184
|
+
this.completionSignalSince.delete(sessionId);
|
|
12185
|
+
return;
|
|
12186
|
+
}
|
|
12187
|
+
if (adapter.detectLogin(rawOutput).required) {
|
|
12188
|
+
this.completionSignalSince.delete(sessionId);
|
|
12189
|
+
return;
|
|
12190
|
+
}
|
|
12191
|
+
if (adapter.detectBlockingPrompt(rawOutput).detected) {
|
|
12192
|
+
this.completionSignalSince.delete(sessionId);
|
|
12193
|
+
return;
|
|
12194
|
+
}
|
|
12195
|
+
const completionSignal = adapter.detectTaskComplete ? adapter.detectTaskComplete(rawOutput) : adapter.detectReady(rawOutput);
|
|
12196
|
+
if (!completionSignal) {
|
|
12197
|
+
this.completionSignalSince.delete(sessionId);
|
|
12198
|
+
return;
|
|
12199
|
+
}
|
|
12200
|
+
const previewResponse = this.taskResponseMarkers.has(sessionId) ? peekTaskResponse(sessionId, this.sessionOutputBuffers, this.taskResponseMarkers) : cleanForChat(rawOutput);
|
|
12201
|
+
if (!this.responseLooksMeaningful(previewResponse, rawOutput)) {
|
|
12202
|
+
this.completionSignalSince.delete(sessionId);
|
|
12203
|
+
return;
|
|
12204
|
+
}
|
|
12205
|
+
const firstSeenAt = this.completionSignalSince.get(sessionId);
|
|
12206
|
+
if (firstSeenAt === undefined) {
|
|
12207
|
+
this.completionSignalSince.set(sessionId, Date.now());
|
|
12208
|
+
return;
|
|
10538
12209
|
}
|
|
12210
|
+
if (Date.now() - firstSeenAt < 2500) {
|
|
12211
|
+
return;
|
|
12212
|
+
}
|
|
12213
|
+
const response = this.taskResponseMarkers.has(sessionId) ? captureTaskResponse(sessionId, this.sessionOutputBuffers, this.taskResponseMarkers) : previewResponse;
|
|
12214
|
+
const durationMs = liveSession.startedAt ? Date.now() - new Date(liveSession.startedAt).getTime() : 0;
|
|
12215
|
+
liveSession.status = "ready";
|
|
12216
|
+
liveSession.lastActivityAt = new Date;
|
|
12217
|
+
this.metricsTracker.recordCompletion(agentType, "output-reconcile", durationMs);
|
|
12218
|
+
this.log(`Reconciled ${sessionId} from busy to task_complete using stable adapter output`);
|
|
12219
|
+
this.emitEvent(sessionId, "task_complete", {
|
|
12220
|
+
session: liveSession,
|
|
12221
|
+
response,
|
|
12222
|
+
source: "output_reconcile"
|
|
12223
|
+
});
|
|
10539
12224
|
}
|
|
10540
12225
|
}
|
|
10541
12226
|
|
|
@@ -10583,7 +12268,16 @@ var listAgentsAction = {
|
|
|
10583
12268
|
const ptyService = runtime.getService("PTY_SERVICE");
|
|
10584
12269
|
return ptyService != null;
|
|
10585
12270
|
},
|
|
10586
|
-
handler: async (runtime,
|
|
12271
|
+
handler: async (runtime, message, _state, _options, callback) => {
|
|
12272
|
+
const access = await requireTaskAgentAccess(runtime, message, "interact");
|
|
12273
|
+
if (!access.allowed) {
|
|
12274
|
+
if (callback) {
|
|
12275
|
+
await callback({
|
|
12276
|
+
text: access.reason
|
|
12277
|
+
});
|
|
12278
|
+
}
|
|
12279
|
+
return { success: false, error: "FORBIDDEN", text: access.reason };
|
|
12280
|
+
}
|
|
10587
12281
|
const ptyService = runtime.getService("PTY_SERVICE");
|
|
10588
12282
|
if (!ptyService) {
|
|
10589
12283
|
if (callback) {
|
|
@@ -10682,169 +12376,6 @@ var listAgentsAction = {
|
|
|
10682
12376
|
};
|
|
10683
12377
|
var listTaskAgentsAction = listAgentsAction;
|
|
10684
12378
|
|
|
10685
|
-
// src/services/task-policy.ts
|
|
10686
|
-
var ROLE_RANK = {
|
|
10687
|
-
GUEST: 0,
|
|
10688
|
-
USER: 1,
|
|
10689
|
-
ADMIN: 2,
|
|
10690
|
-
OWNER: 3
|
|
10691
|
-
};
|
|
10692
|
-
var DEFAULT_POLICY = {
|
|
10693
|
-
default: "GUEST",
|
|
10694
|
-
connectors: {
|
|
10695
|
-
discord: {
|
|
10696
|
-
create: "ADMIN",
|
|
10697
|
-
interact: "ADMIN"
|
|
10698
|
-
}
|
|
10699
|
-
}
|
|
10700
|
-
};
|
|
10701
|
-
function normalizeRole(value) {
|
|
10702
|
-
const upper = typeof value === "string" ? value.trim().toUpperCase() : "";
|
|
10703
|
-
switch (upper) {
|
|
10704
|
-
case "OWNER":
|
|
10705
|
-
case "ADMIN":
|
|
10706
|
-
case "USER":
|
|
10707
|
-
return upper;
|
|
10708
|
-
default:
|
|
10709
|
-
return "GUEST";
|
|
10710
|
-
}
|
|
10711
|
-
}
|
|
10712
|
-
function normalizeConnectorPolicy(value) {
|
|
10713
|
-
if (!value)
|
|
10714
|
-
return {};
|
|
10715
|
-
if (typeof value === "string") {
|
|
10716
|
-
const role = normalizeRole(value);
|
|
10717
|
-
return {
|
|
10718
|
-
create: role,
|
|
10719
|
-
interact: role
|
|
10720
|
-
};
|
|
10721
|
-
}
|
|
10722
|
-
return {
|
|
10723
|
-
...value.create ? { create: normalizeRole(value.create) } : {},
|
|
10724
|
-
...value.interact ? { interact: normalizeRole(value.interact) } : {}
|
|
10725
|
-
};
|
|
10726
|
-
}
|
|
10727
|
-
function parseTaskAgentPolicy(runtime) {
|
|
10728
|
-
const configured = runtime.getSetting("TASK_AGENT_ROLE_POLICY") ?? runtime.getSetting("TASK_AGENT_CONNECTOR_ROLE_POLICY");
|
|
10729
|
-
if (!configured) {
|
|
10730
|
-
return DEFAULT_POLICY;
|
|
10731
|
-
}
|
|
10732
|
-
let parsed = configured;
|
|
10733
|
-
if (typeof configured === "string") {
|
|
10734
|
-
try {
|
|
10735
|
-
parsed = JSON.parse(configured);
|
|
10736
|
-
} catch {
|
|
10737
|
-
return DEFAULT_POLICY;
|
|
10738
|
-
}
|
|
10739
|
-
}
|
|
10740
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
10741
|
-
return DEFAULT_POLICY;
|
|
10742
|
-
}
|
|
10743
|
-
const record = parsed;
|
|
10744
|
-
const connectors = record.connectors && typeof record.connectors === "object" && !Array.isArray(record.connectors) ? Object.fromEntries(Object.entries(record.connectors).map(([connector, value]) => [
|
|
10745
|
-
connector,
|
|
10746
|
-
normalizeConnectorPolicy(value)
|
|
10747
|
-
])) : DEFAULT_POLICY.connectors;
|
|
10748
|
-
return {
|
|
10749
|
-
default: normalizeConnectorPolicy(record.default ?? DEFAULT_POLICY.default),
|
|
10750
|
-
connectors
|
|
10751
|
-
};
|
|
10752
|
-
}
|
|
10753
|
-
function getConnectorFromBridgeMetadata(message) {
|
|
10754
|
-
const metadata = message.content?.metadata;
|
|
10755
|
-
if (!metadata || typeof metadata !== "object")
|
|
10756
|
-
return null;
|
|
10757
|
-
const bridgeSender = metadata.bridgeSender;
|
|
10758
|
-
if (!bridgeSender || typeof bridgeSender !== "object")
|
|
10759
|
-
return null;
|
|
10760
|
-
const liveMetadata = bridgeSender.metadata;
|
|
10761
|
-
if (!liveMetadata || typeof liveMetadata !== "object")
|
|
10762
|
-
return null;
|
|
10763
|
-
for (const [connector, value] of Object.entries(liveMetadata)) {
|
|
10764
|
-
if (value && typeof value === "object") {
|
|
10765
|
-
return connector;
|
|
10766
|
-
}
|
|
10767
|
-
}
|
|
10768
|
-
return null;
|
|
10769
|
-
}
|
|
10770
|
-
async function resolveConnectorSource(runtime, message) {
|
|
10771
|
-
const content = message.content;
|
|
10772
|
-
const directSource = typeof content?.source === "string" && content.source !== "client_chat" ? content.source : null;
|
|
10773
|
-
if (directSource)
|
|
10774
|
-
return directSource;
|
|
10775
|
-
const bridgeSource = getConnectorFromBridgeMetadata(message);
|
|
10776
|
-
if (bridgeSource)
|
|
10777
|
-
return bridgeSource;
|
|
10778
|
-
try {
|
|
10779
|
-
const room = await runtime.getRoom(message.roomId);
|
|
10780
|
-
if (typeof room?.source === "string" && room.source.trim().length > 0) {
|
|
10781
|
-
return room.source;
|
|
10782
|
-
}
|
|
10783
|
-
} catch {}
|
|
10784
|
-
return null;
|
|
10785
|
-
}
|
|
10786
|
-
async function resolveSenderRole(runtime, message) {
|
|
10787
|
-
try {
|
|
10788
|
-
const rolesModuleSpecifier = "@miladyai/plugin-roles";
|
|
10789
|
-
const rolesModule = await import(rolesModuleSpecifier);
|
|
10790
|
-
if (typeof rolesModule.checkSenderRole !== "function") {
|
|
10791
|
-
return null;
|
|
10792
|
-
}
|
|
10793
|
-
return await rolesModule.checkSenderRole(runtime, message);
|
|
10794
|
-
} catch {
|
|
10795
|
-
return null;
|
|
10796
|
-
}
|
|
10797
|
-
}
|
|
10798
|
-
async function requireTaskAgentAccess(runtime, message, ability) {
|
|
10799
|
-
if (message.entityId === runtime.agentId) {
|
|
10800
|
-
return {
|
|
10801
|
-
allowed: true,
|
|
10802
|
-
connector: null,
|
|
10803
|
-
requiredRole: "GUEST",
|
|
10804
|
-
actualRole: "OWNER"
|
|
10805
|
-
};
|
|
10806
|
-
}
|
|
10807
|
-
const connector = await resolveConnectorSource(runtime, message);
|
|
10808
|
-
const policy = parseTaskAgentPolicy(runtime);
|
|
10809
|
-
const connectorPolicy = connector ? normalizeConnectorPolicy(policy.connectors?.[connector]) : {};
|
|
10810
|
-
const defaultPolicy = normalizeConnectorPolicy(policy.default);
|
|
10811
|
-
const requiredRole = connectorPolicy[ability] ?? defaultPolicy[ability] ?? "GUEST";
|
|
10812
|
-
if (requiredRole === "GUEST") {
|
|
10813
|
-
return {
|
|
10814
|
-
allowed: true,
|
|
10815
|
-
connector,
|
|
10816
|
-
requiredRole,
|
|
10817
|
-
actualRole: "GUEST"
|
|
10818
|
-
};
|
|
10819
|
-
}
|
|
10820
|
-
const roleCheck = await resolveSenderRole(runtime, message);
|
|
10821
|
-
if (!roleCheck) {
|
|
10822
|
-
return {
|
|
10823
|
-
allowed: false,
|
|
10824
|
-
connector,
|
|
10825
|
-
requiredRole,
|
|
10826
|
-
actualRole: "GUEST",
|
|
10827
|
-
reason: connector === "discord" ? "Task-agent access in Discord requires a verified OWNER or ADMIN role." : "Task-agent access requires a verified role, but role context is unavailable."
|
|
10828
|
-
};
|
|
10829
|
-
}
|
|
10830
|
-
const actualRole = normalizeRole(roleCheck.role);
|
|
10831
|
-
if (ROLE_RANK[actualRole] < ROLE_RANK[requiredRole]) {
|
|
10832
|
-
return {
|
|
10833
|
-
allowed: false,
|
|
10834
|
-
connector,
|
|
10835
|
-
requiredRole,
|
|
10836
|
-
actualRole,
|
|
10837
|
-
reason: connector === "discord" ? `Task-agent access in Discord requires ${requiredRole} or higher. Current role: ${actualRole}.` : `Task-agent access requires ${requiredRole} or higher. Current role: ${actualRole}.`
|
|
10838
|
-
};
|
|
10839
|
-
}
|
|
10840
|
-
return {
|
|
10841
|
-
allowed: true,
|
|
10842
|
-
connector,
|
|
10843
|
-
requiredRole,
|
|
10844
|
-
actualRole
|
|
10845
|
-
};
|
|
10846
|
-
}
|
|
10847
|
-
|
|
10848
12379
|
// src/actions/task-thread-target.ts
|
|
10849
12380
|
function stringValue(value) {
|
|
10850
12381
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
@@ -11400,17 +12931,17 @@ var taskHistoryAction = {
|
|
|
11400
12931
|
// src/services/task-share.ts
|
|
11401
12932
|
init_ansi_utils();
|
|
11402
12933
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
11403
|
-
import
|
|
11404
|
-
import
|
|
12934
|
+
import os5 from "node:os";
|
|
12935
|
+
import path9 from "node:path";
|
|
11405
12936
|
var URL_RE = /\bhttps?:\/\/[^\s<>"'`]+/gi;
|
|
11406
12937
|
function resolveConfigPath() {
|
|
11407
12938
|
const explicit = process.env.MILADY_CONFIG_PATH?.trim() || process.env.ELIZA_CONFIG_PATH?.trim();
|
|
11408
12939
|
if (explicit)
|
|
11409
12940
|
return explicit;
|
|
11410
|
-
const stateDir = process.env.MILADY_STATE_DIR?.trim() || process.env.ELIZA_STATE_DIR?.trim() ||
|
|
12941
|
+
const stateDir = process.env.MILADY_STATE_DIR?.trim() || process.env.ELIZA_STATE_DIR?.trim() || path9.join(os5.homedir(), ".milady");
|
|
11411
12942
|
const namespace = process.env.ELIZA_NAMESPACE?.trim();
|
|
11412
12943
|
const filename = !namespace || namespace === "milady" ? "milady.json" : `${namespace}.json`;
|
|
11413
|
-
return
|
|
12944
|
+
return path9.join(stateDir, filename);
|
|
11414
12945
|
}
|
|
11415
12946
|
function readMiladyConfig() {
|
|
11416
12947
|
try {
|
|
@@ -11758,6 +13289,13 @@ var manageIssuesAction = {
|
|
|
11758
13289
|
return workspaceService != null;
|
|
11759
13290
|
},
|
|
11760
13291
|
handler: async (runtime, message, _state, options, callback) => {
|
|
13292
|
+
const access = await requireTaskAgentAccess(runtime, message, "interact");
|
|
13293
|
+
if (!access.allowed) {
|
|
13294
|
+
if (callback) {
|
|
13295
|
+
await callback({ text: access.reason });
|
|
13296
|
+
}
|
|
13297
|
+
return { success: false, error: "FORBIDDEN", text: access.reason };
|
|
13298
|
+
}
|
|
11761
13299
|
const workspaceService = runtime.getService("CODING_WORKSPACE_SERVICE");
|
|
11762
13300
|
if (!workspaceService) {
|
|
11763
13301
|
if (callback) {
|
|
@@ -12140,6 +13678,15 @@ var provisionWorkspaceAction = {
|
|
|
12140
13678
|
return workspaceService != null;
|
|
12141
13679
|
},
|
|
12142
13680
|
handler: async (runtime, message, state, _options, callback) => {
|
|
13681
|
+
const access = await requireTaskAgentAccess(runtime, message, "create");
|
|
13682
|
+
if (!access.allowed) {
|
|
13683
|
+
if (callback) {
|
|
13684
|
+
await callback({
|
|
13685
|
+
text: access.reason
|
|
13686
|
+
});
|
|
13687
|
+
}
|
|
13688
|
+
return { success: false, error: "FORBIDDEN", text: access.reason };
|
|
13689
|
+
}
|
|
12143
13690
|
const workspaceService = runtime.getService("CODING_WORKSPACE_SERVICE");
|
|
12144
13691
|
if (!workspaceService) {
|
|
12145
13692
|
if (callback) {
|
|
@@ -12452,6 +13999,7 @@ var sendToAgentAction = {
|
|
|
12452
13999
|
...existingTask?.repo ? { repo: existingTask.repo } : {},
|
|
12453
14000
|
metadata: session.metadata && typeof session.metadata === "object" && !Array.isArray(session.metadata) ? session.metadata : undefined
|
|
12454
14001
|
});
|
|
14002
|
+
await coordinator.setTaskDelivered(sessionId);
|
|
12455
14003
|
}
|
|
12456
14004
|
}
|
|
12457
14005
|
if (callback) {
|
|
@@ -12518,8 +14066,8 @@ var sendToAgentAction = {
|
|
|
12518
14066
|
var sendToTaskAgentAction = sendToAgentAction;
|
|
12519
14067
|
|
|
12520
14068
|
// src/actions/spawn-agent.ts
|
|
12521
|
-
import * as
|
|
12522
|
-
import * as
|
|
14069
|
+
import * as os6 from "node:os";
|
|
14070
|
+
import * as path10 from "node:path";
|
|
12523
14071
|
import {
|
|
12524
14072
|
logger as logger5
|
|
12525
14073
|
} from "@elizaos/core";
|
|
@@ -12576,6 +14124,15 @@ var spawnAgentAction = {
|
|
|
12576
14124
|
return true;
|
|
12577
14125
|
},
|
|
12578
14126
|
handler: async (runtime, message, state, options, callback) => {
|
|
14127
|
+
const access = await requireTaskAgentAccess(runtime, message, "create");
|
|
14128
|
+
if (!access.allowed) {
|
|
14129
|
+
if (callback) {
|
|
14130
|
+
await callback({
|
|
14131
|
+
text: access.reason
|
|
14132
|
+
});
|
|
14133
|
+
}
|
|
14134
|
+
return { success: false, error: "FORBIDDEN", text: access.reason };
|
|
14135
|
+
}
|
|
12579
14136
|
const ptyService = runtime.getService("PTY_SERVICE");
|
|
12580
14137
|
if (!ptyService) {
|
|
12581
14138
|
if (callback) {
|
|
@@ -12588,9 +14145,12 @@ var spawnAgentAction = {
|
|
|
12588
14145
|
const params = options?.parameters;
|
|
12589
14146
|
const content = message.content;
|
|
12590
14147
|
const explicitRawType = params?.agentType ?? content.agentType;
|
|
12591
|
-
const rawAgentType = explicitRawType ?? await ptyService.resolveAgentType();
|
|
12592
|
-
const agentType = normalizeAgentType(rawAgentType);
|
|
12593
14148
|
const task = params?.task ?? content.task;
|
|
14149
|
+
const rawAgentType = explicitRawType ?? await ptyService.resolveAgentType({
|
|
14150
|
+
task,
|
|
14151
|
+
workdir: (params?.workdir ?? content.workdir) || undefined
|
|
14152
|
+
});
|
|
14153
|
+
const agentType = normalizeAgentType(rawAgentType);
|
|
12594
14154
|
const piRequested = isPiAgentType(rawAgentType);
|
|
12595
14155
|
const initialTask = piRequested ? toPiCommand(task) : task;
|
|
12596
14156
|
let workdir = params?.workdir ?? content.workdir;
|
|
@@ -12614,13 +14174,13 @@ var spawnAgentAction = {
|
|
|
12614
14174
|
}
|
|
12615
14175
|
return { success: false, error: "NO_WORKSPACE" };
|
|
12616
14176
|
}
|
|
12617
|
-
const resolvedWorkdir =
|
|
12618
|
-
const workspaceBaseDir =
|
|
14177
|
+
const resolvedWorkdir = path10.resolve(workdir);
|
|
14178
|
+
const workspaceBaseDir = path10.join(os6.homedir(), ".milady", "workspaces");
|
|
12619
14179
|
const allowedPrefixes = [
|
|
12620
|
-
|
|
12621
|
-
|
|
14180
|
+
path10.resolve(workspaceBaseDir),
|
|
14181
|
+
path10.resolve(process.cwd())
|
|
12622
14182
|
];
|
|
12623
|
-
const isAllowed = allowedPrefixes.some((prefix) => resolvedWorkdir.startsWith(prefix +
|
|
14183
|
+
const isAllowed = allowedPrefixes.some((prefix) => resolvedWorkdir.startsWith(prefix + path10.sep) || resolvedWorkdir === prefix);
|
|
12624
14184
|
if (!isAllowed) {
|
|
12625
14185
|
if (callback) {
|
|
12626
14186
|
await callback({
|
|
@@ -12642,6 +14202,8 @@ var spawnAgentAction = {
|
|
|
12642
14202
|
customCredentials[key] = val;
|
|
12643
14203
|
}
|
|
12644
14204
|
}
|
|
14205
|
+
const rawAnthropicKey = runtime.getSetting("ANTHROPIC_API_KEY");
|
|
14206
|
+
customCredentials = sanitizeCustomCredentials(customCredentials, isAnthropicOAuthToken(rawAnthropicKey) ? [rawAnthropicKey] : []);
|
|
12645
14207
|
const llmProvider = readConfigEnvKey("PARALLAX_LLM_PROVIDER") || "subscription";
|
|
12646
14208
|
let credentials;
|
|
12647
14209
|
try {
|
|
@@ -12813,7 +14375,7 @@ import {
|
|
|
12813
14375
|
// src/actions/coding-task-handlers.ts
|
|
12814
14376
|
import {
|
|
12815
14377
|
logger as logger7,
|
|
12816
|
-
ModelType as
|
|
14378
|
+
ModelType as ModelType8
|
|
12817
14379
|
} from "@elizaos/core";
|
|
12818
14380
|
// src/services/trajectory-feedback.ts
|
|
12819
14381
|
import { logger as elizaLogger } from "@elizaos/core";
|
|
@@ -12828,13 +14390,13 @@ function withTimeout2(promise, ms) {
|
|
|
12828
14390
|
function getTrajectoryLogger(runtime) {
|
|
12829
14391
|
const runtimeAny = runtime;
|
|
12830
14392
|
if (typeof runtimeAny.getService === "function") {
|
|
12831
|
-
const svc = runtimeAny.getService("
|
|
14393
|
+
const svc = runtimeAny.getService("trajectories");
|
|
12832
14394
|
if (svc && typeof svc === "object" && hasListMethod(svc)) {
|
|
12833
14395
|
return svc;
|
|
12834
14396
|
}
|
|
12835
14397
|
}
|
|
12836
14398
|
if (typeof runtimeAny.getServicesByType === "function") {
|
|
12837
|
-
const services = runtimeAny.getServicesByType("
|
|
14399
|
+
const services = runtimeAny.getServicesByType("trajectories");
|
|
12838
14400
|
if (Array.isArray(services)) {
|
|
12839
14401
|
for (const svc of services) {
|
|
12840
14402
|
if (svc && typeof svc === "object" && hasListMethod(svc)) {
|
|
@@ -13000,9 +14562,9 @@ function formatAge(timestamp) {
|
|
|
13000
14562
|
|
|
13001
14563
|
// src/actions/coding-task-helpers.ts
|
|
13002
14564
|
import { randomUUID } from "node:crypto";
|
|
13003
|
-
import * as
|
|
13004
|
-
import * as
|
|
13005
|
-
import * as
|
|
14565
|
+
import * as fs4 from "node:fs";
|
|
14566
|
+
import * as os7 from "node:os";
|
|
14567
|
+
import * as path11 from "node:path";
|
|
13006
14568
|
import {
|
|
13007
14569
|
logger as logger6
|
|
13008
14570
|
} from "@elizaos/core";
|
|
@@ -13010,29 +14572,29 @@ function sanitizeDirName(label) {
|
|
|
13010
14572
|
return label.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "scratch";
|
|
13011
14573
|
}
|
|
13012
14574
|
function resolveNonColliding(baseDir, name2) {
|
|
13013
|
-
let candidate =
|
|
13014
|
-
if (!
|
|
14575
|
+
let candidate = path11.join(baseDir, name2);
|
|
14576
|
+
if (!fs4.existsSync(candidate))
|
|
13015
14577
|
return candidate;
|
|
13016
14578
|
for (let i = 2;i < 100; i++) {
|
|
13017
|
-
candidate =
|
|
13018
|
-
if (!
|
|
14579
|
+
candidate = path11.join(baseDir, `${name2}-${i}`);
|
|
14580
|
+
if (!fs4.existsSync(candidate))
|
|
13019
14581
|
return candidate;
|
|
13020
14582
|
}
|
|
13021
|
-
return
|
|
14583
|
+
return path11.join(baseDir, `${name2}-${randomUUID().slice(0, 8)}`);
|
|
13022
14584
|
}
|
|
13023
14585
|
function createScratchDir(runtime, label) {
|
|
13024
14586
|
const codingDir = runtime?.getSetting("PARALLAX_CODING_DIRECTORY") ?? readConfigEnvKey("PARALLAX_CODING_DIRECTORY") ?? process.env.PARALLAX_CODING_DIRECTORY;
|
|
13025
14587
|
if (codingDir?.trim()) {
|
|
13026
|
-
const resolved = codingDir.startsWith("~") ?
|
|
14588
|
+
const resolved = codingDir.startsWith("~") ? path11.join(os7.homedir(), codingDir.slice(1)) : path11.resolve(codingDir);
|
|
13027
14589
|
const dirName = label ? sanitizeDirName(label) : `scratch-${randomUUID().slice(0, 8)}`;
|
|
13028
14590
|
const scratchDir2 = resolveNonColliding(resolved, dirName);
|
|
13029
|
-
|
|
14591
|
+
fs4.mkdirSync(scratchDir2, { recursive: true });
|
|
13030
14592
|
return scratchDir2;
|
|
13031
14593
|
}
|
|
13032
|
-
const baseDir =
|
|
14594
|
+
const baseDir = path11.join(os7.homedir(), ".milady", "workspaces");
|
|
13033
14595
|
const scratchId = randomUUID();
|
|
13034
|
-
const scratchDir =
|
|
13035
|
-
|
|
14596
|
+
const scratchDir = path11.join(baseDir, scratchId);
|
|
14597
|
+
fs4.mkdirSync(scratchDir, { recursive: true });
|
|
13036
14598
|
return scratchDir;
|
|
13037
14599
|
}
|
|
13038
14600
|
function generateLabel(repo, task) {
|
|
@@ -13071,9 +14633,6 @@ function registerSessionEvents(ptyService, runtime, sessionId, label, scratchDir
|
|
|
13071
14633
|
${preview}` : `Agent "${label}" completed the task.`
|
|
13072
14634
|
});
|
|
13073
14635
|
}
|
|
13074
|
-
ptyService.stopSession(sessionId, true).catch((err) => {
|
|
13075
|
-
logger6.warn(`[START_CODING_TASK] Failed to stop session for "${label}" after task complete: ${err}`);
|
|
13076
|
-
});
|
|
13077
14636
|
}
|
|
13078
14637
|
if (event === "error" && callback) {
|
|
13079
14638
|
callback({
|
|
@@ -13171,7 +14730,7 @@ ${taskList}
|
|
|
13171
14730
|
|
|
13172
14731
|
` + `Output ONLY the bullet points, no preamble.`;
|
|
13173
14732
|
try {
|
|
13174
|
-
const result = await withTrajectoryContext(runtime, { source: "orchestrator", decisionType: "swarm-context-generation" }, () => runtime.useModel(
|
|
14733
|
+
const result = await withTrajectoryContext(runtime, { source: "orchestrator", decisionType: "swarm-context-generation" }, () => runtime.useModel(ModelType8.TEXT_SMALL, {
|
|
13175
14734
|
prompt,
|
|
13176
14735
|
temperature: 0.3,
|
|
13177
14736
|
stream: false
|
|
@@ -13195,6 +14754,7 @@ async function handleMultiAgent(ctx, agentsParam) {
|
|
|
13195
14754
|
repo,
|
|
13196
14755
|
defaultAgentType,
|
|
13197
14756
|
rawAgentType,
|
|
14757
|
+
agentTypeExplicit,
|
|
13198
14758
|
memoryContent,
|
|
13199
14759
|
approvalPreset,
|
|
13200
14760
|
explicitLabel
|
|
@@ -13261,15 +14821,17 @@ async function handleMultiAgent(ctx, agentsParam) {
|
|
|
13261
14821
|
} : { subtasks: cleanSubtasks },
|
|
13262
14822
|
metadata: evalMetadata.metadata
|
|
13263
14823
|
}) : null;
|
|
13264
|
-
const plannedAgents = agentSpecs.map((spec, i) => {
|
|
14824
|
+
const plannedAgents = await Promise.all(agentSpecs.map(async (spec, i) => {
|
|
13265
14825
|
let specAgentType = defaultAgentType;
|
|
13266
14826
|
let specPiRequested = isPiAgentType(rawAgentType);
|
|
13267
14827
|
let specRequestedType = rawAgentType;
|
|
13268
14828
|
let specTask = spec;
|
|
14829
|
+
let hasExplicitPrefix = false;
|
|
13269
14830
|
const colonIdx = spec.indexOf(":");
|
|
13270
14831
|
if (ctx.agentSelectionStrategy !== "fixed" && colonIdx > 0 && colonIdx < 20) {
|
|
13271
14832
|
const prefix = spec.slice(0, colonIdx).trim().toLowerCase();
|
|
13272
14833
|
if (KNOWN_AGENT_PREFIXES.includes(prefix)) {
|
|
14834
|
+
hasExplicitPrefix = true;
|
|
13273
14835
|
specRequestedType = prefix;
|
|
13274
14836
|
specPiRequested = isPiAgentType(prefix);
|
|
13275
14837
|
specAgentType = normalizeAgentType(prefix);
|
|
@@ -13279,6 +14841,15 @@ async function handleMultiAgent(ctx, agentsParam) {
|
|
|
13279
14841
|
specTask = stripAgentPrefix(spec);
|
|
13280
14842
|
}
|
|
13281
14843
|
const specLabel = explicitLabel ? `${explicitLabel}-${i + 1}` : generateLabel(repo, specTask);
|
|
14844
|
+
if (!agentTypeExplicit && !hasExplicitPrefix) {
|
|
14845
|
+
specRequestedType = await ptyService.resolveAgentType({
|
|
14846
|
+
task: specTask,
|
|
14847
|
+
repo,
|
|
14848
|
+
subtaskCount: agentSpecs.length
|
|
14849
|
+
});
|
|
14850
|
+
specPiRequested = isPiAgentType(specRequestedType);
|
|
14851
|
+
specAgentType = normalizeAgentType(specRequestedType);
|
|
14852
|
+
}
|
|
13282
14853
|
return {
|
|
13283
14854
|
specAgentType,
|
|
13284
14855
|
specPiRequested,
|
|
@@ -13286,7 +14857,7 @@ async function handleMultiAgent(ctx, agentsParam) {
|
|
|
13286
14857
|
specTask,
|
|
13287
14858
|
specLabel
|
|
13288
14859
|
};
|
|
13289
|
-
});
|
|
14860
|
+
}));
|
|
13290
14861
|
const graphPlan = coordinator && taskThread ? await coordinator.planTaskThreadGraph({
|
|
13291
14862
|
threadId: taskThread.id,
|
|
13292
14863
|
title: threadTitle,
|
|
@@ -13348,6 +14919,8 @@ ${swarmContext}
|
|
|
13348
14919
|
const agentMemory = [memoryContent, swarmMemory, pastExperienceBlock].filter(Boolean).join(`
|
|
13349
14920
|
|
|
13350
14921
|
`) || undefined;
|
|
14922
|
+
const coordinatorManagedSession = !!coordinator && llmProvider === "subscription";
|
|
14923
|
+
const useDirectCallbackResponses = Boolean(callback);
|
|
13351
14924
|
const session = await ptyService.spawnSession({
|
|
13352
14925
|
name: `coding-${Date.now()}-${i}`,
|
|
13353
14926
|
agentType: specAgentType,
|
|
@@ -13357,7 +14930,7 @@ ${swarmContext}
|
|
|
13357
14930
|
credentials,
|
|
13358
14931
|
approvalPreset: approvalPreset ?? ptyService.defaultApprovalPreset,
|
|
13359
14932
|
customCredentials,
|
|
13360
|
-
...
|
|
14933
|
+
...coordinatorManagedSession ? { skipAdapterAutoResponse: true } : {},
|
|
13361
14934
|
metadata: {
|
|
13362
14935
|
threadId: taskThread?.id,
|
|
13363
14936
|
taskNodeId,
|
|
@@ -13366,12 +14939,15 @@ ${swarmContext}
|
|
|
13366
14939
|
userId: message.userId,
|
|
13367
14940
|
workspaceId,
|
|
13368
14941
|
label: specLabel,
|
|
13369
|
-
multiAgentIndex: i
|
|
14942
|
+
multiAgentIndex: i,
|
|
14943
|
+
roomId: message.roomId,
|
|
14944
|
+
worldId: message.worldId,
|
|
14945
|
+
source: message.content?.source
|
|
13370
14946
|
}
|
|
13371
14947
|
});
|
|
13372
14948
|
const isScratch = !repo;
|
|
13373
14949
|
const scratchDir = isScratch ? workdir : null;
|
|
13374
|
-
registerSessionEvents(ptyService, runtime, session.id, specLabel, scratchDir, callback,
|
|
14950
|
+
registerSessionEvents(ptyService, runtime, session.id, specLabel, scratchDir, callback, coordinatorManagedSession && !useDirectCallbackResponses);
|
|
13375
14951
|
if (coordinator && specTask) {
|
|
13376
14952
|
await coordinator.registerTask(session.id, {
|
|
13377
14953
|
threadId: taskThread?.id ?? session.id,
|
|
@@ -13506,8 +15082,6 @@ var startCodingTaskAction = {
|
|
|
13506
15082
|
const params = options?.parameters;
|
|
13507
15083
|
const content = message.content;
|
|
13508
15084
|
const explicitRawType = params?.agentType ?? content.agentType;
|
|
13509
|
-
const rawAgentType = explicitRawType ?? await ptyService.resolveAgentType();
|
|
13510
|
-
const defaultAgentType = normalizeAgentType(rawAgentType);
|
|
13511
15085
|
const memoryContent = params?.memoryContent ?? content.memoryContent;
|
|
13512
15086
|
const approvalPreset = params?.approvalPreset ?? content.approvalPreset;
|
|
13513
15087
|
let repo = params?.repo ?? content.repo;
|
|
@@ -13534,6 +15108,13 @@ var startCodingTaskAction = {
|
|
|
13534
15108
|
}
|
|
13535
15109
|
}
|
|
13536
15110
|
}
|
|
15111
|
+
const selectionTask = params?.task ?? content.task ?? content.text;
|
|
15112
|
+
const rawAgentType = explicitRawType ?? await ptyService.resolveAgentType({
|
|
15113
|
+
task: selectionTask,
|
|
15114
|
+
repo,
|
|
15115
|
+
subtaskCount: typeof params?.agents === "string" || typeof content.agents === "string" ? (params?.agents ?? content.agents).split("|").map((value) => value.trim()).filter(Boolean).length || 1 : 1
|
|
15116
|
+
});
|
|
15117
|
+
const defaultAgentType = normalizeAgentType(rawAgentType);
|
|
13537
15118
|
const customCredentialKeys = runtime.getSetting("CUSTOM_CREDENTIAL_KEYS");
|
|
13538
15119
|
let customCredentials;
|
|
13539
15120
|
if (customCredentialKeys) {
|
|
@@ -13544,6 +15125,8 @@ var startCodingTaskAction = {
|
|
|
13544
15125
|
customCredentials[key] = val;
|
|
13545
15126
|
}
|
|
13546
15127
|
}
|
|
15128
|
+
const rawAnthropicKey = runtime.getSetting("ANTHROPIC_API_KEY");
|
|
15129
|
+
customCredentials = sanitizeCustomCredentials(customCredentials, isAnthropicOAuthToken(rawAnthropicKey) ? [rawAnthropicKey] : []);
|
|
13547
15130
|
let credentials;
|
|
13548
15131
|
try {
|
|
13549
15132
|
credentials = buildAgentCredentials(runtime);
|
|
@@ -13568,6 +15151,7 @@ var startCodingTaskAction = {
|
|
|
13568
15151
|
repo,
|
|
13569
15152
|
defaultAgentType,
|
|
13570
15153
|
rawAgentType,
|
|
15154
|
+
agentTypeExplicit: Boolean(explicitRawType),
|
|
13571
15155
|
agentSelectionStrategy: ptyService.agentSelectionStrategy,
|
|
13572
15156
|
memoryContent,
|
|
13573
15157
|
approvalPreset,
|
|
@@ -13692,6 +15276,15 @@ var stopAgentAction = {
|
|
|
13692
15276
|
}
|
|
13693
15277
|
},
|
|
13694
15278
|
handler: async (runtime, message, state, options, callback) => {
|
|
15279
|
+
const access = await requireTaskAgentAccess(runtime, message, "interact");
|
|
15280
|
+
if (!access.allowed) {
|
|
15281
|
+
if (callback) {
|
|
15282
|
+
await callback({
|
|
15283
|
+
text: access.reason
|
|
15284
|
+
});
|
|
15285
|
+
}
|
|
15286
|
+
return { success: false, error: "FORBIDDEN", text: access.reason };
|
|
15287
|
+
}
|
|
13695
15288
|
const ptyService = runtime.getService("PTY_SERVICE");
|
|
13696
15289
|
if (!ptyService) {
|
|
13697
15290
|
if (callback) {
|
|
@@ -14055,9 +15648,9 @@ var activeWorkspaceContextProvider = {
|
|
|
14055
15648
|
};
|
|
14056
15649
|
|
|
14057
15650
|
// src/services/workspace-service.ts
|
|
14058
|
-
import * as
|
|
14059
|
-
import * as
|
|
14060
|
-
import * as
|
|
15651
|
+
import * as os9 from "node:os";
|
|
15652
|
+
import * as path13 from "node:path";
|
|
15653
|
+
import * as fs6 from "node:fs/promises";
|
|
14061
15654
|
import {
|
|
14062
15655
|
CredentialService,
|
|
14063
15656
|
GitHubPatClient as GitHubPatClient2,
|
|
@@ -14270,23 +15863,23 @@ async function createPR(workspaceService, workspace, workspaceId, options, log)
|
|
|
14270
15863
|
}
|
|
14271
15864
|
|
|
14272
15865
|
// src/services/workspace-lifecycle.ts
|
|
14273
|
-
import * as
|
|
14274
|
-
import * as
|
|
14275
|
-
import * as
|
|
15866
|
+
import * as fs5 from "node:fs";
|
|
15867
|
+
import * as os8 from "node:os";
|
|
15868
|
+
import * as path12 from "node:path";
|
|
14276
15869
|
async function removeScratchDir(dirPath, baseDir, log, allowedDirs) {
|
|
14277
|
-
const resolved =
|
|
14278
|
-
const expandTilde = (p) => p.startsWith("~") ?
|
|
15870
|
+
const resolved = path12.resolve(dirPath);
|
|
15871
|
+
const expandTilde = (p) => p.startsWith("~") ? path12.join(os8.homedir(), p.slice(1)) : p;
|
|
14279
15872
|
const allAllowed = [baseDir, ...allowedDirs ?? []];
|
|
14280
15873
|
const isAllowed = allAllowed.some((dir) => {
|
|
14281
|
-
const resolvedDir =
|
|
14282
|
-
return resolved.startsWith(resolvedDir) || resolved ===
|
|
15874
|
+
const resolvedDir = path12.resolve(expandTilde(dir)) + path12.sep;
|
|
15875
|
+
return resolved.startsWith(resolvedDir) || resolved === path12.resolve(expandTilde(dir));
|
|
14283
15876
|
});
|
|
14284
15877
|
if (!isAllowed) {
|
|
14285
15878
|
console.warn(`[CodingWorkspaceService] Refusing to remove dir outside allowed paths: ${resolved}`);
|
|
14286
15879
|
return;
|
|
14287
15880
|
}
|
|
14288
15881
|
try {
|
|
14289
|
-
await
|
|
15882
|
+
await fs5.promises.rm(resolved, { recursive: true, force: true });
|
|
14290
15883
|
log(`Removed scratch dir ${resolved}`);
|
|
14291
15884
|
} catch (err) {
|
|
14292
15885
|
console.warn(`[CodingWorkspaceService] Failed to remove scratch dir ${resolved}:`, err);
|
|
@@ -14299,7 +15892,7 @@ async function gcOrphanedWorkspaces(baseDir, workspaceTtlMs, trackedWorkspaceIds
|
|
|
14299
15892
|
}
|
|
14300
15893
|
let entries;
|
|
14301
15894
|
try {
|
|
14302
|
-
entries = await
|
|
15895
|
+
entries = await fs5.promises.readdir(baseDir, { withFileTypes: true });
|
|
14303
15896
|
} catch {
|
|
14304
15897
|
return;
|
|
14305
15898
|
}
|
|
@@ -14313,12 +15906,12 @@ async function gcOrphanedWorkspaces(baseDir, workspaceTtlMs, trackedWorkspaceIds
|
|
|
14313
15906
|
skipped++;
|
|
14314
15907
|
continue;
|
|
14315
15908
|
}
|
|
14316
|
-
const dirPath =
|
|
15909
|
+
const dirPath = path12.join(baseDir, entry.name);
|
|
14317
15910
|
try {
|
|
14318
|
-
const stat3 = await
|
|
15911
|
+
const stat3 = await fs5.promises.stat(dirPath);
|
|
14319
15912
|
const age = now - stat3.mtimeMs;
|
|
14320
15913
|
if (age > workspaceTtlMs) {
|
|
14321
|
-
await
|
|
15914
|
+
await fs5.promises.rm(dirPath, { recursive: true, force: true });
|
|
14322
15915
|
removed++;
|
|
14323
15916
|
} else {
|
|
14324
15917
|
skipped++;
|
|
@@ -14353,7 +15946,7 @@ class CodingWorkspaceService {
|
|
|
14353
15946
|
constructor(runtime, config = {}) {
|
|
14354
15947
|
this.runtime = runtime;
|
|
14355
15948
|
this.serviceConfig = {
|
|
14356
|
-
baseDir: config.baseDir ??
|
|
15949
|
+
baseDir: config.baseDir ?? path13.join(os9.homedir(), ".milady", "workspaces"),
|
|
14357
15950
|
branchPrefix: config.branchPrefix ?? "milady",
|
|
14358
15951
|
debug: config.debug ?? false,
|
|
14359
15952
|
workspaceTtlMs: config.workspaceTtlMs ?? 24 * 60 * 60 * 1000
|
|
@@ -14610,7 +16203,7 @@ class CodingWorkspaceService {
|
|
|
14610
16203
|
}
|
|
14611
16204
|
async removeScratchDir(dirPath) {
|
|
14612
16205
|
const rawCodingDir = this.runtime.getSetting("PARALLAX_CODING_DIRECTORY") ?? this.readConfigEnvKey("PARALLAX_CODING_DIRECTORY") ?? process.env.PARALLAX_CODING_DIRECTORY;
|
|
14613
|
-
const codingDir = rawCodingDir?.trim() ? rawCodingDir.trim().startsWith("~") ?
|
|
16206
|
+
const codingDir = rawCodingDir?.trim() ? rawCodingDir.trim().startsWith("~") ? path13.join(os9.homedir(), rawCodingDir.trim().slice(1)) : path13.resolve(rawCodingDir.trim()) : undefined;
|
|
14614
16207
|
const allowedDirs = codingDir ? [codingDir] : undefined;
|
|
14615
16208
|
return removeScratchDir(dirPath, this.serviceConfig.baseDir, (msg) => this.log(msg), allowedDirs);
|
|
14616
16209
|
}
|
|
@@ -14687,14 +16280,14 @@ class CodingWorkspaceService {
|
|
|
14687
16280
|
const suggestedName = this.sanitizeWorkspaceName(name2 || record.label);
|
|
14688
16281
|
const targetPath = await this.allocatePromotedPath(baseDir, suggestedName);
|
|
14689
16282
|
try {
|
|
14690
|
-
await
|
|
16283
|
+
await fs6.rename(record.path, targetPath);
|
|
14691
16284
|
} catch (error) {
|
|
14692
16285
|
const isExdev = typeof error === "object" && error !== null && "code" in error && error.code === "EXDEV";
|
|
14693
16286
|
if (!isExdev)
|
|
14694
16287
|
throw error;
|
|
14695
|
-
await
|
|
14696
|
-
await
|
|
14697
|
-
await
|
|
16288
|
+
await fs6.cp(record.path, targetPath, { recursive: true });
|
|
16289
|
+
await fs6.access(targetPath);
|
|
16290
|
+
await fs6.rm(record.path, { recursive: true, force: true });
|
|
14698
16291
|
}
|
|
14699
16292
|
const next = {
|
|
14700
16293
|
...record,
|
|
@@ -14775,15 +16368,15 @@ class CodingWorkspaceService {
|
|
|
14775
16368
|
return compact || `scratch-${Date.now().toString(36)}`;
|
|
14776
16369
|
}
|
|
14777
16370
|
async allocatePromotedPath(baseDir, baseName) {
|
|
14778
|
-
const baseResolved =
|
|
16371
|
+
const baseResolved = path13.resolve(baseDir);
|
|
14779
16372
|
for (let i = 0;i < 1000; i++) {
|
|
14780
16373
|
const candidateName = i === 0 ? baseName : `${baseName}-${i}`;
|
|
14781
|
-
const candidate =
|
|
14782
|
-
if (candidate !== baseResolved && !candidate.startsWith(`${baseResolved}${
|
|
16374
|
+
const candidate = path13.resolve(baseResolved, candidateName);
|
|
16375
|
+
if (candidate !== baseResolved && !candidate.startsWith(`${baseResolved}${path13.sep}`)) {
|
|
14783
16376
|
continue;
|
|
14784
16377
|
}
|
|
14785
16378
|
try {
|
|
14786
|
-
await
|
|
16379
|
+
await fs6.access(candidate);
|
|
14787
16380
|
} catch {
|
|
14788
16381
|
return candidate;
|
|
14789
16382
|
}
|
|
@@ -14793,9 +16386,9 @@ class CodingWorkspaceService {
|
|
|
14793
16386
|
}
|
|
14794
16387
|
// src/api/agent-routes.ts
|
|
14795
16388
|
import { access as access2, readFile as readFile4, realpath, rm as rm2 } from "node:fs/promises";
|
|
14796
|
-
import { createHash as
|
|
14797
|
-
import * as
|
|
14798
|
-
import * as
|
|
16389
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
16390
|
+
import * as os10 from "node:os";
|
|
16391
|
+
import * as path14 from "node:path";
|
|
14799
16392
|
import { execFile as execFile2 } from "node:child_process";
|
|
14800
16393
|
import { promisify as promisify2 } from "node:util";
|
|
14801
16394
|
var execFileAsync2 = promisify2(execFile2);
|
|
@@ -14807,23 +16400,23 @@ function shouldAutoPreflight() {
|
|
|
14807
16400
|
return false;
|
|
14808
16401
|
}
|
|
14809
16402
|
function isPathInside(parent, candidate) {
|
|
14810
|
-
return candidate === parent || candidate.startsWith(`${parent}${
|
|
16403
|
+
return candidate === parent || candidate.startsWith(`${parent}${path14.sep}`);
|
|
14811
16404
|
}
|
|
14812
16405
|
async function resolveSafeVenvPath(workdir, venvDirRaw) {
|
|
14813
16406
|
const venvDir = venvDirRaw.trim();
|
|
14814
16407
|
if (!venvDir) {
|
|
14815
16408
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be non-empty");
|
|
14816
16409
|
}
|
|
14817
|
-
if (
|
|
16410
|
+
if (path14.isAbsolute(venvDir)) {
|
|
14818
16411
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be relative to workdir");
|
|
14819
16412
|
}
|
|
14820
|
-
const normalized =
|
|
14821
|
-
if (normalized === "." || normalized === ".." || normalized.startsWith(`..${
|
|
16413
|
+
const normalized = path14.normalize(venvDir);
|
|
16414
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith(`..${path14.sep}`)) {
|
|
14822
16415
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must stay within workdir");
|
|
14823
16416
|
}
|
|
14824
|
-
const workdirResolved =
|
|
16417
|
+
const workdirResolved = path14.resolve(workdir);
|
|
14825
16418
|
const workdirReal = await realpath(workdirResolved);
|
|
14826
|
-
const resolved =
|
|
16419
|
+
const resolved = path14.resolve(workdirReal, normalized);
|
|
14827
16420
|
if (!isPathInside(workdirReal, resolved)) {
|
|
14828
16421
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV resolves outside workdir");
|
|
14829
16422
|
}
|
|
@@ -14839,7 +16432,7 @@ async function resolveSafeVenvPath(workdir, venvDirRaw) {
|
|
|
14839
16432
|
const maybeErr = err;
|
|
14840
16433
|
if (maybeErr?.code !== "ENOENT")
|
|
14841
16434
|
throw err;
|
|
14842
|
-
const parentReal = await realpath(
|
|
16435
|
+
const parentReal = await realpath(path14.dirname(resolved));
|
|
14843
16436
|
if (!isPathInside(workdirReal, parentReal)) {
|
|
14844
16437
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV parent resolves outside workdir");
|
|
14845
16438
|
}
|
|
@@ -14855,10 +16448,10 @@ async function fileExists(filePath) {
|
|
|
14855
16448
|
}
|
|
14856
16449
|
}
|
|
14857
16450
|
async function resolveRequirementsPath(workdir) {
|
|
14858
|
-
const workdirReal = await realpath(
|
|
16451
|
+
const workdirReal = await realpath(path14.resolve(workdir));
|
|
14859
16452
|
const candidates = [
|
|
14860
|
-
|
|
14861
|
-
|
|
16453
|
+
path14.join(workdir, "apps", "api", "requirements.txt"),
|
|
16454
|
+
path14.join(workdir, "requirements.txt")
|
|
14862
16455
|
];
|
|
14863
16456
|
for (const candidate of candidates) {
|
|
14864
16457
|
if (!await fileExists(candidate))
|
|
@@ -14873,7 +16466,7 @@ async function resolveRequirementsPath(workdir) {
|
|
|
14873
16466
|
}
|
|
14874
16467
|
async function fingerprintRequirementsFile(requirementsPath) {
|
|
14875
16468
|
const file = await readFile4(requirementsPath);
|
|
14876
|
-
return
|
|
16469
|
+
return createHash3("sha256").update(file).digest("hex");
|
|
14877
16470
|
}
|
|
14878
16471
|
async function runBenchmarkPreflight(workdir) {
|
|
14879
16472
|
if (!shouldAutoPreflight())
|
|
@@ -14885,7 +16478,7 @@ async function runBenchmarkPreflight(workdir) {
|
|
|
14885
16478
|
const mode = process.env.PARALLAX_BENCHMARK_PREFLIGHT_MODE?.toLowerCase() === "warm" ? "warm" : "cold";
|
|
14886
16479
|
const venvDir = process.env.PARALLAX_BENCHMARK_PREFLIGHT_VENV || ".benchmark-venv";
|
|
14887
16480
|
const venvPath = await resolveSafeVenvPath(workdir, venvDir);
|
|
14888
|
-
const pythonInVenv =
|
|
16481
|
+
const pythonInVenv = path14.join(venvPath, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
|
|
14889
16482
|
const key = `${workdir}::${mode}::${venvPath}::${requirementsFingerprint}`;
|
|
14890
16483
|
if (PREFLIGHT_DONE.has(key)) {
|
|
14891
16484
|
if (await fileExists(pythonInVenv))
|
|
@@ -15128,21 +16721,21 @@ async function handleAgentRoutes(req, res, pathname, ctx) {
|
|
|
15128
16721
|
customCredentials,
|
|
15129
16722
|
metadata
|
|
15130
16723
|
} = body;
|
|
15131
|
-
const workspaceBaseDir =
|
|
15132
|
-
const workspaceBaseDirResolved =
|
|
15133
|
-
const cwdResolved =
|
|
16724
|
+
const workspaceBaseDir = path14.join(os10.homedir(), ".milady", "workspaces");
|
|
16725
|
+
const workspaceBaseDirResolved = path14.resolve(workspaceBaseDir);
|
|
16726
|
+
const cwdResolved = path14.resolve(process.cwd());
|
|
15134
16727
|
const workspaceBaseDirReal = await realpath(workspaceBaseDirResolved).catch(() => workspaceBaseDirResolved);
|
|
15135
16728
|
const cwdReal = await realpath(cwdResolved).catch(() => cwdResolved);
|
|
15136
16729
|
const allowedPrefixes = [workspaceBaseDirReal, cwdReal];
|
|
15137
16730
|
let workdir = rawWorkdir;
|
|
15138
16731
|
if (workdir) {
|
|
15139
|
-
const resolved =
|
|
16732
|
+
const resolved = path14.resolve(workdir);
|
|
15140
16733
|
const resolvedReal = await realpath(resolved).catch(() => null);
|
|
15141
16734
|
if (!resolvedReal) {
|
|
15142
16735
|
sendError(res, "workdir must exist", 403);
|
|
15143
16736
|
return true;
|
|
15144
16737
|
}
|
|
15145
|
-
const isAllowed = allowedPrefixes.some((prefix2) => resolvedReal === prefix2 || resolvedReal.startsWith(prefix2 +
|
|
16738
|
+
const isAllowed = allowedPrefixes.some((prefix2) => resolvedReal === prefix2 || resolvedReal.startsWith(prefix2 + path14.sep));
|
|
15146
16739
|
if (!isAllowed) {
|
|
15147
16740
|
sendError(res, "workdir must be within workspace base directory or cwd", 403);
|
|
15148
16741
|
return true;
|
|
@@ -15162,12 +16755,15 @@ async function handleAgentRoutes(req, res, pathname, ctx) {
|
|
|
15162
16755
|
console.warn(`[coding-agent] benchmark preflight failed for ${workdir}:`, preflightError);
|
|
15163
16756
|
}
|
|
15164
16757
|
}
|
|
15165
|
-
const
|
|
15166
|
-
|
|
15167
|
-
|
|
15168
|
-
|
|
15169
|
-
|
|
15170
|
-
|
|
16758
|
+
const rawAnthropicKey = ctx.runtime.getSetting("ANTHROPIC_API_KEY");
|
|
16759
|
+
let credentials;
|
|
16760
|
+
try {
|
|
16761
|
+
credentials = buildAgentCredentials(ctx.runtime);
|
|
16762
|
+
} catch (error) {
|
|
16763
|
+
const message = error instanceof Error ? error.message : "Failed to build credentials";
|
|
16764
|
+
sendError(res, message, 400);
|
|
16765
|
+
return true;
|
|
16766
|
+
}
|
|
15171
16767
|
const agentStr = agentType ? agentType.toLowerCase() : await ctx.ptyService.resolveAgentType();
|
|
15172
16768
|
const piRequested = isPiAgentType(agentStr);
|
|
15173
16769
|
const normalizedType = normalizeAgentType(agentStr);
|
|
@@ -15204,7 +16800,7 @@ async function handleAgentRoutes(req, res, pathname, ctx) {
|
|
|
15204
16800
|
memoryContent,
|
|
15205
16801
|
credentials,
|
|
15206
16802
|
approvalPreset,
|
|
15207
|
-
customCredentials,
|
|
16803
|
+
customCredentials: sanitizeCustomCredentials(customCredentials, isAnthropicOAuthToken(rawAnthropicKey) ? [rawAnthropicKey] : []),
|
|
15208
16804
|
metadata: {
|
|
15209
16805
|
threadId: taskThread?.id ?? requestedThreadId,
|
|
15210
16806
|
requestedType: agentStr,
|
|
@@ -16136,5 +17732,5 @@ export {
|
|
|
16136
17732
|
CodingWorkspaceService
|
|
16137
17733
|
};
|
|
16138
17734
|
|
|
16139
|
-
//# debugId=
|
|
17735
|
+
//# debugId=FCF9DB7BF01E156E64756E2164756E21
|
|
16140
17736
|
//# sourceMappingURL=index.js.map
|