opencode-orchestrator 0.2.3 → 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/index.d.ts +62 -0
- package/dist/index.js +1350 -8
- 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/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,6 +2076,22 @@ 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
|
+
}
|
|
782
2095
|
|
|
783
2096
|
// src/utils/sanity.ts
|
|
784
2097
|
function checkOutputSanity(text) {
|
|
@@ -885,6 +2198,7 @@ Request a fresh plan from architect with reduced scope.
|
|
|
885
2198
|
</critical_anomaly>`;
|
|
886
2199
|
|
|
887
2200
|
// src/index.ts
|
|
2201
|
+
var PLUGIN_VERSION = "0.2.4";
|
|
888
2202
|
var DEFAULT_MAX_STEPS = 500;
|
|
889
2203
|
var TASK_COMMAND_MAX_STEPS = 1e3;
|
|
890
2204
|
var AGENT_EMOJI2 = {
|
|
@@ -912,7 +2226,10 @@ Execute it NOW.
|
|
|
912
2226
|
</auto_continue>`;
|
|
913
2227
|
var OrchestratorPlugin = async (input) => {
|
|
914
2228
|
const { directory, client } = input;
|
|
2229
|
+
console.log(`[orchestrator] v${PLUGIN_VERSION} loaded`);
|
|
915
2230
|
const sessions = /* @__PURE__ */ new Map();
|
|
2231
|
+
const parallelAgentManager2 = ParallelAgentManager.getInstance(client, directory);
|
|
2232
|
+
const asyncAgentTools = createAsyncAgentTools(parallelAgentManager2, client);
|
|
916
2233
|
return {
|
|
917
2234
|
// -----------------------------------------------------------------
|
|
918
2235
|
// Tools we expose to the LLM
|
|
@@ -921,7 +2238,16 @@ var OrchestratorPlugin = async (input) => {
|
|
|
921
2238
|
call_agent: callAgentTool,
|
|
922
2239
|
slashcommand: createSlashcommandTool(),
|
|
923
2240
|
grep_search: grepSearchTool(directory),
|
|
924
|
-
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
|
|
925
2251
|
},
|
|
926
2252
|
// -----------------------------------------------------------------
|
|
927
2253
|
// Config hook - registers our commands and agents with OpenCode
|
|
@@ -960,11 +2286,14 @@ var OrchestratorPlugin = async (input) => {
|
|
|
960
2286
|
const sessionID = msgInput.sessionID;
|
|
961
2287
|
const agentName = (msgInput.agent || "").toLowerCase();
|
|
962
2288
|
if (agentName === "commander" && !sessions.has(sessionID)) {
|
|
2289
|
+
const now = Date.now();
|
|
963
2290
|
sessions.set(sessionID, {
|
|
964
2291
|
active: true,
|
|
965
2292
|
step: 0,
|
|
966
2293
|
maxSteps: DEFAULT_MAX_STEPS,
|
|
967
|
-
timestamp:
|
|
2294
|
+
timestamp: now,
|
|
2295
|
+
startTime: now,
|
|
2296
|
+
lastStepTime: now
|
|
968
2297
|
});
|
|
969
2298
|
state.missionActive = true;
|
|
970
2299
|
state.sessions.set(sessionID, {
|
|
@@ -985,11 +2314,14 @@ var OrchestratorPlugin = async (input) => {
|
|
|
985
2314
|
}
|
|
986
2315
|
}
|
|
987
2316
|
if (parsed?.command === "task") {
|
|
2317
|
+
const now = Date.now();
|
|
988
2318
|
sessions.set(sessionID, {
|
|
989
2319
|
active: true,
|
|
990
2320
|
step: 0,
|
|
991
2321
|
maxSteps: TASK_COMMAND_MAX_STEPS,
|
|
992
|
-
timestamp:
|
|
2322
|
+
timestamp: now,
|
|
2323
|
+
startTime: now,
|
|
2324
|
+
lastStepTime: now
|
|
993
2325
|
});
|
|
994
2326
|
state.missionActive = true;
|
|
995
2327
|
state.sessions.set(sessionID, {
|
|
@@ -1020,8 +2352,12 @@ var OrchestratorPlugin = async (input) => {
|
|
|
1020
2352
|
"tool.execute.after": async (toolInput, toolOutput) => {
|
|
1021
2353
|
const session = sessions.get(toolInput.sessionID);
|
|
1022
2354
|
if (!session?.active) return;
|
|
2355
|
+
const now = Date.now();
|
|
2356
|
+
const stepDuration = formatElapsedTime(session.lastStepTime, now);
|
|
2357
|
+
const totalElapsed = formatElapsedTime(session.startTime, now);
|
|
1023
2358
|
session.step++;
|
|
1024
|
-
session.timestamp =
|
|
2359
|
+
session.timestamp = now;
|
|
2360
|
+
session.lastStepTime = now;
|
|
1025
2361
|
const stateSession = state.sessions.get(toolInput.sessionID);
|
|
1026
2362
|
if (toolInput.tool === "call_agent" && stateSession) {
|
|
1027
2363
|
const sanityResult = checkOutputSanity(toolOutput.output);
|
|
@@ -1116,9 +2452,10 @@ ${stateSession.graph.getTaskSummary()}`;
|
|
|
1116
2452
|
\u{1F449} NEXT: ${readyTasks.map((t) => `[${t.id}]`).join(", ")}`;
|
|
1117
2453
|
}
|
|
1118
2454
|
}
|
|
2455
|
+
const currentTime = formatTimestamp();
|
|
1119
2456
|
toolOutput.output += `
|
|
1120
2457
|
|
|
1121
|
-
[${session.step}/${session.maxSteps}
|
|
2458
|
+
\u23F1\uFE0F [${currentTime}] Step ${session.step}/${session.maxSteps} | This step: ${stepDuration} | Total: ${totalElapsed}`;
|
|
1122
2459
|
},
|
|
1123
2460
|
// -----------------------------------------------------------------
|
|
1124
2461
|
// assistant.done hook - runs when the LLM finishes responding
|
|
@@ -1177,8 +2514,13 @@ ${stateSession.graph.getTaskSummary()}`;
|
|
|
1177
2514
|
state.sessions.delete(sessionID);
|
|
1178
2515
|
return;
|
|
1179
2516
|
}
|
|
2517
|
+
const now = Date.now();
|
|
2518
|
+
const stepDuration = formatElapsedTime(session.lastStepTime, now);
|
|
2519
|
+
const totalElapsed = formatElapsedTime(session.startTime, now);
|
|
1180
2520
|
session.step++;
|
|
1181
|
-
session.timestamp =
|
|
2521
|
+
session.timestamp = now;
|
|
2522
|
+
session.lastStepTime = now;
|
|
2523
|
+
const currentTime = formatTimestamp();
|
|
1182
2524
|
if (session.step >= session.maxSteps) {
|
|
1183
2525
|
session.active = false;
|
|
1184
2526
|
state.missionActive = false;
|
|
@@ -1193,7 +2535,7 @@ ${stateSession.graph.getTaskSummary()}`;
|
|
|
1193
2535
|
type: "text",
|
|
1194
2536
|
text: CONTINUE_INSTRUCTION + `
|
|
1195
2537
|
|
|
1196
|
-
[Step ${session.step}/${session.maxSteps}
|
|
2538
|
+
\u23F1\uFE0F [${currentTime}] Step ${session.step}/${session.maxSteps} | This step: ${stepDuration} | Total: ${totalElapsed}`
|
|
1197
2539
|
}]
|
|
1198
2540
|
}
|
|
1199
2541
|
});
|