opencode-orchestrator 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -1
- package/bin/orchestrator-linux-arm64 +0 -0
- package/bin/orchestrator-linux-x64 +0 -0
- package/dist/core/async-agent.d.ts +100 -0
- package/dist/core/background.d.ts +78 -0
- package/dist/core/state.d.ts +2 -0
- package/dist/index.d.ts +67 -6
- package/dist/index.js +1535 -10
- package/dist/tools/async-agent.d.ts +70 -0
- package/dist/tools/background.d.ts +55 -0
- package/dist/tools/search.d.ts +22 -0
- package/dist/utils/common.d.ts +11 -0
- package/dist/utils/sanity.d.ts +31 -0
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -27,7 +27,13 @@ You are Commander. Complete missions autonomously. Never stop until done.
|
|
|
27
27
|
3. Never stop because agent returned nothing
|
|
28
28
|
4. Always survey environment & codebase BEFORE coding
|
|
29
29
|
5. Always verify with evidence based on runtime context
|
|
30
|
-
6. LANGUAGE:
|
|
30
|
+
6. LANGUAGE:
|
|
31
|
+
- THINK and REASON in English for maximum stability
|
|
32
|
+
- FINAL REPORT: Detect the user's language from their request and respond in the SAME language
|
|
33
|
+
- If user writes in Korean \u2192 Report in Korean
|
|
34
|
+
- If user writes in English \u2192 Report in English
|
|
35
|
+
- If user writes in Japanese \u2192 Report in Japanese
|
|
36
|
+
- Default to English if language is unclear
|
|
31
37
|
</core_rules>
|
|
32
38
|
|
|
33
39
|
<phase_0 name="TRIAGE">
|
|
@@ -66,6 +72,44 @@ DEFAULT to Deep Track if unsure to act safely.
|
|
|
66
72
|
</phase_2>
|
|
67
73
|
|
|
68
74
|
<phase_3 name="DELEGATION">
|
|
75
|
+
<agent_calling>
|
|
76
|
+
\u26A0\uFE0F CRITICAL: USE delegate_task FOR ALL DELEGATION \u26A0\uFE0F
|
|
77
|
+
|
|
78
|
+
delegate_task has TWO MODES:
|
|
79
|
+
- background=true: Non-blocking, parallel execution
|
|
80
|
+
- background=false: Blocking, waits for result
|
|
81
|
+
|
|
82
|
+
| Situation | How to Call |
|
|
83
|
+
|-----------|-------------|
|
|
84
|
+
| Multiple independent tasks | \`delegate_task({ ..., background: true })\` for each |
|
|
85
|
+
| Single task, continue working | \`delegate_task({ ..., background: true })\` |
|
|
86
|
+
| Need result for VERY next step | \`delegate_task({ ..., background: false })\` |
|
|
87
|
+
|
|
88
|
+
PREFER background=true (PARALLEL):
|
|
89
|
+
- Run multiple agents simultaneously
|
|
90
|
+
- Continue analysis while they work
|
|
91
|
+
- System notifies when ALL complete
|
|
92
|
+
|
|
93
|
+
EXAMPLE - PARALLEL:
|
|
94
|
+
\`\`\`
|
|
95
|
+
// Multiple tasks in parallel
|
|
96
|
+
delegate_task({ agent: "builder", description: "Implement X", prompt: "...", background: true })
|
|
97
|
+
delegate_task({ agent: "inspector", description: "Review Y", prompt: "...", background: true })
|
|
98
|
+
|
|
99
|
+
// Continue other work (don't wait!)
|
|
100
|
+
|
|
101
|
+
// When notified "All Complete":
|
|
102
|
+
get_task_result({ taskId: "task_xxx" })
|
|
103
|
+
\`\`\`
|
|
104
|
+
|
|
105
|
+
EXAMPLE - SYNC (rare):
|
|
106
|
+
\`\`\`
|
|
107
|
+
// Only when you absolutely need the result now
|
|
108
|
+
const result = delegate_task({ agent: "builder", ..., background: false })
|
|
109
|
+
// Result is immediately available
|
|
110
|
+
\`\`\`
|
|
111
|
+
</agent_calling>
|
|
112
|
+
|
|
69
113
|
<delegation_template>
|
|
70
114
|
AGENT: [name]
|
|
71
115
|
TASK: [one atomic action]
|
|
@@ -84,6 +128,45 @@ During implementation:
|
|
|
84
128
|
- Match existing codebase style exactly
|
|
85
129
|
- Run lsp_diagnostics after each change
|
|
86
130
|
|
|
131
|
+
<background_parallel_execution>
|
|
132
|
+
PARALLEL EXECUTION TOOLS:
|
|
133
|
+
|
|
134
|
+
1. **spawn_agent** - Launch agents in parallel sessions
|
|
135
|
+
spawn_agent({ agent: "builder", description: "Implement X", prompt: "..." })
|
|
136
|
+
spawn_agent({ agent: "inspector", description: "Review Y", prompt: "..." })
|
|
137
|
+
\u2192 Agents run concurrently, system notifies when ALL complete
|
|
138
|
+
\u2192 Use get_task_result({ taskId }) to retrieve results
|
|
139
|
+
|
|
140
|
+
2. **run_background** - Run shell commands asynchronously
|
|
141
|
+
run_background({ command: "npm run build" })
|
|
142
|
+
\u2192 Use check_background({ taskId }) for results
|
|
143
|
+
|
|
144
|
+
SAFETY FEATURES:
|
|
145
|
+
- Queue-based concurrency: Max 3 per agent type (extras queue automatically)
|
|
146
|
+
- Auto-timeout: 30 minutes max runtime
|
|
147
|
+
- Auto-cleanup: Removed from memory 5 min after completion
|
|
148
|
+
- Batched notifications: Notifies when ALL tasks complete (not individually)
|
|
149
|
+
|
|
150
|
+
MANAGEMENT TOOLS:
|
|
151
|
+
- list_tasks: View all parallel tasks and status
|
|
152
|
+
- cancel_task: Stop a running task (frees concurrency slot)
|
|
153
|
+
|
|
154
|
+
SAFE PATTERNS:
|
|
155
|
+
\u2705 Builder on file A + Inspector on file B (different files)
|
|
156
|
+
\u2705 Multiple research agents (read-only)
|
|
157
|
+
\u2705 Build command + Test command (independent)
|
|
158
|
+
|
|
159
|
+
UNSAFE PATTERNS:
|
|
160
|
+
\u274C Multiple builders editing SAME FILE (conflict!)
|
|
161
|
+
|
|
162
|
+
WORKFLOW:
|
|
163
|
+
1. list_tasks: Check current status first
|
|
164
|
+
2. spawn_agent: Launch for INDEPENDENT tasks
|
|
165
|
+
3. Continue working (NO WAITING)
|
|
166
|
+
4. Wait for "All Complete" notification
|
|
167
|
+
5. get_task_result: Retrieve each result
|
|
168
|
+
</background_parallel_execution>
|
|
169
|
+
|
|
87
170
|
<verification_methods>
|
|
88
171
|
| Infra | Proof Method |
|
|
89
172
|
|-------|--------------|
|
|
@@ -294,6 +377,16 @@ If your reasoning collapses into gibberish, stop and output "ERROR: REASONING_CO
|
|
|
294
377
|
| Volume-mount | Host-level syntax + internal service check |
|
|
295
378
|
</verification_by_context>
|
|
296
379
|
|
|
380
|
+
<background_tools>
|
|
381
|
+
USE BACKGROUND TASKS FOR PARALLEL VERIFICATION:
|
|
382
|
+
- run_background("npm run build") \u2192 Don't wait, continue analysis
|
|
383
|
+
- run_background("npm test") \u2192 Run tests in parallel with build
|
|
384
|
+
- list_background() \u2192 Check all running jobs
|
|
385
|
+
- check_background(taskId) \u2192 Get results when ready
|
|
386
|
+
|
|
387
|
+
ALWAYS prefer background for build/test commands.
|
|
388
|
+
</background_tools>
|
|
389
|
+
|
|
297
390
|
<output_format>
|
|
298
391
|
<pass>
|
|
299
392
|
\u2705 PASS
|
|
@@ -772,6 +865,1210 @@ var globSearchTool = (directory) => tool3({
|
|
|
772
865
|
});
|
|
773
866
|
}
|
|
774
867
|
});
|
|
868
|
+
var mgrepTool = (directory) => tool3({
|
|
869
|
+
description: `Search multiple patterns in parallel (high-performance).
|
|
870
|
+
|
|
871
|
+
<purpose>
|
|
872
|
+
Search for multiple regex patterns simultaneously using Rust's parallel execution.
|
|
873
|
+
Much faster than running grep multiple times sequentially.
|
|
874
|
+
</purpose>
|
|
875
|
+
|
|
876
|
+
<examples>
|
|
877
|
+
- patterns: ["useState", "useEffect", "useContext"] \u2192 Find all React hooks usage
|
|
878
|
+
- patterns: ["TODO", "FIXME", "HACK"] \u2192 Find all code annotations
|
|
879
|
+
- patterns: ["import.*lodash", "require.*lodash"] \u2192 Find all lodash imports
|
|
880
|
+
</examples>
|
|
881
|
+
|
|
882
|
+
<output>
|
|
883
|
+
Returns matches grouped by pattern, with file paths and line numbers.
|
|
884
|
+
</output>`,
|
|
885
|
+
args: {
|
|
886
|
+
patterns: tool3.schema.array(tool3.schema.string()).describe("Array of regex patterns to search for"),
|
|
887
|
+
dir: tool3.schema.string().optional().describe("Directory to search (defaults to project root)"),
|
|
888
|
+
max_results_per_pattern: tool3.schema.number().optional().describe("Max results per pattern (default: 50)")
|
|
889
|
+
},
|
|
890
|
+
async execute(args) {
|
|
891
|
+
return callRustTool("mgrep", {
|
|
892
|
+
patterns: args.patterns,
|
|
893
|
+
directory: args.dir || directory,
|
|
894
|
+
max_results_per_pattern: args.max_results_per_pattern || 50
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
// src/tools/background.ts
|
|
900
|
+
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
901
|
+
|
|
902
|
+
// src/core/background.ts
|
|
903
|
+
import { spawn as spawn2 } from "child_process";
|
|
904
|
+
import { randomBytes } from "crypto";
|
|
905
|
+
var BackgroundTaskManager = class _BackgroundTaskManager {
|
|
906
|
+
static _instance;
|
|
907
|
+
tasks = /* @__PURE__ */ new Map();
|
|
908
|
+
debugMode = true;
|
|
909
|
+
// Enable debug mode
|
|
910
|
+
constructor() {
|
|
911
|
+
}
|
|
912
|
+
static get instance() {
|
|
913
|
+
if (!_BackgroundTaskManager._instance) {
|
|
914
|
+
_BackgroundTaskManager._instance = new _BackgroundTaskManager();
|
|
915
|
+
}
|
|
916
|
+
return _BackgroundTaskManager._instance;
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Generate a unique task ID in the format job_xxxxxxxx
|
|
920
|
+
*/
|
|
921
|
+
generateId() {
|
|
922
|
+
const hex = randomBytes(4).toString("hex");
|
|
923
|
+
return `job_${hex}`;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Debug logging helper
|
|
927
|
+
*/
|
|
928
|
+
debug(taskId, message) {
|
|
929
|
+
if (this.debugMode) {
|
|
930
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().substring(11, 23);
|
|
931
|
+
console.log(`[BG-DEBUG ${timestamp}] ${taskId}: ${message}`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Run a command in the background
|
|
936
|
+
*/
|
|
937
|
+
run(options) {
|
|
938
|
+
const id = this.generateId();
|
|
939
|
+
const { command, cwd = process.cwd(), timeout = 3e5, label } = options;
|
|
940
|
+
const isWindows = process.platform === "win32";
|
|
941
|
+
const shell = isWindows ? "cmd.exe" : "/bin/sh";
|
|
942
|
+
const shellFlag = isWindows ? "/c" : "-c";
|
|
943
|
+
const task = {
|
|
944
|
+
id,
|
|
945
|
+
command,
|
|
946
|
+
args: [shellFlag, command],
|
|
947
|
+
cwd,
|
|
948
|
+
label,
|
|
949
|
+
status: "running",
|
|
950
|
+
output: "",
|
|
951
|
+
errorOutput: "",
|
|
952
|
+
exitCode: null,
|
|
953
|
+
startTime: Date.now(),
|
|
954
|
+
timeout
|
|
955
|
+
};
|
|
956
|
+
this.tasks.set(id, task);
|
|
957
|
+
this.debug(id, `Starting: ${command} (cwd: ${cwd})`);
|
|
958
|
+
try {
|
|
959
|
+
const proc = spawn2(shell, task.args, {
|
|
960
|
+
cwd,
|
|
961
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
962
|
+
detached: false
|
|
963
|
+
});
|
|
964
|
+
task.process = proc;
|
|
965
|
+
proc.stdout?.on("data", (data) => {
|
|
966
|
+
const text = data.toString();
|
|
967
|
+
task.output += text;
|
|
968
|
+
this.debug(id, `stdout: ${text.substring(0, 100)}${text.length > 100 ? "..." : ""}`);
|
|
969
|
+
});
|
|
970
|
+
proc.stderr?.on("data", (data) => {
|
|
971
|
+
const text = data.toString();
|
|
972
|
+
task.errorOutput += text;
|
|
973
|
+
this.debug(id, `stderr: ${text.substring(0, 100)}${text.length > 100 ? "..." : ""}`);
|
|
974
|
+
});
|
|
975
|
+
proc.on("close", (code) => {
|
|
976
|
+
task.exitCode = code;
|
|
977
|
+
task.endTime = Date.now();
|
|
978
|
+
task.status = code === 0 ? "done" : "error";
|
|
979
|
+
task.process = void 0;
|
|
980
|
+
const duration = ((task.endTime - task.startTime) / 1e3).toFixed(2);
|
|
981
|
+
this.debug(id, `Completed with code ${code} in ${duration}s`);
|
|
982
|
+
});
|
|
983
|
+
proc.on("error", (err) => {
|
|
984
|
+
task.status = "error";
|
|
985
|
+
task.errorOutput += `
|
|
986
|
+
Process error: ${err.message}`;
|
|
987
|
+
task.endTime = Date.now();
|
|
988
|
+
task.process = void 0;
|
|
989
|
+
this.debug(id, `Error: ${err.message}`);
|
|
990
|
+
});
|
|
991
|
+
setTimeout(() => {
|
|
992
|
+
if (task.status === "running" && task.process) {
|
|
993
|
+
this.debug(id, `Timeout after ${timeout}ms, killing process`);
|
|
994
|
+
task.process.kill("SIGKILL");
|
|
995
|
+
task.status = "timeout";
|
|
996
|
+
task.endTime = Date.now();
|
|
997
|
+
task.errorOutput += `
|
|
998
|
+
Process killed: timeout after ${timeout}ms`;
|
|
999
|
+
}
|
|
1000
|
+
}, timeout);
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
task.status = "error";
|
|
1003
|
+
task.errorOutput = `Failed to spawn: ${err instanceof Error ? err.message : String(err)}`;
|
|
1004
|
+
task.endTime = Date.now();
|
|
1005
|
+
this.debug(id, `Spawn failed: ${task.errorOutput}`);
|
|
1006
|
+
}
|
|
1007
|
+
return task;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Get task by ID
|
|
1011
|
+
*/
|
|
1012
|
+
get(taskId) {
|
|
1013
|
+
return this.tasks.get(taskId);
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Get all tasks
|
|
1017
|
+
*/
|
|
1018
|
+
getAll() {
|
|
1019
|
+
return Array.from(this.tasks.values());
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Get tasks by status
|
|
1023
|
+
*/
|
|
1024
|
+
getByStatus(status) {
|
|
1025
|
+
return this.getAll().filter((t) => t.status === status);
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Clear completed/failed tasks
|
|
1029
|
+
*/
|
|
1030
|
+
clearCompleted() {
|
|
1031
|
+
let count = 0;
|
|
1032
|
+
for (const [id, task] of this.tasks) {
|
|
1033
|
+
if (task.status !== "running" && task.status !== "pending") {
|
|
1034
|
+
this.tasks.delete(id);
|
|
1035
|
+
count++;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return count;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Kill a running task
|
|
1042
|
+
*/
|
|
1043
|
+
kill(taskId) {
|
|
1044
|
+
const task = this.tasks.get(taskId);
|
|
1045
|
+
if (task?.process) {
|
|
1046
|
+
task.process.kill("SIGKILL");
|
|
1047
|
+
task.status = "error";
|
|
1048
|
+
task.errorOutput += "\nKilled by user";
|
|
1049
|
+
task.endTime = Date.now();
|
|
1050
|
+
this.debug(taskId, "Killed by user");
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Format duration for display
|
|
1057
|
+
*/
|
|
1058
|
+
formatDuration(task) {
|
|
1059
|
+
const end = task.endTime || Date.now();
|
|
1060
|
+
const seconds = (end - task.startTime) / 1e3;
|
|
1061
|
+
if (seconds < 60) {
|
|
1062
|
+
return `${seconds.toFixed(1)}s`;
|
|
1063
|
+
}
|
|
1064
|
+
const minutes = Math.floor(seconds / 60);
|
|
1065
|
+
const remainingSeconds = seconds % 60;
|
|
1066
|
+
return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Get status emoji
|
|
1070
|
+
*/
|
|
1071
|
+
getStatusEmoji(status) {
|
|
1072
|
+
switch (status) {
|
|
1073
|
+
case "pending":
|
|
1074
|
+
return "\u23F8\uFE0F";
|
|
1075
|
+
case "running":
|
|
1076
|
+
return "\u23F3";
|
|
1077
|
+
case "done":
|
|
1078
|
+
return "\u2705";
|
|
1079
|
+
case "error":
|
|
1080
|
+
return "\u274C";
|
|
1081
|
+
case "timeout":
|
|
1082
|
+
return "\u23F0";
|
|
1083
|
+
default:
|
|
1084
|
+
return "\u2753";
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
var backgroundTaskManager = BackgroundTaskManager.instance;
|
|
1089
|
+
|
|
1090
|
+
// src/tools/background.ts
|
|
1091
|
+
var runBackgroundTool = tool4({
|
|
1092
|
+
description: `Run a shell command in the background and get a task ID.
|
|
1093
|
+
|
|
1094
|
+
<purpose>
|
|
1095
|
+
Execute long-running commands (builds, tests, etc.) without blocking.
|
|
1096
|
+
The command runs asynchronously - use check_background to get results.
|
|
1097
|
+
</purpose>
|
|
1098
|
+
|
|
1099
|
+
<examples>
|
|
1100
|
+
- "npm run build" \u2192 Build project in background
|
|
1101
|
+
- "cargo test" \u2192 Run Rust tests
|
|
1102
|
+
- "sleep 10 && echo done" \u2192 Delayed execution
|
|
1103
|
+
</examples>
|
|
1104
|
+
|
|
1105
|
+
<flow>
|
|
1106
|
+
1. Call run_background with command
|
|
1107
|
+
2. Get task ID immediately (e.g., job_a1b2c3d4)
|
|
1108
|
+
3. Continue other work
|
|
1109
|
+
4. Call check_background with task ID to get results
|
|
1110
|
+
</flow>`,
|
|
1111
|
+
args: {
|
|
1112
|
+
command: tool4.schema.string().describe("Shell command to execute"),
|
|
1113
|
+
cwd: tool4.schema.string().optional().describe("Working directory (default: project root)"),
|
|
1114
|
+
timeout: tool4.schema.number().optional().describe("Timeout in milliseconds (default: 300000 = 5 min)"),
|
|
1115
|
+
label: tool4.schema.string().optional().describe("Human-readable label for this task")
|
|
1116
|
+
},
|
|
1117
|
+
async execute(args) {
|
|
1118
|
+
const { command, cwd, timeout, label } = args;
|
|
1119
|
+
const task = backgroundTaskManager.run({
|
|
1120
|
+
command,
|
|
1121
|
+
cwd: cwd || process.cwd(),
|
|
1122
|
+
timeout: timeout || 3e5,
|
|
1123
|
+
label
|
|
1124
|
+
});
|
|
1125
|
+
const displayLabel = label ? ` (${label})` : "";
|
|
1126
|
+
return `\u{1F680} **Background Task Started**${displayLabel}
|
|
1127
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1128
|
+
| Property | Value |
|
|
1129
|
+
|----------|-------|
|
|
1130
|
+
| **Task ID** | \`${task.id}\` |
|
|
1131
|
+
| **Command** | \`${command}\` |
|
|
1132
|
+
| **Status** | ${backgroundTaskManager.getStatusEmoji(task.status)} ${task.status} |
|
|
1133
|
+
| **Working Dir** | ${task.cwd} |
|
|
1134
|
+
| **Timeout** | ${(task.timeout / 1e3).toFixed(0)}s |
|
|
1135
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1136
|
+
|
|
1137
|
+
\u{1F4CC} **Next Step**: Use \`check_background\` with task ID \`${task.id}\` to get results.`;
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
var checkBackgroundTool = tool4({
|
|
1141
|
+
description: `Check the status and output of a background task.
|
|
1142
|
+
|
|
1143
|
+
<purpose>
|
|
1144
|
+
Retrieve the current status and output of a previously started background task.
|
|
1145
|
+
Use this after run_background to get results.
|
|
1146
|
+
</purpose>
|
|
1147
|
+
|
|
1148
|
+
<output_includes>
|
|
1149
|
+
- Status: running/done/error/timeout
|
|
1150
|
+
- Exit code (if completed)
|
|
1151
|
+
- Duration
|
|
1152
|
+
- Full output (stdout + stderr)
|
|
1153
|
+
</output_includes>`,
|
|
1154
|
+
args: {
|
|
1155
|
+
taskId: tool4.schema.string().describe("Task ID from run_background (e.g., job_a1b2c3d4)"),
|
|
1156
|
+
tailLines: tool4.schema.number().optional().describe("Limit output to last N lines (default: show all)")
|
|
1157
|
+
},
|
|
1158
|
+
async execute(args) {
|
|
1159
|
+
const { taskId, tailLines } = args;
|
|
1160
|
+
const task = backgroundTaskManager.get(taskId);
|
|
1161
|
+
if (!task) {
|
|
1162
|
+
const allTasks = backgroundTaskManager.getAll();
|
|
1163
|
+
if (allTasks.length === 0) {
|
|
1164
|
+
return `\u274C Task \`${taskId}\` not found. No background tasks exist.`;
|
|
1165
|
+
}
|
|
1166
|
+
const taskList = allTasks.map((t) => `- \`${t.id}\`: ${t.command.substring(0, 30)}...`).join("\n");
|
|
1167
|
+
return `\u274C Task \`${taskId}\` not found.
|
|
1168
|
+
|
|
1169
|
+
**Available tasks:**
|
|
1170
|
+
${taskList}`;
|
|
1171
|
+
}
|
|
1172
|
+
const duration = backgroundTaskManager.formatDuration(task);
|
|
1173
|
+
const statusEmoji = backgroundTaskManager.getStatusEmoji(task.status);
|
|
1174
|
+
let output = task.output;
|
|
1175
|
+
let stderr = task.errorOutput;
|
|
1176
|
+
if (tailLines && tailLines > 0) {
|
|
1177
|
+
const outputLines = output.split("\n");
|
|
1178
|
+
const stderrLines = stderr.split("\n");
|
|
1179
|
+
output = outputLines.slice(-tailLines).join("\n");
|
|
1180
|
+
stderr = stderrLines.slice(-tailLines).join("\n");
|
|
1181
|
+
}
|
|
1182
|
+
const maxLen = 1e4;
|
|
1183
|
+
if (output.length > maxLen) {
|
|
1184
|
+
output = `[...truncated ${output.length - maxLen} chars...]
|
|
1185
|
+
` + output.substring(output.length - maxLen);
|
|
1186
|
+
}
|
|
1187
|
+
if (stderr.length > maxLen) {
|
|
1188
|
+
stderr = `[...truncated ${stderr.length - maxLen} chars...]
|
|
1189
|
+
` + stderr.substring(stderr.length - maxLen);
|
|
1190
|
+
}
|
|
1191
|
+
const labelDisplay = task.label ? ` (${task.label})` : "";
|
|
1192
|
+
let result = `${statusEmoji} **Task ${task.id}**${labelDisplay}
|
|
1193
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1194
|
+
| Property | Value |
|
|
1195
|
+
|----------|-------|
|
|
1196
|
+
| **Command** | \`${task.command}\` |
|
|
1197
|
+
| **Status** | ${statusEmoji} **${task.status.toUpperCase()}** |
|
|
1198
|
+
| **Duration** | ${duration}${task.status === "running" ? " (ongoing)" : ""} |
|
|
1199
|
+
${task.exitCode !== null ? `| **Exit Code** | ${task.exitCode} |` : ""}
|
|
1200
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`;
|
|
1201
|
+
if (output.trim()) {
|
|
1202
|
+
result += `
|
|
1203
|
+
|
|
1204
|
+
\u{1F4E4} **Output (stdout)**:
|
|
1205
|
+
\`\`\`
|
|
1206
|
+
${output.trim()}
|
|
1207
|
+
\`\`\``;
|
|
1208
|
+
}
|
|
1209
|
+
if (stderr.trim()) {
|
|
1210
|
+
result += `
|
|
1211
|
+
|
|
1212
|
+
\u26A0\uFE0F **Errors (stderr)**:
|
|
1213
|
+
\`\`\`
|
|
1214
|
+
${stderr.trim()}
|
|
1215
|
+
\`\`\``;
|
|
1216
|
+
}
|
|
1217
|
+
if (task.status === "running") {
|
|
1218
|
+
result += `
|
|
1219
|
+
|
|
1220
|
+
\u23F3 Task still running... Check again later with:
|
|
1221
|
+
\`check_background({ taskId: "${task.id}" })\``;
|
|
1222
|
+
}
|
|
1223
|
+
return result;
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
var listBackgroundTool = tool4({
|
|
1227
|
+
description: `List all background tasks and their current status.
|
|
1228
|
+
|
|
1229
|
+
<purpose>
|
|
1230
|
+
Get an overview of all running and completed background tasks.
|
|
1231
|
+
Useful to check what's in progress before starting new tasks.
|
|
1232
|
+
</purpose>`,
|
|
1233
|
+
args: {
|
|
1234
|
+
status: tool4.schema.enum(["all", "running", "done", "error"]).optional().describe("Filter by status (default: all)")
|
|
1235
|
+
},
|
|
1236
|
+
async execute(args) {
|
|
1237
|
+
const { status = "all" } = args;
|
|
1238
|
+
let tasks;
|
|
1239
|
+
if (status === "all") {
|
|
1240
|
+
tasks = backgroundTaskManager.getAll();
|
|
1241
|
+
} else {
|
|
1242
|
+
tasks = backgroundTaskManager.getByStatus(status);
|
|
1243
|
+
}
|
|
1244
|
+
if (tasks.length === 0) {
|
|
1245
|
+
return `\u{1F4CB} **No background tasks** ${status !== "all" ? `with status "${status}"` : ""}
|
|
1246
|
+
|
|
1247
|
+
Use \`run_background\` to start a new background task.`;
|
|
1248
|
+
}
|
|
1249
|
+
tasks.sort((a, b) => b.startTime - a.startTime);
|
|
1250
|
+
const rows = tasks.map((task) => {
|
|
1251
|
+
const emoji = backgroundTaskManager.getStatusEmoji(task.status);
|
|
1252
|
+
const duration = backgroundTaskManager.formatDuration(task);
|
|
1253
|
+
const cmdShort = task.command.length > 25 ? task.command.substring(0, 22) + "..." : task.command;
|
|
1254
|
+
const labelPart = task.label ? ` [${task.label}]` : "";
|
|
1255
|
+
return `| \`${task.id}\` | ${emoji} ${task.status.padEnd(7)} | ${cmdShort.padEnd(25)}${labelPart} | ${duration.padStart(8)} |`;
|
|
1256
|
+
}).join("\n");
|
|
1257
|
+
const runningCount = tasks.filter((t) => t.status === "running").length;
|
|
1258
|
+
const doneCount = tasks.filter((t) => t.status === "done").length;
|
|
1259
|
+
const errorCount = tasks.filter((t) => t.status === "error" || t.status === "timeout").length;
|
|
1260
|
+
return `\u{1F4CB} **Background Tasks** (${tasks.length} total)
|
|
1261
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1262
|
+
| \u23F3 Running: ${runningCount} | \u2705 Done: ${doneCount} | \u274C Error/Timeout: ${errorCount} |
|
|
1263
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1264
|
+
|
|
1265
|
+
| Task ID | Status | Command | Duration |
|
|
1266
|
+
|---------|--------|---------|----------|
|
|
1267
|
+
${rows}
|
|
1268
|
+
|
|
1269
|
+
\u{1F4A1} Use \`check_background({ taskId: "job_xxxxx" })\` to see full output.`;
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
var killBackgroundTool = tool4({
|
|
1273
|
+
description: `Kill a running background task.
|
|
1274
|
+
|
|
1275
|
+
<purpose>
|
|
1276
|
+
Stop a background task that is taking too long or no longer needed.
|
|
1277
|
+
</purpose>`,
|
|
1278
|
+
args: {
|
|
1279
|
+
taskId: tool4.schema.string().describe("Task ID to kill (e.g., job_a1b2c3d4)")
|
|
1280
|
+
},
|
|
1281
|
+
async execute(args) {
|
|
1282
|
+
const { taskId } = args;
|
|
1283
|
+
const task = backgroundTaskManager.get(taskId);
|
|
1284
|
+
if (!task) {
|
|
1285
|
+
return `\u274C Task \`${taskId}\` not found.`;
|
|
1286
|
+
}
|
|
1287
|
+
if (task.status !== "running") {
|
|
1288
|
+
return `\u26A0\uFE0F Task \`${taskId}\` is not running (status: ${task.status}).`;
|
|
1289
|
+
}
|
|
1290
|
+
const killed = backgroundTaskManager.kill(taskId);
|
|
1291
|
+
if (killed) {
|
|
1292
|
+
return `\u{1F6D1} Task \`${taskId}\` has been killed.
|
|
1293
|
+
Command: \`${task.command}\`
|
|
1294
|
+
Duration before kill: ${backgroundTaskManager.formatDuration(task)}`;
|
|
1295
|
+
}
|
|
1296
|
+
return `\u26A0\uFE0F Could not kill task \`${taskId}\`. It may have already finished.`;
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
// src/core/async-agent.ts
|
|
1301
|
+
var TASK_TTL_MS = 30 * 60 * 1e3;
|
|
1302
|
+
var CLEANUP_DELAY_MS = 5 * 60 * 1e3;
|
|
1303
|
+
var MIN_STABILITY_MS = 5 * 1e3;
|
|
1304
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
1305
|
+
var DEFAULT_CONCURRENCY = 3;
|
|
1306
|
+
var DEBUG = process.env.DEBUG_PARALLEL_AGENT === "true";
|
|
1307
|
+
var log = (...args) => {
|
|
1308
|
+
if (DEBUG) console.log("[parallel-agent]", ...args);
|
|
1309
|
+
};
|
|
1310
|
+
var ConcurrencyController = class {
|
|
1311
|
+
counts = /* @__PURE__ */ new Map();
|
|
1312
|
+
queues = /* @__PURE__ */ new Map();
|
|
1313
|
+
limits = /* @__PURE__ */ new Map();
|
|
1314
|
+
setLimit(key, limit) {
|
|
1315
|
+
this.limits.set(key, limit);
|
|
1316
|
+
}
|
|
1317
|
+
getLimit(key) {
|
|
1318
|
+
return this.limits.get(key) ?? DEFAULT_CONCURRENCY;
|
|
1319
|
+
}
|
|
1320
|
+
async acquire(key) {
|
|
1321
|
+
const limit = this.getLimit(key);
|
|
1322
|
+
if (limit === 0) return;
|
|
1323
|
+
const current = this.counts.get(key) ?? 0;
|
|
1324
|
+
if (current < limit) {
|
|
1325
|
+
this.counts.set(key, current + 1);
|
|
1326
|
+
log(`Acquired slot for ${key}: ${current + 1}/${limit}`);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
log(`Queueing for ${key}: ${current}/${limit} (waiting...)`);
|
|
1330
|
+
return new Promise((resolve) => {
|
|
1331
|
+
const queue = this.queues.get(key) ?? [];
|
|
1332
|
+
queue.push(resolve);
|
|
1333
|
+
this.queues.set(key, queue);
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
release(key) {
|
|
1337
|
+
const limit = this.getLimit(key);
|
|
1338
|
+
if (limit === 0) return;
|
|
1339
|
+
const queue = this.queues.get(key);
|
|
1340
|
+
if (queue && queue.length > 0) {
|
|
1341
|
+
const next = queue.shift();
|
|
1342
|
+
log(`Released slot for ${key}: next in queue`);
|
|
1343
|
+
next();
|
|
1344
|
+
} else {
|
|
1345
|
+
const current = this.counts.get(key) ?? 0;
|
|
1346
|
+
if (current > 0) {
|
|
1347
|
+
this.counts.set(key, current - 1);
|
|
1348
|
+
log(`Released slot for ${key}: ${current - 1}`);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
getQueueLength(key) {
|
|
1353
|
+
return this.queues.get(key)?.length ?? 0;
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
var ParallelAgentManager = class _ParallelAgentManager {
|
|
1357
|
+
static _instance;
|
|
1358
|
+
// Core state
|
|
1359
|
+
tasks = /* @__PURE__ */ new Map();
|
|
1360
|
+
pendingByParent = /* @__PURE__ */ new Map();
|
|
1361
|
+
notifications = /* @__PURE__ */ new Map();
|
|
1362
|
+
// Dependencies
|
|
1363
|
+
client;
|
|
1364
|
+
directory;
|
|
1365
|
+
concurrency;
|
|
1366
|
+
// Polling
|
|
1367
|
+
pollingInterval;
|
|
1368
|
+
constructor(client, directory) {
|
|
1369
|
+
this.client = client;
|
|
1370
|
+
this.directory = directory;
|
|
1371
|
+
this.concurrency = new ConcurrencyController();
|
|
1372
|
+
}
|
|
1373
|
+
static getInstance(client, directory) {
|
|
1374
|
+
if (!_ParallelAgentManager._instance) {
|
|
1375
|
+
if (!client || !directory) {
|
|
1376
|
+
throw new Error("ParallelAgentManager requires client and directory on first call");
|
|
1377
|
+
}
|
|
1378
|
+
_ParallelAgentManager._instance = new _ParallelAgentManager(client, directory);
|
|
1379
|
+
}
|
|
1380
|
+
return _ParallelAgentManager._instance;
|
|
1381
|
+
}
|
|
1382
|
+
// ========================================================================
|
|
1383
|
+
// Public API
|
|
1384
|
+
// ========================================================================
|
|
1385
|
+
/**
|
|
1386
|
+
* Launch an agent in a new session (async, non-blocking)
|
|
1387
|
+
*/
|
|
1388
|
+
async launch(input) {
|
|
1389
|
+
const concurrencyKey = input.agent;
|
|
1390
|
+
await this.concurrency.acquire(concurrencyKey);
|
|
1391
|
+
this.pruneExpiredTasks();
|
|
1392
|
+
try {
|
|
1393
|
+
const createResult = await this.client.session.create({
|
|
1394
|
+
body: {
|
|
1395
|
+
parentID: input.parentSessionID,
|
|
1396
|
+
title: `Parallel: ${input.description}`
|
|
1397
|
+
},
|
|
1398
|
+
query: {
|
|
1399
|
+
directory: this.directory
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
if (createResult.error) {
|
|
1403
|
+
this.concurrency.release(concurrencyKey);
|
|
1404
|
+
throw new Error(`Failed to create session: ${createResult.error}`);
|
|
1405
|
+
}
|
|
1406
|
+
const sessionID = createResult.data.id;
|
|
1407
|
+
const taskId = `task_${crypto.randomUUID().slice(0, 8)}`;
|
|
1408
|
+
const task = {
|
|
1409
|
+
id: taskId,
|
|
1410
|
+
sessionID,
|
|
1411
|
+
parentSessionID: input.parentSessionID,
|
|
1412
|
+
description: input.description,
|
|
1413
|
+
agent: input.agent,
|
|
1414
|
+
status: "running",
|
|
1415
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
1416
|
+
concurrencyKey
|
|
1417
|
+
};
|
|
1418
|
+
this.tasks.set(taskId, task);
|
|
1419
|
+
this.trackPending(input.parentSessionID, taskId);
|
|
1420
|
+
this.startPolling();
|
|
1421
|
+
this.client.session.prompt({
|
|
1422
|
+
path: { id: sessionID },
|
|
1423
|
+
body: {
|
|
1424
|
+
agent: input.agent,
|
|
1425
|
+
parts: [{ type: "text", text: input.prompt }]
|
|
1426
|
+
}
|
|
1427
|
+
}).catch((error) => {
|
|
1428
|
+
log(`Prompt error for ${taskId}:`, error);
|
|
1429
|
+
this.handleTaskError(taskId, error);
|
|
1430
|
+
});
|
|
1431
|
+
log(`Launched ${taskId} in session ${sessionID}`);
|
|
1432
|
+
return task;
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
this.concurrency.release(concurrencyKey);
|
|
1435
|
+
throw error;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Get task by ID
|
|
1440
|
+
*/
|
|
1441
|
+
getTask(id) {
|
|
1442
|
+
return this.tasks.get(id);
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Get all running tasks
|
|
1446
|
+
*/
|
|
1447
|
+
getRunningTasks() {
|
|
1448
|
+
return Array.from(this.tasks.values()).filter((t) => t.status === "running");
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Get all tasks
|
|
1452
|
+
*/
|
|
1453
|
+
getAllTasks() {
|
|
1454
|
+
return Array.from(this.tasks.values());
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Get tasks by parent session
|
|
1458
|
+
*/
|
|
1459
|
+
getTasksByParent(parentSessionID) {
|
|
1460
|
+
return Array.from(this.tasks.values()).filter((t) => t.parentSessionID === parentSessionID);
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Cancel a running task
|
|
1464
|
+
*/
|
|
1465
|
+
async cancelTask(taskId) {
|
|
1466
|
+
const task = this.tasks.get(taskId);
|
|
1467
|
+
if (!task || task.status !== "running") {
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
task.status = "error";
|
|
1471
|
+
task.error = "Cancelled by user";
|
|
1472
|
+
task.completedAt = /* @__PURE__ */ new Date();
|
|
1473
|
+
if (task.concurrencyKey) {
|
|
1474
|
+
this.concurrency.release(task.concurrencyKey);
|
|
1475
|
+
}
|
|
1476
|
+
this.untrackPending(task.parentSessionID, taskId);
|
|
1477
|
+
try {
|
|
1478
|
+
await this.client.session.delete({
|
|
1479
|
+
path: { id: task.sessionID }
|
|
1480
|
+
});
|
|
1481
|
+
console.log(`[parallel] \u{1F5D1}\uFE0F Session ${task.sessionID.slice(0, 8)}... deleted`);
|
|
1482
|
+
} catch {
|
|
1483
|
+
console.log(`[parallel] \u{1F5D1}\uFE0F Session ${task.sessionID.slice(0, 8)}... already gone`);
|
|
1484
|
+
}
|
|
1485
|
+
this.scheduleCleanup(taskId);
|
|
1486
|
+
console.log(`[parallel] \u{1F6D1} CANCELLED ${taskId}`);
|
|
1487
|
+
log(`Cancelled ${taskId}`);
|
|
1488
|
+
return true;
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Get result from completed task
|
|
1492
|
+
*/
|
|
1493
|
+
async getResult(taskId) {
|
|
1494
|
+
const task = this.tasks.get(taskId);
|
|
1495
|
+
if (!task) return null;
|
|
1496
|
+
if (task.result) return task.result;
|
|
1497
|
+
if (task.status === "error") return `Error: ${task.error}`;
|
|
1498
|
+
if (task.status === "running") return null;
|
|
1499
|
+
try {
|
|
1500
|
+
const messagesResult = await this.client.session.messages({
|
|
1501
|
+
path: { id: task.sessionID }
|
|
1502
|
+
});
|
|
1503
|
+
if (messagesResult.error) {
|
|
1504
|
+
return `Error: ${messagesResult.error}`;
|
|
1505
|
+
}
|
|
1506
|
+
const messages = messagesResult.data ?? [];
|
|
1507
|
+
const assistantMsgs = messages.filter((m) => m.info?.role === "assistant").reverse();
|
|
1508
|
+
const lastMsg = assistantMsgs[0];
|
|
1509
|
+
if (!lastMsg) return "(No response)";
|
|
1510
|
+
const textParts = lastMsg.parts?.filter(
|
|
1511
|
+
(p) => p.type === "text" || p.type === "reasoning"
|
|
1512
|
+
) ?? [];
|
|
1513
|
+
const result = textParts.map((p) => p.text ?? "").filter(Boolean).join("\n");
|
|
1514
|
+
task.result = result;
|
|
1515
|
+
return result;
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
return `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Set concurrency limit for agent type
|
|
1522
|
+
*/
|
|
1523
|
+
setConcurrencyLimit(agentType, limit) {
|
|
1524
|
+
this.concurrency.setLimit(agentType, limit);
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Get pending notification count
|
|
1528
|
+
*/
|
|
1529
|
+
getPendingCount(parentSessionID) {
|
|
1530
|
+
return this.pendingByParent.get(parentSessionID)?.size ?? 0;
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Cleanup all state
|
|
1534
|
+
*/
|
|
1535
|
+
cleanup() {
|
|
1536
|
+
this.stopPolling();
|
|
1537
|
+
this.tasks.clear();
|
|
1538
|
+
this.pendingByParent.clear();
|
|
1539
|
+
this.notifications.clear();
|
|
1540
|
+
}
|
|
1541
|
+
// ========================================================================
|
|
1542
|
+
// Internal: Tracking
|
|
1543
|
+
// ========================================================================
|
|
1544
|
+
trackPending(parentSessionID, taskId) {
|
|
1545
|
+
const pending = this.pendingByParent.get(parentSessionID) ?? /* @__PURE__ */ new Set();
|
|
1546
|
+
pending.add(taskId);
|
|
1547
|
+
this.pendingByParent.set(parentSessionID, pending);
|
|
1548
|
+
}
|
|
1549
|
+
untrackPending(parentSessionID, taskId) {
|
|
1550
|
+
const pending = this.pendingByParent.get(parentSessionID);
|
|
1551
|
+
if (pending) {
|
|
1552
|
+
pending.delete(taskId);
|
|
1553
|
+
if (pending.size === 0) {
|
|
1554
|
+
this.pendingByParent.delete(parentSessionID);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
// ========================================================================
|
|
1559
|
+
// Internal: Error Handling
|
|
1560
|
+
// ========================================================================
|
|
1561
|
+
handleTaskError(taskId, error) {
|
|
1562
|
+
const task = this.tasks.get(taskId);
|
|
1563
|
+
if (!task) return;
|
|
1564
|
+
task.status = "error";
|
|
1565
|
+
task.error = error instanceof Error ? error.message : String(error);
|
|
1566
|
+
task.completedAt = /* @__PURE__ */ new Date();
|
|
1567
|
+
if (task.concurrencyKey) {
|
|
1568
|
+
this.concurrency.release(task.concurrencyKey);
|
|
1569
|
+
}
|
|
1570
|
+
this.untrackPending(task.parentSessionID, taskId);
|
|
1571
|
+
this.queueNotification(task);
|
|
1572
|
+
this.notifyParentIfAllComplete(task.parentSessionID);
|
|
1573
|
+
this.scheduleCleanup(taskId);
|
|
1574
|
+
}
|
|
1575
|
+
// ========================================================================
|
|
1576
|
+
// Internal: Polling
|
|
1577
|
+
// ========================================================================
|
|
1578
|
+
startPolling() {
|
|
1579
|
+
if (this.pollingInterval) return;
|
|
1580
|
+
this.pollingInterval = setInterval(() => {
|
|
1581
|
+
this.pollRunningTasks();
|
|
1582
|
+
}, POLL_INTERVAL_MS);
|
|
1583
|
+
this.pollingInterval.unref();
|
|
1584
|
+
}
|
|
1585
|
+
stopPolling() {
|
|
1586
|
+
if (this.pollingInterval) {
|
|
1587
|
+
clearInterval(this.pollingInterval);
|
|
1588
|
+
this.pollingInterval = void 0;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
async pollRunningTasks() {
|
|
1592
|
+
this.pruneExpiredTasks();
|
|
1593
|
+
const runningTasks = this.getRunningTasks();
|
|
1594
|
+
if (runningTasks.length === 0) {
|
|
1595
|
+
this.stopPolling();
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
try {
|
|
1599
|
+
const statusResult = await this.client.session.status();
|
|
1600
|
+
const allStatuses = statusResult.data ?? {};
|
|
1601
|
+
for (const task of runningTasks) {
|
|
1602
|
+
const sessionStatus = allStatuses[task.sessionID];
|
|
1603
|
+
if (sessionStatus?.type === "idle") {
|
|
1604
|
+
const elapsed = Date.now() - task.startedAt.getTime();
|
|
1605
|
+
if (elapsed < MIN_STABILITY_MS) continue;
|
|
1606
|
+
const hasOutput = await this.validateSessionHasOutput(task.sessionID);
|
|
1607
|
+
if (!hasOutput) continue;
|
|
1608
|
+
task.status = "completed";
|
|
1609
|
+
task.completedAt = /* @__PURE__ */ new Date();
|
|
1610
|
+
if (task.concurrencyKey) {
|
|
1611
|
+
this.concurrency.release(task.concurrencyKey);
|
|
1612
|
+
}
|
|
1613
|
+
this.untrackPending(task.parentSessionID, task.id);
|
|
1614
|
+
this.queueNotification(task);
|
|
1615
|
+
this.notifyParentIfAllComplete(task.parentSessionID);
|
|
1616
|
+
this.scheduleCleanup(task.id);
|
|
1617
|
+
const duration = this.formatDuration(task.startedAt, task.completedAt);
|
|
1618
|
+
console.log(`[parallel] \u2705 COMPLETED ${task.id} \u2192 ${task.agent}: ${task.description} (${duration})`);
|
|
1619
|
+
log(`Completed ${task.id}`);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
log("Polling error:", error);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
// ========================================================================
|
|
1627
|
+
// Internal: Validation
|
|
1628
|
+
// ========================================================================
|
|
1629
|
+
async validateSessionHasOutput(sessionID) {
|
|
1630
|
+
try {
|
|
1631
|
+
const response = await this.client.session.messages({
|
|
1632
|
+
path: { id: sessionID }
|
|
1633
|
+
});
|
|
1634
|
+
const messages = response.data ?? [];
|
|
1635
|
+
const hasContent = messages.some((m) => {
|
|
1636
|
+
if (m.info?.role !== "assistant") return false;
|
|
1637
|
+
const parts = m.parts ?? [];
|
|
1638
|
+
return parts.some(
|
|
1639
|
+
(p) => p.type === "text" && p.text?.trim() || p.type === "reasoning" && p.text?.trim() || p.type === "tool"
|
|
1640
|
+
);
|
|
1641
|
+
});
|
|
1642
|
+
return hasContent;
|
|
1643
|
+
} catch {
|
|
1644
|
+
return true;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
// ========================================================================
|
|
1648
|
+
// Internal: Cleanup & TTL
|
|
1649
|
+
// ========================================================================
|
|
1650
|
+
pruneExpiredTasks() {
|
|
1651
|
+
const now = Date.now();
|
|
1652
|
+
for (const [taskId, task] of this.tasks.entries()) {
|
|
1653
|
+
const age = now - task.startedAt.getTime();
|
|
1654
|
+
if (age > TASK_TTL_MS) {
|
|
1655
|
+
log(`Timeout: ${taskId} (${Math.round(age / 1e3)}s)`);
|
|
1656
|
+
if (task.status === "running") {
|
|
1657
|
+
task.status = "timeout";
|
|
1658
|
+
task.error = "Task exceeded 30 minute time limit";
|
|
1659
|
+
task.completedAt = /* @__PURE__ */ new Date();
|
|
1660
|
+
if (task.concurrencyKey) {
|
|
1661
|
+
this.concurrency.release(task.concurrencyKey);
|
|
1662
|
+
}
|
|
1663
|
+
this.untrackPending(task.parentSessionID, taskId);
|
|
1664
|
+
console.log(`[parallel] \u23F1\uFE0F TIMEOUT ${taskId} \u2192 ${task.agent}: ${task.description}`);
|
|
1665
|
+
}
|
|
1666
|
+
this.client.session.delete({
|
|
1667
|
+
path: { id: task.sessionID }
|
|
1668
|
+
}).then(() => {
|
|
1669
|
+
console.log(`[parallel] \u{1F5D1}\uFE0F CLEANED ${taskId} (timeout session deleted)`);
|
|
1670
|
+
}).catch(() => {
|
|
1671
|
+
console.log(`[parallel] \u{1F5D1}\uFE0F CLEANED ${taskId} (timeout session already gone)`);
|
|
1672
|
+
});
|
|
1673
|
+
this.tasks.delete(taskId);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
for (const [sessionID, queue] of this.notifications.entries()) {
|
|
1677
|
+
if (queue.length === 0) {
|
|
1678
|
+
this.notifications.delete(sessionID);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
scheduleCleanup(taskId) {
|
|
1683
|
+
const task = this.tasks.get(taskId);
|
|
1684
|
+
const sessionID = task?.sessionID;
|
|
1685
|
+
setTimeout(async () => {
|
|
1686
|
+
if (sessionID) {
|
|
1687
|
+
try {
|
|
1688
|
+
await this.client.session.delete({
|
|
1689
|
+
path: { id: sessionID }
|
|
1690
|
+
});
|
|
1691
|
+
console.log(`[parallel] \u{1F5D1}\uFE0F CLEANED ${taskId} (session deleted)`);
|
|
1692
|
+
log(`Deleted session ${sessionID}`);
|
|
1693
|
+
} catch {
|
|
1694
|
+
console.log(`[parallel] \u{1F5D1}\uFE0F CLEANED ${taskId} (session already gone)`);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
this.tasks.delete(taskId);
|
|
1698
|
+
log(`Cleaned up ${taskId} from memory`);
|
|
1699
|
+
}, CLEANUP_DELAY_MS);
|
|
1700
|
+
}
|
|
1701
|
+
// ========================================================================
|
|
1702
|
+
// Internal: Notifications
|
|
1703
|
+
// ========================================================================
|
|
1704
|
+
queueNotification(task) {
|
|
1705
|
+
const queue = this.notifications.get(task.parentSessionID) ?? [];
|
|
1706
|
+
queue.push(task);
|
|
1707
|
+
this.notifications.set(task.parentSessionID, queue);
|
|
1708
|
+
}
|
|
1709
|
+
async notifyParentIfAllComplete(parentSessionID) {
|
|
1710
|
+
const pending = this.pendingByParent.get(parentSessionID);
|
|
1711
|
+
if (pending && pending.size > 0) {
|
|
1712
|
+
log(`${pending.size} tasks still pending for ${parentSessionID}`);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
const completedTasks = this.notifications.get(parentSessionID) ?? [];
|
|
1716
|
+
if (completedTasks.length === 0) return;
|
|
1717
|
+
const summary = completedTasks.map((t) => {
|
|
1718
|
+
const status = t.status === "completed" ? "\u2705" : "\u274C";
|
|
1719
|
+
return `${status} \`${t.id}\`: ${t.description}`;
|
|
1720
|
+
}).join("\n");
|
|
1721
|
+
const notification = `<system-notification>
|
|
1722
|
+
**All Parallel Tasks Complete**
|
|
1723
|
+
|
|
1724
|
+
${summary}
|
|
1725
|
+
|
|
1726
|
+
Use \`get_task_result({ taskId: "task_xxx" })\` to retrieve results.
|
|
1727
|
+
</system-notification>`;
|
|
1728
|
+
try {
|
|
1729
|
+
await this.client.session.prompt({
|
|
1730
|
+
path: { id: parentSessionID },
|
|
1731
|
+
body: {
|
|
1732
|
+
noReply: true,
|
|
1733
|
+
parts: [{ type: "text", text: notification }]
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
log(`Notified parent ${parentSessionID}: ${completedTasks.length} tasks`);
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
log("Notification error:", error);
|
|
1739
|
+
}
|
|
1740
|
+
this.notifications.delete(parentSessionID);
|
|
1741
|
+
}
|
|
1742
|
+
// ========================================================================
|
|
1743
|
+
// Internal: Formatting
|
|
1744
|
+
// ========================================================================
|
|
1745
|
+
formatDuration(start, end) {
|
|
1746
|
+
const duration = (end ?? /* @__PURE__ */ new Date()).getTime() - start.getTime();
|
|
1747
|
+
const seconds = Math.floor(duration / 1e3);
|
|
1748
|
+
const minutes = Math.floor(seconds / 60);
|
|
1749
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
1750
|
+
return `${seconds}s`;
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
var parallelAgentManager = {
|
|
1754
|
+
getInstance: ParallelAgentManager.getInstance.bind(ParallelAgentManager)
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
// src/tools/async-agent.ts
|
|
1758
|
+
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
1759
|
+
var createDelegateTaskTool = (manager, client) => tool5({
|
|
1760
|
+
description: `Delegate a task to an agent.
|
|
1761
|
+
|
|
1762
|
+
<mode>
|
|
1763
|
+
- background=true: Non-blocking. Task runs in parallel session. Use get_task_result later.
|
|
1764
|
+
- background=false: Blocking. Waits for result. Use when you need output immediately.
|
|
1765
|
+
</mode>
|
|
1766
|
+
|
|
1767
|
+
<when_to_use_background_true>
|
|
1768
|
+
- Multiple independent tasks to run in parallel
|
|
1769
|
+
- Long-running tasks (build, test, analysis)
|
|
1770
|
+
- You have other work to do while waiting
|
|
1771
|
+
- Example: "Build module A" + "Test module B" in parallel
|
|
1772
|
+
</when_to_use_background_true>
|
|
1773
|
+
|
|
1774
|
+
<when_to_use_background_false>
|
|
1775
|
+
- You need the result for the very next step
|
|
1776
|
+
- Single task with nothing else to do
|
|
1777
|
+
- Quick questions that return fast
|
|
1778
|
+
</when_to_use_background_false>
|
|
1779
|
+
|
|
1780
|
+
<safety>
|
|
1781
|
+
- Max 3 background tasks per agent type (extras queue automatically)
|
|
1782
|
+
- Auto-timeout: 30 minutes
|
|
1783
|
+
- Auto-cleanup: 5 minutes after completion
|
|
1784
|
+
</safety>`,
|
|
1785
|
+
args: {
|
|
1786
|
+
agent: tool5.schema.string().describe("Agent name (e.g., 'builder', 'inspector', 'architect')"),
|
|
1787
|
+
description: tool5.schema.string().describe("Short task description"),
|
|
1788
|
+
prompt: tool5.schema.string().describe("Full prompt/instructions for the agent"),
|
|
1789
|
+
background: tool5.schema.boolean().describe("true=async (returns task_id), false=sync (waits for result). REQUIRED.")
|
|
1790
|
+
},
|
|
1791
|
+
async execute(args, context) {
|
|
1792
|
+
const { agent, description, prompt, background } = args;
|
|
1793
|
+
const ctx = context;
|
|
1794
|
+
const sessionClient = client;
|
|
1795
|
+
if (background === void 0) {
|
|
1796
|
+
return `\u274C 'background' parameter is REQUIRED.
|
|
1797
|
+
- background=true: Run in parallel, returns task ID
|
|
1798
|
+
- background=false: Wait for result (blocking)`;
|
|
1799
|
+
}
|
|
1800
|
+
if (background === true) {
|
|
1801
|
+
try {
|
|
1802
|
+
const task = await manager.launch({
|
|
1803
|
+
agent,
|
|
1804
|
+
description,
|
|
1805
|
+
prompt,
|
|
1806
|
+
parentSessionID: ctx.sessionID
|
|
1807
|
+
});
|
|
1808
|
+
const runningCount = manager.getRunningTasks().length;
|
|
1809
|
+
const pendingCount = manager.getPendingCount(ctx.sessionID);
|
|
1810
|
+
console.log(`[parallel] \u{1F680} SPAWNED ${task.id} \u2192 ${agent}: ${description}`);
|
|
1811
|
+
return `
|
|
1812
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
1813
|
+
\u2551 \u{1F680} BACKGROUND TASK SPAWNED \u2551
|
|
1814
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
1815
|
+
\u2551 Task ID: ${task.id.padEnd(45)}\u2551
|
|
1816
|
+
\u2551 Agent: ${task.agent.padEnd(45)}\u2551
|
|
1817
|
+
\u2551 Description: ${task.description.slice(0, 45).padEnd(45)}\u2551
|
|
1818
|
+
\u2551 Status: \u23F3 RUNNING (background) \u2551
|
|
1819
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
1820
|
+
\u2551 Running: ${String(runningCount).padEnd(5)} \u2502 Pending: ${String(pendingCount).padEnd(5)} \u2551
|
|
1821
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
1822
|
+
|
|
1823
|
+
\u{1F4CC} Continue your work! System notifies when ALL complete.
|
|
1824
|
+
\u{1F50D} Use \`get_task_result({ taskId: "${task.id}" })\` later.`;
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1827
|
+
console.log(`[parallel] \u274C FAILED: ${message}`);
|
|
1828
|
+
return `\u274C Failed to spawn background task: ${message}`;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
console.log(`[delegate] \u23F3 SYNC ${agent}: ${description}`);
|
|
1832
|
+
try {
|
|
1833
|
+
const session = sessionClient.session;
|
|
1834
|
+
const directory = ".";
|
|
1835
|
+
const createResult = await session.create({
|
|
1836
|
+
body: {
|
|
1837
|
+
parentID: ctx.sessionID,
|
|
1838
|
+
title: `Task: ${description}`
|
|
1839
|
+
},
|
|
1840
|
+
query: { directory }
|
|
1841
|
+
});
|
|
1842
|
+
if (createResult.error || !createResult.data?.id) {
|
|
1843
|
+
return `\u274C Failed to create session: ${createResult.error || "No session ID"}`;
|
|
1844
|
+
}
|
|
1845
|
+
const sessionID = createResult.data.id;
|
|
1846
|
+
const startTime = Date.now();
|
|
1847
|
+
try {
|
|
1848
|
+
await session.prompt({
|
|
1849
|
+
path: { id: sessionID },
|
|
1850
|
+
body: {
|
|
1851
|
+
agent,
|
|
1852
|
+
parts: [{ type: "text", text: prompt }]
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
} catch (promptError) {
|
|
1856
|
+
const errorMessage = promptError instanceof Error ? promptError.message : String(promptError);
|
|
1857
|
+
return `\u274C Failed to send prompt: ${errorMessage}
|
|
1858
|
+
|
|
1859
|
+
Session ID: ${sessionID}`;
|
|
1860
|
+
}
|
|
1861
|
+
const POLL_INTERVAL_MS2 = 500;
|
|
1862
|
+
const MAX_POLL_TIME_MS = 10 * 60 * 1e3;
|
|
1863
|
+
const MIN_STABILITY_MS2 = 5e3;
|
|
1864
|
+
const STABILITY_POLLS_REQUIRED = 3;
|
|
1865
|
+
let stablePolls = 0;
|
|
1866
|
+
let lastMsgCount = 0;
|
|
1867
|
+
while (Date.now() - startTime < MAX_POLL_TIME_MS) {
|
|
1868
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS2));
|
|
1869
|
+
const statusResult = await session.status();
|
|
1870
|
+
const allStatuses = statusResult.data ?? {};
|
|
1871
|
+
const sessionStatus = allStatuses[sessionID];
|
|
1872
|
+
if (sessionStatus?.type !== "idle") {
|
|
1873
|
+
stablePolls = 0;
|
|
1874
|
+
lastMsgCount = 0;
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
if (Date.now() - startTime < MIN_STABILITY_MS2) continue;
|
|
1878
|
+
const messagesResult2 = await session.messages({ path: { id: sessionID } });
|
|
1879
|
+
const messages2 = messagesResult2.data ?? [];
|
|
1880
|
+
const currentMsgCount = messages2.length;
|
|
1881
|
+
if (currentMsgCount === lastMsgCount) {
|
|
1882
|
+
stablePolls++;
|
|
1883
|
+
if (stablePolls >= STABILITY_POLLS_REQUIRED) break;
|
|
1884
|
+
} else {
|
|
1885
|
+
stablePolls = 0;
|
|
1886
|
+
lastMsgCount = currentMsgCount;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
const messagesResult = await session.messages({ path: { id: sessionID } });
|
|
1890
|
+
const messages = messagesResult.data ?? [];
|
|
1891
|
+
const assistantMsgs = messages.filter((m) => m.info?.role === "assistant").reverse();
|
|
1892
|
+
const lastMsg = assistantMsgs[0];
|
|
1893
|
+
if (!lastMsg) {
|
|
1894
|
+
return `\u274C No assistant response found.
|
|
1895
|
+
|
|
1896
|
+
Session ID: ${sessionID}`;
|
|
1897
|
+
}
|
|
1898
|
+
const textParts = lastMsg.parts?.filter(
|
|
1899
|
+
(p) => p.type === "text" || p.type === "reasoning"
|
|
1900
|
+
) ?? [];
|
|
1901
|
+
const textContent = textParts.map((p) => p.text ?? "").filter(Boolean).join("\n");
|
|
1902
|
+
const duration = Math.floor((Date.now() - startTime) / 1e3);
|
|
1903
|
+
console.log(`[delegate] \u2705 COMPLETED ${agent}: ${description} (${duration}s)`);
|
|
1904
|
+
return `\u2705 **Task Completed** (${duration}s)
|
|
1905
|
+
|
|
1906
|
+
Agent: ${agent}
|
|
1907
|
+
Session ID: ${sessionID}
|
|
1908
|
+
|
|
1909
|
+
---
|
|
1910
|
+
|
|
1911
|
+
${textContent || "(No text output)"}`;
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1914
|
+
console.log(`[delegate] \u274C FAILED: ${message}`);
|
|
1915
|
+
return `\u274C Task failed: ${message}`;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
});
|
|
1919
|
+
var createGetTaskResultTool = (manager) => tool5({
|
|
1920
|
+
description: `Get the result from a completed background task.
|
|
1921
|
+
|
|
1922
|
+
<note>
|
|
1923
|
+
If the task is still running, returns status info.
|
|
1924
|
+
Wait for the "All Complete" notification before checking.
|
|
1925
|
+
</note>`,
|
|
1926
|
+
args: {
|
|
1927
|
+
taskId: tool5.schema.string().describe("Task ID from delegate_task (e.g., 'task_a1b2c3d4')")
|
|
1928
|
+
},
|
|
1929
|
+
async execute(args) {
|
|
1930
|
+
const { taskId } = args;
|
|
1931
|
+
const task = manager.getTask(taskId);
|
|
1932
|
+
if (!task) {
|
|
1933
|
+
return `\u274C Task not found: \`${taskId}\`
|
|
1934
|
+
|
|
1935
|
+
Use \`list_tasks\` to see available tasks.`;
|
|
1936
|
+
}
|
|
1937
|
+
if (task.status === "running") {
|
|
1938
|
+
const elapsed = Math.floor((Date.now() - task.startedAt.getTime()) / 1e3);
|
|
1939
|
+
return `\u23F3 **Task Still Running**
|
|
1940
|
+
|
|
1941
|
+
| Property | Value |
|
|
1942
|
+
|----------|-------|
|
|
1943
|
+
| **Task ID** | \`${taskId}\` |
|
|
1944
|
+
| **Agent** | ${task.agent} |
|
|
1945
|
+
| **Elapsed** | ${elapsed}s |
|
|
1946
|
+
|
|
1947
|
+
Wait for "All Complete" notification, then try again.`;
|
|
1948
|
+
}
|
|
1949
|
+
const result = await manager.getResult(taskId);
|
|
1950
|
+
const duration = manager.formatDuration(task.startedAt, task.completedAt);
|
|
1951
|
+
if (task.status === "error" || task.status === "timeout") {
|
|
1952
|
+
return `\u274C **Task ${task.status === "timeout" ? "Timed Out" : "Failed"}**
|
|
1953
|
+
|
|
1954
|
+
| Property | Value |
|
|
1955
|
+
|----------|-------|
|
|
1956
|
+
| **Task ID** | \`${taskId}\` |
|
|
1957
|
+
| **Agent** | ${task.agent} |
|
|
1958
|
+
| **Error** | ${task.error} |
|
|
1959
|
+
| **Duration** | ${duration} |`;
|
|
1960
|
+
}
|
|
1961
|
+
return `\u2705 **Task Completed**
|
|
1962
|
+
|
|
1963
|
+
| Property | Value |
|
|
1964
|
+
|----------|-------|
|
|
1965
|
+
| **Task ID** | \`${taskId}\` |
|
|
1966
|
+
| **Agent** | ${task.agent} |
|
|
1967
|
+
| **Duration** | ${duration} |
|
|
1968
|
+
|
|
1969
|
+
---
|
|
1970
|
+
|
|
1971
|
+
**Result:**
|
|
1972
|
+
|
|
1973
|
+
${result || "(No output)"}`;
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1976
|
+
var createListTasksTool = (manager) => tool5({
|
|
1977
|
+
description: `List all background tasks and their status.`,
|
|
1978
|
+
args: {
|
|
1979
|
+
status: tool5.schema.string().optional().describe("Filter: 'all', 'running', 'completed', 'error'")
|
|
1980
|
+
},
|
|
1981
|
+
async execute(args) {
|
|
1982
|
+
const { status = "all" } = args;
|
|
1983
|
+
let tasks;
|
|
1984
|
+
switch (status) {
|
|
1985
|
+
case "running":
|
|
1986
|
+
tasks = manager.getRunningTasks();
|
|
1987
|
+
break;
|
|
1988
|
+
case "completed":
|
|
1989
|
+
tasks = manager.getAllTasks().filter((t) => t.status === "completed");
|
|
1990
|
+
break;
|
|
1991
|
+
case "error":
|
|
1992
|
+
tasks = manager.getAllTasks().filter((t) => t.status === "error" || t.status === "timeout");
|
|
1993
|
+
break;
|
|
1994
|
+
default:
|
|
1995
|
+
tasks = manager.getAllTasks();
|
|
1996
|
+
}
|
|
1997
|
+
if (tasks.length === 0) {
|
|
1998
|
+
return `\u{1F4CB} No background tasks found${status !== "all" ? ` (filter: ${status})` : ""}.
|
|
1999
|
+
|
|
2000
|
+
Use \`delegate_task({ ..., background: true })\` to spawn background tasks.`;
|
|
2001
|
+
}
|
|
2002
|
+
const runningCount = manager.getRunningTasks().length;
|
|
2003
|
+
const completedCount = manager.getAllTasks().filter((t) => t.status === "completed").length;
|
|
2004
|
+
const errorCount = manager.getAllTasks().filter((t) => t.status === "error" || t.status === "timeout").length;
|
|
2005
|
+
const statusIcon = (s) => {
|
|
2006
|
+
switch (s) {
|
|
2007
|
+
case "running":
|
|
2008
|
+
return "\u23F3";
|
|
2009
|
+
case "completed":
|
|
2010
|
+
return "\u2705";
|
|
2011
|
+
case "error":
|
|
2012
|
+
return "\u274C";
|
|
2013
|
+
case "timeout":
|
|
2014
|
+
return "\u23F1\uFE0F";
|
|
2015
|
+
default:
|
|
2016
|
+
return "\u2753";
|
|
2017
|
+
}
|
|
2018
|
+
};
|
|
2019
|
+
const rows = tasks.map((t) => {
|
|
2020
|
+
const elapsed = Math.floor((Date.now() - t.startedAt.getTime()) / 1e3);
|
|
2021
|
+
const desc = t.description.length > 25 ? t.description.slice(0, 22) + "..." : t.description;
|
|
2022
|
+
return `| \`${t.id}\` | ${statusIcon(t.status)} ${t.status} | ${t.agent} | ${desc} | ${elapsed}s |`;
|
|
2023
|
+
}).join("\n");
|
|
2024
|
+
return `\u{1F4CB} **Background Tasks** (${tasks.length} shown)
|
|
2025
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
2026
|
+
| \u23F3 Running: ${runningCount} | \u2705 Completed: ${completedCount} | \u274C Error: ${errorCount} |
|
|
2027
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
2028
|
+
|
|
2029
|
+
| Task ID | Status | Agent | Description | Elapsed |
|
|
2030
|
+
|---------|--------|-------|-------------|---------|
|
|
2031
|
+
${rows}
|
|
2032
|
+
|
|
2033
|
+
\u{1F4A1} Use \`get_task_result({ taskId: "task_xxx" })\` to get results.
|
|
2034
|
+
\u{1F6D1} Use \`cancel_task({ taskId: "task_xxx" })\` to stop a running task.`;
|
|
2035
|
+
}
|
|
2036
|
+
});
|
|
2037
|
+
var createCancelTaskTool = (manager) => tool5({
|
|
2038
|
+
description: `Cancel a running background task.
|
|
2039
|
+
|
|
2040
|
+
<purpose>
|
|
2041
|
+
Stop a runaway or no-longer-needed task.
|
|
2042
|
+
Frees up concurrency slot for other tasks.
|
|
2043
|
+
</purpose>`,
|
|
2044
|
+
args: {
|
|
2045
|
+
taskId: tool5.schema.string().describe("Task ID to cancel (e.g., 'task_a1b2c3d4')")
|
|
2046
|
+
},
|
|
2047
|
+
async execute(args) {
|
|
2048
|
+
const { taskId } = args;
|
|
2049
|
+
const cancelled = await manager.cancelTask(taskId);
|
|
2050
|
+
if (cancelled) {
|
|
2051
|
+
return `\u{1F6D1} **Task Cancelled**
|
|
2052
|
+
|
|
2053
|
+
Task \`${taskId}\` has been stopped. Concurrency slot released.`;
|
|
2054
|
+
}
|
|
2055
|
+
const task = manager.getTask(taskId);
|
|
2056
|
+
if (task) {
|
|
2057
|
+
return `\u26A0\uFE0F Cannot cancel: Task \`${taskId}\` is ${task.status} (not running).`;
|
|
2058
|
+
}
|
|
2059
|
+
return `\u274C Task \`${taskId}\` not found.
|
|
2060
|
+
|
|
2061
|
+
Use \`list_tasks\` to see available tasks.`;
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
function createAsyncAgentTools(manager, client) {
|
|
2065
|
+
return {
|
|
2066
|
+
delegate_task: createDelegateTaskTool(manager, client),
|
|
2067
|
+
get_task_result: createGetTaskResultTool(manager),
|
|
2068
|
+
list_tasks: createListTasksTool(manager),
|
|
2069
|
+
cancel_task: createCancelTaskTool(manager)
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
775
2072
|
|
|
776
2073
|
// src/utils/common.ts
|
|
777
2074
|
function detectSlashCommand(text) {
|
|
@@ -779,8 +2076,129 @@ function detectSlashCommand(text) {
|
|
|
779
2076
|
if (!match) return null;
|
|
780
2077
|
return { command: match[1], args: match[2] || "" };
|
|
781
2078
|
}
|
|
2079
|
+
function formatTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
2080
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
2081
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
2082
|
+
}
|
|
2083
|
+
function formatElapsedTime(startMs, endMs = Date.now()) {
|
|
2084
|
+
const elapsed = endMs - startMs;
|
|
2085
|
+
if (elapsed < 0) return "0s";
|
|
2086
|
+
const seconds = Math.floor(elapsed / 1e3) % 60;
|
|
2087
|
+
const minutes = Math.floor(elapsed / (1e3 * 60)) % 60;
|
|
2088
|
+
const hours = Math.floor(elapsed / (1e3 * 60 * 60));
|
|
2089
|
+
const parts = [];
|
|
2090
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
2091
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
2092
|
+
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
|
|
2093
|
+
return parts.join(" ");
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/utils/sanity.ts
|
|
2097
|
+
function checkOutputSanity(text) {
|
|
2098
|
+
if (!text || text.length < 50) {
|
|
2099
|
+
return { isHealthy: true, severity: "ok" };
|
|
2100
|
+
}
|
|
2101
|
+
if (/(.)\1{15,}/.test(text)) {
|
|
2102
|
+
return {
|
|
2103
|
+
isHealthy: false,
|
|
2104
|
+
reason: "Single character repetition detected",
|
|
2105
|
+
severity: "critical"
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
if (/(.{2,6})\1{8,}/.test(text)) {
|
|
2109
|
+
return {
|
|
2110
|
+
isHealthy: false,
|
|
2111
|
+
reason: "Pattern loop detected",
|
|
2112
|
+
severity: "critical"
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
if (text.length > 200) {
|
|
2116
|
+
const cleanText = text.replace(/\s/g, "");
|
|
2117
|
+
if (cleanText.length > 100) {
|
|
2118
|
+
const uniqueChars = new Set(cleanText).size;
|
|
2119
|
+
const ratio = uniqueChars / cleanText.length;
|
|
2120
|
+
if (ratio < 0.02) {
|
|
2121
|
+
return {
|
|
2122
|
+
isHealthy: false,
|
|
2123
|
+
reason: "Low information density",
|
|
2124
|
+
severity: "critical"
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
const boxChars = (text.match(/[\u2500-\u257f\u2580-\u259f\u2800-\u28ff]/g) || []).length;
|
|
2130
|
+
if (boxChars > 100 && boxChars / text.length > 0.3) {
|
|
2131
|
+
return {
|
|
2132
|
+
isHealthy: false,
|
|
2133
|
+
reason: "Visual gibberish detected",
|
|
2134
|
+
severity: "critical"
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
const lines = text.split("\n").filter((l) => l.trim().length > 10);
|
|
2138
|
+
if (lines.length > 10) {
|
|
2139
|
+
const lineSet = new Set(lines);
|
|
2140
|
+
if (lineSet.size < lines.length * 0.2) {
|
|
2141
|
+
return {
|
|
2142
|
+
isHealthy: false,
|
|
2143
|
+
reason: "Excessive line repetition",
|
|
2144
|
+
severity: "warning"
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
const cjkChars = (text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length;
|
|
2149
|
+
if (cjkChars > 200) {
|
|
2150
|
+
const uniqueCjk = new Set(
|
|
2151
|
+
text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []
|
|
2152
|
+
).size;
|
|
2153
|
+
if (uniqueCjk < 10 && cjkChars / uniqueCjk > 20) {
|
|
2154
|
+
return {
|
|
2155
|
+
isHealthy: false,
|
|
2156
|
+
reason: "CJK character spam detected",
|
|
2157
|
+
severity: "critical"
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
return { isHealthy: true, severity: "ok" };
|
|
2162
|
+
}
|
|
2163
|
+
var RECOVERY_PROMPT = `<anomaly_recovery>
|
|
2164
|
+
\u26A0\uFE0F SYSTEM NOTICE: Previous output was malformed (gibberish/loop detected).
|
|
2165
|
+
|
|
2166
|
+
<recovery_protocol>
|
|
2167
|
+
1. DISCARD the corrupted output completely - do not reference it
|
|
2168
|
+
2. RECALL the original mission objective
|
|
2169
|
+
3. IDENTIFY the last confirmed successful step
|
|
2170
|
+
4. RESTART with a simpler, more focused approach
|
|
2171
|
+
</recovery_protocol>
|
|
2172
|
+
|
|
2173
|
+
<instructions>
|
|
2174
|
+
- If a sub-agent produced bad output: try a different agent or simpler task
|
|
2175
|
+
- If stuck in a loop: break down the task into smaller pieces
|
|
2176
|
+
- If context seems corrupted: call recorder to restore context
|
|
2177
|
+
- THINK in English for maximum stability
|
|
2178
|
+
</instructions>
|
|
2179
|
+
|
|
2180
|
+
What was the original task? Proceed from the last known good state.
|
|
2181
|
+
</anomaly_recovery>`;
|
|
2182
|
+
var ESCALATION_PROMPT = `<critical_anomaly>
|
|
2183
|
+
\u{1F6A8} CRITICAL: Multiple consecutive malformed outputs detected.
|
|
2184
|
+
|
|
2185
|
+
<emergency_protocol>
|
|
2186
|
+
1. STOP current execution path immediately
|
|
2187
|
+
2. DO NOT continue with the same approach - it is failing
|
|
2188
|
+
3. CALL architect for a completely new strategy
|
|
2189
|
+
4. If architect also fails: report status to user and await guidance
|
|
2190
|
+
</emergency_protocol>
|
|
2191
|
+
|
|
2192
|
+
<diagnosis>
|
|
2193
|
+
The current approach is producing corrupted output.
|
|
2194
|
+
This may indicate: context overload, model instability, or task complexity.
|
|
2195
|
+
</diagnosis>
|
|
2196
|
+
|
|
2197
|
+
Request a fresh plan from architect with reduced scope.
|
|
2198
|
+
</critical_anomaly>`;
|
|
782
2199
|
|
|
783
2200
|
// src/index.ts
|
|
2201
|
+
var PLUGIN_VERSION = "0.2.4";
|
|
784
2202
|
var DEFAULT_MAX_STEPS = 500;
|
|
785
2203
|
var TASK_COMMAND_MAX_STEPS = 1e3;
|
|
786
2204
|
var AGENT_EMOJI2 = {
|
|
@@ -808,14 +2226,32 @@ Execute it NOW.
|
|
|
808
2226
|
</auto_continue>`;
|
|
809
2227
|
var OrchestratorPlugin = async (input) => {
|
|
810
2228
|
const { directory, client } = input;
|
|
2229
|
+
console.log(`[orchestrator] v${PLUGIN_VERSION} loaded`);
|
|
811
2230
|
const sessions = /* @__PURE__ */ new Map();
|
|
2231
|
+
const parallelAgentManager2 = ParallelAgentManager.getInstance(client, directory);
|
|
2232
|
+
const asyncAgentTools = createAsyncAgentTools(parallelAgentManager2, client);
|
|
812
2233
|
return {
|
|
2234
|
+
// -----------------------------------------------------------------
|
|
2235
|
+
// Tools we expose to the LLM
|
|
2236
|
+
// -----------------------------------------------------------------
|
|
813
2237
|
tool: {
|
|
814
2238
|
call_agent: callAgentTool,
|
|
815
2239
|
slashcommand: createSlashcommandTool(),
|
|
816
2240
|
grep_search: grepSearchTool(directory),
|
|
817
|
-
glob_search: globSearchTool(directory)
|
|
2241
|
+
glob_search: globSearchTool(directory),
|
|
2242
|
+
mgrep: mgrepTool(directory),
|
|
2243
|
+
// Multi-pattern grep (parallel, Rust-powered)
|
|
2244
|
+
// Background task tools - run shell commands asynchronously
|
|
2245
|
+
run_background: runBackgroundTool,
|
|
2246
|
+
check_background: checkBackgroundTool,
|
|
2247
|
+
list_background: listBackgroundTool,
|
|
2248
|
+
kill_background: killBackgroundTool,
|
|
2249
|
+
// Async agent tools - spawn agents in parallel sessions
|
|
2250
|
+
...asyncAgentTools
|
|
818
2251
|
},
|
|
2252
|
+
// -----------------------------------------------------------------
|
|
2253
|
+
// Config hook - registers our commands and agents with OpenCode
|
|
2254
|
+
// -----------------------------------------------------------------
|
|
819
2255
|
config: async (config) => {
|
|
820
2256
|
const existingCommands = config.command ?? {};
|
|
821
2257
|
const existingAgents = config.agent ?? {};
|
|
@@ -837,6 +2273,10 @@ var OrchestratorPlugin = async (input) => {
|
|
|
837
2273
|
config.command = { ...orchestratorCommands, ...existingCommands };
|
|
838
2274
|
config.agent = { ...orchestratorAgents, ...existingAgents };
|
|
839
2275
|
},
|
|
2276
|
+
// -----------------------------------------------------------------
|
|
2277
|
+
// chat.message hook - runs when user sends a message
|
|
2278
|
+
// This is where we intercept commands and set up sessions
|
|
2279
|
+
// -----------------------------------------------------------------
|
|
840
2280
|
"chat.message": async (msgInput, msgOutput) => {
|
|
841
2281
|
const parts = msgOutput.parts;
|
|
842
2282
|
const textPartIndex = parts.findIndex((p) => p.type === "text" && p.text);
|
|
@@ -846,18 +2286,22 @@ var OrchestratorPlugin = async (input) => {
|
|
|
846
2286
|
const sessionID = msgInput.sessionID;
|
|
847
2287
|
const agentName = (msgInput.agent || "").toLowerCase();
|
|
848
2288
|
if (agentName === "commander" && !sessions.has(sessionID)) {
|
|
2289
|
+
const now = Date.now();
|
|
849
2290
|
sessions.set(sessionID, {
|
|
850
2291
|
active: true,
|
|
851
2292
|
step: 0,
|
|
852
2293
|
maxSteps: DEFAULT_MAX_STEPS,
|
|
853
|
-
timestamp:
|
|
2294
|
+
timestamp: now,
|
|
2295
|
+
startTime: now,
|
|
2296
|
+
lastStepTime: now
|
|
854
2297
|
});
|
|
855
2298
|
state.missionActive = true;
|
|
856
2299
|
state.sessions.set(sessionID, {
|
|
857
2300
|
enabled: true,
|
|
858
2301
|
iterations: 0,
|
|
859
2302
|
taskRetries: /* @__PURE__ */ new Map(),
|
|
860
|
-
currentTask: ""
|
|
2303
|
+
currentTask: "",
|
|
2304
|
+
anomalyCount: 0
|
|
861
2305
|
});
|
|
862
2306
|
if (!parsed) {
|
|
863
2307
|
const userMessage = originalText.trim();
|
|
@@ -870,18 +2314,22 @@ var OrchestratorPlugin = async (input) => {
|
|
|
870
2314
|
}
|
|
871
2315
|
}
|
|
872
2316
|
if (parsed?.command === "task") {
|
|
2317
|
+
const now = Date.now();
|
|
873
2318
|
sessions.set(sessionID, {
|
|
874
2319
|
active: true,
|
|
875
2320
|
step: 0,
|
|
876
2321
|
maxSteps: TASK_COMMAND_MAX_STEPS,
|
|
877
|
-
timestamp:
|
|
2322
|
+
timestamp: now,
|
|
2323
|
+
startTime: now,
|
|
2324
|
+
lastStepTime: now
|
|
878
2325
|
});
|
|
879
2326
|
state.missionActive = true;
|
|
880
2327
|
state.sessions.set(sessionID, {
|
|
881
2328
|
enabled: true,
|
|
882
2329
|
iterations: 0,
|
|
883
2330
|
taskRetries: /* @__PURE__ */ new Map(),
|
|
884
|
-
currentTask: ""
|
|
2331
|
+
currentTask: "",
|
|
2332
|
+
anomalyCount: 0
|
|
885
2333
|
});
|
|
886
2334
|
parts[textPartIndex].text = COMMANDS["task"].template.replace(
|
|
887
2335
|
/\$ARGUMENTS/g,
|
|
@@ -897,12 +2345,43 @@ var OrchestratorPlugin = async (input) => {
|
|
|
897
2345
|
}
|
|
898
2346
|
}
|
|
899
2347
|
},
|
|
2348
|
+
// -----------------------------------------------------------------
|
|
2349
|
+
// tool.execute.after hook - runs after any tool call completes
|
|
2350
|
+
// We use this to track progress and detect problems
|
|
2351
|
+
// -----------------------------------------------------------------
|
|
900
2352
|
"tool.execute.after": async (toolInput, toolOutput) => {
|
|
901
2353
|
const session = sessions.get(toolInput.sessionID);
|
|
902
2354
|
if (!session?.active) return;
|
|
2355
|
+
const now = Date.now();
|
|
2356
|
+
const stepDuration = formatElapsedTime(session.lastStepTime, now);
|
|
2357
|
+
const totalElapsed = formatElapsedTime(session.startTime, now);
|
|
903
2358
|
session.step++;
|
|
904
|
-
session.timestamp =
|
|
2359
|
+
session.timestamp = now;
|
|
2360
|
+
session.lastStepTime = now;
|
|
905
2361
|
const stateSession = state.sessions.get(toolInput.sessionID);
|
|
2362
|
+
if (toolInput.tool === "call_agent" && stateSession) {
|
|
2363
|
+
const sanityResult = checkOutputSanity(toolOutput.output);
|
|
2364
|
+
if (!sanityResult.isHealthy) {
|
|
2365
|
+
stateSession.anomalyCount = (stateSession.anomalyCount || 0) + 1;
|
|
2366
|
+
const agentName = toolInput.arguments?.agent || "unknown";
|
|
2367
|
+
toolOutput.output = `\u26A0\uFE0F [${agentName.toUpperCase()}] OUTPUT ANOMALY DETECTED
|
|
2368
|
+
|
|
2369
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
2370
|
+
\u26A0\uFE0F Gibberish/loop detected: ${sanityResult.reason}
|
|
2371
|
+
Anomaly count: ${stateSession.anomalyCount}
|
|
2372
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
2373
|
+
|
|
2374
|
+
` + (stateSession.anomalyCount >= 2 ? ESCALATION_PROMPT : RECOVERY_PROMPT);
|
|
2375
|
+
return;
|
|
2376
|
+
} else {
|
|
2377
|
+
if (stateSession.anomalyCount > 0) {
|
|
2378
|
+
stateSession.anomalyCount = 0;
|
|
2379
|
+
}
|
|
2380
|
+
if (toolOutput.output.length < 5e3) {
|
|
2381
|
+
stateSession.lastHealthyOutput = toolOutput.output.substring(0, 1e3);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
906
2385
|
if (toolInput.tool === "call_agent" && toolInput.arguments?.task && stateSession) {
|
|
907
2386
|
const taskIdMatch = toolInput.arguments.task.match(/\[(TASK-\d+)\]/i);
|
|
908
2387
|
if (taskIdMatch) {
|
|
@@ -973,16 +2452,54 @@ ${stateSession.graph.getTaskSummary()}`;
|
|
|
973
2452
|
\u{1F449} NEXT: ${readyTasks.map((t) => `[${t.id}]`).join(", ")}`;
|
|
974
2453
|
}
|
|
975
2454
|
}
|
|
2455
|
+
const currentTime = formatTimestamp();
|
|
976
2456
|
toolOutput.output += `
|
|
977
2457
|
|
|
978
|
-
[${session.step}/${session.maxSteps}
|
|
2458
|
+
\u23F1\uFE0F [${currentTime}] Step ${session.step}/${session.maxSteps} | This step: ${stepDuration} | Total: ${totalElapsed}`;
|
|
979
2459
|
},
|
|
2460
|
+
// -----------------------------------------------------------------
|
|
2461
|
+
// assistant.done hook - runs when the LLM finishes responding
|
|
2462
|
+
// This is the heart of the "relentless loop" - we keep pushing it
|
|
2463
|
+
// to continue until we see MISSION COMPLETE or hit the limit
|
|
2464
|
+
// -----------------------------------------------------------------
|
|
980
2465
|
"assistant.done": async (assistantInput, assistantOutput) => {
|
|
981
2466
|
const sessionID = assistantInput.sessionID;
|
|
982
2467
|
const session = sessions.get(sessionID);
|
|
983
2468
|
if (!session?.active) return;
|
|
984
2469
|
const parts = assistantOutput.parts;
|
|
985
2470
|
const textContent = parts?.filter((p) => p.type === "text" || p.type === "reasoning").map((p) => p.text || "").join("\n") || "";
|
|
2471
|
+
const stateSession = state.sessions.get(sessionID);
|
|
2472
|
+
const sanityResult = checkOutputSanity(textContent);
|
|
2473
|
+
if (!sanityResult.isHealthy && stateSession) {
|
|
2474
|
+
stateSession.anomalyCount = (stateSession.anomalyCount || 0) + 1;
|
|
2475
|
+
session.step++;
|
|
2476
|
+
session.timestamp = Date.now();
|
|
2477
|
+
const recoveryText = stateSession.anomalyCount >= 2 ? ESCALATION_PROMPT : RECOVERY_PROMPT;
|
|
2478
|
+
try {
|
|
2479
|
+
if (client?.session?.prompt) {
|
|
2480
|
+
await client.session.prompt({
|
|
2481
|
+
path: { id: sessionID },
|
|
2482
|
+
body: {
|
|
2483
|
+
parts: [{
|
|
2484
|
+
type: "text",
|
|
2485
|
+
text: `\u26A0\uFE0F ANOMALY #${stateSession.anomalyCount}: ${sanityResult.reason}
|
|
2486
|
+
|
|
2487
|
+
` + recoveryText + `
|
|
2488
|
+
|
|
2489
|
+
[Recovery Step ${session.step}/${session.maxSteps}]`
|
|
2490
|
+
}]
|
|
2491
|
+
}
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
} catch {
|
|
2495
|
+
session.active = false;
|
|
2496
|
+
state.missionActive = false;
|
|
2497
|
+
}
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
if (stateSession && stateSession.anomalyCount > 0) {
|
|
2501
|
+
stateSession.anomalyCount = 0;
|
|
2502
|
+
}
|
|
986
2503
|
if (textContent.includes("\u2705 MISSION COMPLETE") || textContent.includes("MISSION COMPLETE")) {
|
|
987
2504
|
session.active = false;
|
|
988
2505
|
state.missionActive = false;
|
|
@@ -997,8 +2514,13 @@ ${stateSession.graph.getTaskSummary()}`;
|
|
|
997
2514
|
state.sessions.delete(sessionID);
|
|
998
2515
|
return;
|
|
999
2516
|
}
|
|
2517
|
+
const now = Date.now();
|
|
2518
|
+
const stepDuration = formatElapsedTime(session.lastStepTime, now);
|
|
2519
|
+
const totalElapsed = formatElapsedTime(session.startTime, now);
|
|
1000
2520
|
session.step++;
|
|
1001
|
-
session.timestamp =
|
|
2521
|
+
session.timestamp = now;
|
|
2522
|
+
session.lastStepTime = now;
|
|
2523
|
+
const currentTime = formatTimestamp();
|
|
1002
2524
|
if (session.step >= session.maxSteps) {
|
|
1003
2525
|
session.active = false;
|
|
1004
2526
|
state.missionActive = false;
|
|
@@ -1013,7 +2535,7 @@ ${stateSession.graph.getTaskSummary()}`;
|
|
|
1013
2535
|
type: "text",
|
|
1014
2536
|
text: CONTINUE_INSTRUCTION + `
|
|
1015
2537
|
|
|
1016
|
-
[Step ${session.step}/${session.maxSteps}
|
|
2538
|
+
\u23F1\uFE0F [${currentTime}] Step ${session.step}/${session.maxSteps} | This step: ${stepDuration} | Total: ${totalElapsed}`
|
|
1017
2539
|
}]
|
|
1018
2540
|
}
|
|
1019
2541
|
});
|
|
@@ -1033,6 +2555,9 @@ ${stateSession.graph.getTaskSummary()}`;
|
|
|
1033
2555
|
}
|
|
1034
2556
|
}
|
|
1035
2557
|
},
|
|
2558
|
+
// -----------------------------------------------------------------
|
|
2559
|
+
// Event handler - cleans up when sessions are deleted
|
|
2560
|
+
// -----------------------------------------------------------------
|
|
1036
2561
|
handler: async ({ event }) => {
|
|
1037
2562
|
if (event.type === "session.deleted") {
|
|
1038
2563
|
const props = event.properties;
|