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/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: THINK and REASON in English for maximum stability. Report final summary in Korean.
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: Date.now()
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: Date.now()
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 = Date.now();
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 = Date.now();
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
  });