opencode-orchestrator 0.4.10 → 0.4.13
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 +2 -154
- package/dist/core/background.d.ts +33 -0
- package/dist/index.d.ts +10 -8
- package/dist/index.js +537 -37
- package/dist/tools/background.d.ts +16 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,17 +28,15 @@ A **5-agent autonomous architecture** designed to solve complex engineering task
|
|
|
28
28
|
|
|
29
29
|
**Core Philosophy**: Intelligence is a resource. We orchestrate that resource through **Phase-based Workflows** and **Mandatory Environment Scans** to ensure code always fits the project's infrastructure.
|
|
30
30
|
|
|
31
|
-
> 🦀 **Powered by Rust** — Background tasks and parallel searches run on native Rust binaries for maximum performance.
|
|
32
|
-
|
|
33
31
|
### Key Features
|
|
34
|
-
- **🎯 Autonomous Loop** — Commander runs relentlessly until
|
|
32
|
+
- **🎯 Autonomous Loop** — Commander runs relentlessly until mission is complete.
|
|
35
33
|
- **🔍 Environment Scan** — Mandatory analysis of Infra (Docker/OS), Stack, and Domain before any code change.
|
|
36
34
|
- **🔨 Smart Implementation** — Builder matches existing codebase patterns exactly.
|
|
37
35
|
- **🛡️ Rigorous Audit** — Inspector proves success with environment-specific evidence (Builds/Tests/Logs).
|
|
38
36
|
- **💾 Persistent Context** — Recorder saves session state to disk, enabling resume at any time.
|
|
39
37
|
- **🏗️ Parallel Agents** — Delegated agent execution (`delegate_task`) with sync/async modes.
|
|
40
38
|
- **⏳ Background Tasks** — Run long commands (builds, tests) in background and check results later.
|
|
41
|
-
- **🔎 mgrep** — Multi-pattern parallel search
|
|
39
|
+
- **🔎 mgrep** — Multi-pattern parallel search for fast codebase analysis.
|
|
42
40
|
|
|
43
41
|
---
|
|
44
42
|
|
|
@@ -95,158 +93,8 @@ Trigger parallel agent execution with prompts like:
|
|
|
95
93
|
|
|
96
94
|
Commander will automatically use `delegate_task` with `background: true` for independent tasks.
|
|
97
95
|
|
|
98
|
-
**Parallel Execution UI**
|
|
99
|
-
|
|
100
|
-
When tasks run in parallel, you'll see detailed progress in OpenCode:
|
|
101
|
-
|
|
102
|
-
```
|
|
103
|
-
## 🚀 BACKGROUND TASK SPAWNED
|
|
104
|
-
|
|
105
|
-
**Task Details**
|
|
106
|
-
- **ID**: `task_a1b2c3d4`
|
|
107
|
-
- **Agent**: builder
|
|
108
|
-
- **Description**: Implement authentication system
|
|
109
|
-
- **Status**: ⏳ Running in background (non-blocking)
|
|
110
|
-
|
|
111
|
-
**Active Tasks**
|
|
112
|
-
- Running: 2
|
|
113
|
-
- Pending: 1
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
**Monitoring Commands**
|
|
118
|
-
|
|
119
|
-
Check progress anytime:
|
|
120
|
-
- `list_tasks()` - View all parallel tasks
|
|
121
|
-
- `get_task_result({ taskId: "task_a1b2c3d4" })` - Get latest result
|
|
122
|
-
- `cancel_task({ taskId: "task_a1b2c3d4" })` - Stop this task
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
|
|
126
|
-
✓ System will notify when ALL tasks complete. You can continue working!
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
**Terminal Logs**
|
|
130
|
-
|
|
131
96
|
Monitor parallel tasks in terminal:
|
|
132
97
|
|
|
133
|
-
```
|
|
134
|
-
[parallel] 🚀 SPAWNED task_a1b2c3d4 → builder: Implement authentication
|
|
135
|
-
[parallel] 🚀 SPAWNED task_e5f6g7h8 → inspector: Review module
|
|
136
|
-
[parallel] ✅ COMPLETED task_e5f6g7h8 → inspector: Review module (45s)
|
|
137
|
-
[parallel] 🗑️ CLEANED task_e5f6g7h8 (session deleted)
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
**All Tasks Complete**
|
|
141
|
-
|
|
142
|
-
When all parallel tasks finish, you'll see:
|
|
143
|
-
|
|
144
|
-
```
|
|
145
|
-
**All Parallel Tasks Complete**
|
|
146
|
-
|
|
147
|
-
✅ `task_a1b2c3d4` (1m 30s): Implement authentication
|
|
148
|
-
✅ `task_e5f6g7h8` (45s): Review module
|
|
149
|
-
|
|
150
|
-
---
|
|
151
|
-
|
|
152
|
-
**Retrieval Options**
|
|
153
|
-
|
|
154
|
-
Use `get_task_result({ taskId: "task_xxx" })` to retrieve full results.
|
|
155
|
-
|
|
156
|
-
---
|
|
157
|
-
|
|
158
|
-
**Task Summary**
|
|
159
|
-
|
|
160
|
-
Total Tasks: 2
|
|
161
|
-
Status: All Complete
|
|
162
|
-
Mode: Background (non-blocking)
|
|
163
|
-
```
|
|
164
|
-
"Build and test in parallel"
|
|
165
|
-
"Implement feature X while reviewing module Y"
|
|
166
|
-
"Run linting, tests, and build at the same time"
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
Commander will automatically use `delegate_task` with `background: true` for independent tasks.
|
|
170
|
-
|
|
171
|
-
**Parallel Execution UI**
|
|
172
|
-
|
|
173
|
-
When tasks run in parallel, you'll see detailed progress in OpenCode:
|
|
174
|
-
|
|
175
|
-
```
|
|
176
|
-
╔════════════════════════════════════════════════════════════╗
|
|
177
|
-
║ 🚀 BACKGROUND TASK SPAWNED ║
|
|
178
|
-
╠═════════════════════════════════════════════════════════════╣
|
|
179
|
-
║ Task ID: task_a1b2c3d4 ║
|
|
180
|
-
║ Agent: builder ║
|
|
181
|
-
║ Description: Implement authentication system ║
|
|
182
|
-
║ Status: ⏳ RUNNING (background) ║
|
|
183
|
-
╠═════════════════════════════════════════════════════════════╣
|
|
184
|
-
║ Running: 2 │ Pending: 1 ║
|
|
185
|
-
╚══════════════════════════════════════════════════════════════╝
|
|
186
|
-
|
|
187
|
-
---
|
|
188
|
-
|
|
189
|
-
**Parallel Execution Started**
|
|
190
|
-
|
|
191
|
-
- 📌 Task ID: `task_a1b2c3d4`
|
|
192
|
-
- 🤖 Agent: builder
|
|
193
|
-
- 📝 Description: Implement authentication system
|
|
194
|
-
- ⏳ Status: Running in background (non-blocking)
|
|
195
|
-
- 🔄 Active Tasks: 2 running, 1 pending
|
|
196
|
-
|
|
197
|
-
**Monitoring**
|
|
198
|
-
|
|
199
|
-
Check progress anytime with:
|
|
200
|
-
- `list_tasks()` - View all parallel tasks
|
|
201
|
-
- `get_task_result({ taskId: "task_a1b2c3d4" })` - Get latest result
|
|
202
|
-
- `cancel_task({ taskId: "task_a1b2c3d4" })` - Stop this task
|
|
203
|
-
|
|
204
|
-
System will notify when ALL tasks complete. You can continue working!
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
**Terminal Logs**
|
|
208
|
-
|
|
209
|
-
Monitor parallel tasks in terminal:
|
|
210
|
-
|
|
211
|
-
```
|
|
212
|
-
[parallel] 🚀 SPAWNED task_a1b2c3d4 → builder: Implement authentication
|
|
213
|
-
[parallel] 🚀 SPAWNED task_e5f6g7h8 → inspector: Review module
|
|
214
|
-
[parallel] ✅ COMPLETED task_e5f6g7h8 → inspector: Review module (45s)
|
|
215
|
-
[parallel] 🗑️ CLEANED task_e5f6g7h8 (session deleted)
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
**All Tasks Complete**
|
|
219
|
-
|
|
220
|
-
When all parallel tasks finish, you'll see:
|
|
221
|
-
|
|
222
|
-
```
|
|
223
|
-
**All Parallel Tasks Complete**
|
|
224
|
-
|
|
225
|
-
✅ `task_a1b2c3d4` (1m 30s): Implement authentication
|
|
226
|
-
✅ `task_e5f6g7h8` (45s): Review module
|
|
227
|
-
|
|
228
|
-
---
|
|
229
|
-
|
|
230
|
-
**Retrieval Options**
|
|
231
|
-
|
|
232
|
-
Use `get_task_result({ taskId: "task_xxx" })` to retrieve full results.
|
|
233
|
-
|
|
234
|
-
---
|
|
235
|
-
|
|
236
|
-
**Task Summary**
|
|
237
|
-
|
|
238
|
-
Total Tasks: 2
|
|
239
|
-
Status: All Complete
|
|
240
|
-
Mode: Background (non-blocking)
|
|
241
|
-
```
|
|
242
|
-
"Build and test in parallel"
|
|
243
|
-
"Implement feature X while reviewing module Y"
|
|
244
|
-
"Run linting, tests, and build at the same time"
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
Commander will automatically use `delegate_task` with `background: true` for independent tasks.
|
|
248
|
-
|
|
249
|
-
Monitor parallel tasks in the terminal:
|
|
250
98
|
```
|
|
251
99
|
[parallel] 🚀 SPAWNED task_a1b2 → builder: Implement feature X
|
|
252
100
|
[parallel] 🚀 SPAWNED task_c3d4 → inspector: Review module Y
|
|
@@ -8,6 +8,7 @@ import { ChildProcess } from "child_process";
|
|
|
8
8
|
export type TaskStatus = "pending" | "running" | "done" | "error" | "timeout";
|
|
9
9
|
export interface BackgroundTask {
|
|
10
10
|
id: string;
|
|
11
|
+
sessionID?: string;
|
|
11
12
|
command: string;
|
|
12
13
|
args: string[];
|
|
13
14
|
cwd: string;
|
|
@@ -26,17 +27,45 @@ export interface RunBackgroundOptions {
|
|
|
26
27
|
cwd?: string;
|
|
27
28
|
timeout?: number;
|
|
28
29
|
label?: string;
|
|
30
|
+
sessionID?: string;
|
|
29
31
|
}
|
|
30
32
|
declare class BackgroundTaskManager {
|
|
31
33
|
private static _instance;
|
|
32
34
|
private tasks;
|
|
33
35
|
private debugMode;
|
|
36
|
+
private storageDir;
|
|
37
|
+
private storageFile;
|
|
38
|
+
private monitoringInterval?;
|
|
34
39
|
private constructor();
|
|
35
40
|
static get instance(): BackgroundTaskManager;
|
|
36
41
|
/**
|
|
37
42
|
* Generate a unique task ID in the format job_xxxxxxxx
|
|
38
43
|
*/
|
|
39
44
|
private generateId;
|
|
45
|
+
/**
|
|
46
|
+
* Ensure storage directory exists
|
|
47
|
+
*/
|
|
48
|
+
private ensureStorageDir;
|
|
49
|
+
/**
|
|
50
|
+
* Load tasks from disk on startup
|
|
51
|
+
*/
|
|
52
|
+
private loadFromDisk;
|
|
53
|
+
/**
|
|
54
|
+
* Save tasks to disk
|
|
55
|
+
*/
|
|
56
|
+
private saveToDisk;
|
|
57
|
+
/**
|
|
58
|
+
* Start periodic monitoring of running processes
|
|
59
|
+
*/
|
|
60
|
+
private startMonitoring;
|
|
61
|
+
/**
|
|
62
|
+
* Stop monitoring
|
|
63
|
+
*/
|
|
64
|
+
private stopMonitoring;
|
|
65
|
+
/**
|
|
66
|
+
* Monitor running processes and detect zombie processes
|
|
67
|
+
*/
|
|
68
|
+
private monitorRunningProcesses;
|
|
40
69
|
/**
|
|
41
70
|
* Debug logging helper
|
|
42
71
|
*/
|
|
@@ -57,6 +86,10 @@ declare class BackgroundTaskManager {
|
|
|
57
86
|
* Get tasks by status
|
|
58
87
|
*/
|
|
59
88
|
getByStatus(status: TaskStatus): BackgroundTask[];
|
|
89
|
+
/**
|
|
90
|
+
* Clean up tasks by session ID
|
|
91
|
+
*/
|
|
92
|
+
cleanupBySession(sessionID: string): number;
|
|
60
93
|
/**
|
|
61
94
|
* Clear completed/failed tasks
|
|
62
95
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -82,23 +82,25 @@ declare const OrchestratorPlugin: (input: PluginInput) => Promise<{
|
|
|
82
82
|
cwd: import("zod").ZodOptional<import("zod").ZodString>;
|
|
83
83
|
timeout: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
84
84
|
label: import("zod").ZodOptional<import("zod").ZodString>;
|
|
85
|
+
sessionID: import("zod").ZodOptional<import("zod").ZodString>;
|
|
85
86
|
};
|
|
86
87
|
execute(args: {
|
|
87
88
|
command: string;
|
|
88
89
|
cwd?: string | undefined;
|
|
89
90
|
timeout?: number | undefined;
|
|
90
91
|
label?: string | undefined;
|
|
92
|
+
sessionID?: string | undefined;
|
|
91
93
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
92
94
|
};
|
|
93
95
|
check_background: {
|
|
94
96
|
description: string;
|
|
95
97
|
args: {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
taskId: import("zod").ZodString;
|
|
99
|
+
tailLines: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
98
100
|
};
|
|
99
101
|
execute(args: {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
taskId: string;
|
|
103
|
+
tailLines?: number | undefined;
|
|
102
104
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
103
105
|
};
|
|
104
106
|
list_background: {
|
|
@@ -106,22 +108,22 @@ declare const OrchestratorPlugin: (input: PluginInput) => Promise<{
|
|
|
106
108
|
args: {
|
|
107
109
|
status: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
108
110
|
running: "running";
|
|
109
|
-
all: "all";
|
|
110
111
|
done: "done";
|
|
111
112
|
error: "error";
|
|
113
|
+
all: "all";
|
|
112
114
|
}>>;
|
|
113
115
|
};
|
|
114
116
|
execute(args: {
|
|
115
|
-
status?: "running" | "
|
|
117
|
+
status?: "running" | "done" | "error" | "all" | undefined;
|
|
116
118
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
117
119
|
};
|
|
118
120
|
kill_background: {
|
|
119
121
|
description: string;
|
|
120
122
|
args: {
|
|
121
|
-
|
|
123
|
+
taskId: import("zod").ZodString;
|
|
122
124
|
};
|
|
123
125
|
execute(args: {
|
|
124
|
-
|
|
126
|
+
taskId: string;
|
|
125
127
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
126
128
|
};
|
|
127
129
|
};
|
package/dist/index.js
CHANGED
|
@@ -99,8 +99,8 @@ PREFER background=true (PARALLEL):
|
|
|
99
99
|
EXAMPLE - PARALLEL:
|
|
100
100
|
\`\`\`
|
|
101
101
|
// Multiple tasks in parallel
|
|
102
|
-
delegate_task({ agent: "
|
|
103
|
-
delegate_task({ agent: "
|
|
102
|
+
delegate_task({ agent: "${AGENT_NAMES.BUILDER}", description: "Implement X", prompt: "...", background: true })
|
|
103
|
+
delegate_task({ agent: "${AGENT_NAMES.INSPECTOR}", description: "Review Y", prompt: "...", background: true })
|
|
104
104
|
|
|
105
105
|
// Continue other work (don't wait!)
|
|
106
106
|
|
|
@@ -111,7 +111,7 @@ get_task_result({ taskId: "task_xxx" })
|
|
|
111
111
|
EXAMPLE - SYNC (rare):
|
|
112
112
|
\`\`\`
|
|
113
113
|
// Only when you absolutely need the result now
|
|
114
|
-
const result = delegate_task({ agent: "
|
|
114
|
+
const result = delegate_task({ agent: "${AGENT_NAMES.BUILDER}", ..., background: false })
|
|
115
115
|
// Result is immediately available
|
|
116
116
|
\`\`\`
|
|
117
117
|
</agent_calling>
|
|
@@ -138,10 +138,10 @@ During implementation:
|
|
|
138
138
|
PARALLEL EXECUTION TOOLS:
|
|
139
139
|
|
|
140
140
|
1. **spawn_agent** - Launch agents in parallel sessions
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
spawn_agent({ agent: "${AGENT_NAMES.BUILDER}", description: "Implement X", prompt: "..." })
|
|
142
|
+
spawn_agent({ agent: "${AGENT_NAMES.INSPECTOR}", description: "Review Y", prompt: "..." })
|
|
143
|
+
\u2192 Agents run concurrently, system notifies when ALL complete
|
|
144
|
+
\u2192 Use get_task_result({ taskId }) to retrieve results
|
|
145
145
|
|
|
146
146
|
2. **run_background** - Run shell commands asynchronously
|
|
147
147
|
run_background({ command: "npm run build" })
|
|
@@ -192,10 +192,10 @@ WORKFLOW:
|
|
|
192
192
|
<empty_responses>
|
|
193
193
|
| Agent Empty (or Gibberish) | Action |
|
|
194
194
|
|----------------------------|--------|
|
|
195
|
-
|
|
|
196
|
-
|
|
|
197
|
-
|
|
|
198
|
-
|
|
|
195
|
+
| ${AGENT_NAMES.RECORDER} | Fresh start. Proceed to survey. |
|
|
196
|
+
| ${AGENT_NAMES.ARCHITECT} | Try simpler plan yourself. |
|
|
197
|
+
| ${AGENT_NAMES.BUILDER} | Call inspector to diagnose. |
|
|
198
|
+
| ${AGENT_NAMES.INSPECTOR} | Retry with more context. |
|
|
199
199
|
</empty_responses>
|
|
200
200
|
|
|
201
201
|
STRICT RULE: If any agent output contains gibberish, mixed-language hallucinations, or fails the language rule, REJECT it immediately and trigger a "STRICT_CLEAN_START" retry.
|
|
@@ -13319,9 +13319,357 @@ Returns matches grouped by pattern, with file paths and line numbers.
|
|
|
13319
13319
|
}
|
|
13320
13320
|
});
|
|
13321
13321
|
|
|
13322
|
+
// src/core/background.ts
|
|
13323
|
+
import { spawn as spawn2 } from "child_process";
|
|
13324
|
+
import { randomBytes } from "crypto";
|
|
13325
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync as existsSync3 } from "fs";
|
|
13326
|
+
import { join as join2 } from "path";
|
|
13327
|
+
import { homedir } from "os";
|
|
13328
|
+
var BackgroundTaskManager = class _BackgroundTaskManager {
|
|
13329
|
+
static _instance;
|
|
13330
|
+
tasks = /* @__PURE__ */ new Map();
|
|
13331
|
+
debugMode = true;
|
|
13332
|
+
// Enable debug mode
|
|
13333
|
+
storageDir;
|
|
13334
|
+
storageFile;
|
|
13335
|
+
monitoringInterval;
|
|
13336
|
+
constructor() {
|
|
13337
|
+
this.storageDir = join2(homedir(), ".opencode-orchestrator");
|
|
13338
|
+
this.storageFile = join2(this.storageDir, "tasks.json");
|
|
13339
|
+
this.loadFromDisk();
|
|
13340
|
+
this.startMonitoring();
|
|
13341
|
+
}
|
|
13342
|
+
static get instance() {
|
|
13343
|
+
if (!_BackgroundTaskManager._instance) {
|
|
13344
|
+
_BackgroundTaskManager._instance = new _BackgroundTaskManager();
|
|
13345
|
+
}
|
|
13346
|
+
return _BackgroundTaskManager._instance;
|
|
13347
|
+
}
|
|
13348
|
+
/**
|
|
13349
|
+
* Generate a unique task ID in the format job_xxxxxxxx
|
|
13350
|
+
*/
|
|
13351
|
+
generateId() {
|
|
13352
|
+
const hex3 = randomBytes(4).toString("hex");
|
|
13353
|
+
return `job_${hex3}`;
|
|
13354
|
+
}
|
|
13355
|
+
/**
|
|
13356
|
+
* Ensure storage directory exists
|
|
13357
|
+
*/
|
|
13358
|
+
ensureStorageDir() {
|
|
13359
|
+
if (!existsSync3(this.storageDir)) {
|
|
13360
|
+
mkdirSync(this.storageDir, { recursive: true });
|
|
13361
|
+
this.debug("system", `Created storage directory: ${this.storageDir}`);
|
|
13362
|
+
}
|
|
13363
|
+
}
|
|
13364
|
+
/**
|
|
13365
|
+
* Load tasks from disk on startup
|
|
13366
|
+
*/
|
|
13367
|
+
loadFromDisk() {
|
|
13368
|
+
this.ensureStorageDir();
|
|
13369
|
+
if (!existsSync3(this.storageFile)) {
|
|
13370
|
+
this.debug("system", "No existing task data on disk");
|
|
13371
|
+
return;
|
|
13372
|
+
}
|
|
13373
|
+
try {
|
|
13374
|
+
const data = readFileSync(this.storageFile, "utf-8");
|
|
13375
|
+
const tasksData = JSON.parse(data);
|
|
13376
|
+
for (const [id, taskData] of Object.entries(tasksData)) {
|
|
13377
|
+
const task = taskData;
|
|
13378
|
+
task.process = void 0;
|
|
13379
|
+
if (task.status === "running") {
|
|
13380
|
+
task.status = "error";
|
|
13381
|
+
task.errorOutput += "\n[Process lost on restart]";
|
|
13382
|
+
task.endTime = Date.now();
|
|
13383
|
+
task.exitCode = null;
|
|
13384
|
+
}
|
|
13385
|
+
this.tasks.set(id, task);
|
|
13386
|
+
}
|
|
13387
|
+
this.debug("system", `Loaded ${this.tasks.size} tasks from disk`);
|
|
13388
|
+
} catch (error45) {
|
|
13389
|
+
this.debug(
|
|
13390
|
+
"system",
|
|
13391
|
+
`Failed to load tasks: ${error45 instanceof Error ? error45.message : String(error45)}`
|
|
13392
|
+
);
|
|
13393
|
+
}
|
|
13394
|
+
}
|
|
13395
|
+
/**
|
|
13396
|
+
* Save tasks to disk
|
|
13397
|
+
*/
|
|
13398
|
+
saveToDisk() {
|
|
13399
|
+
this.ensureStorageDir();
|
|
13400
|
+
try {
|
|
13401
|
+
const tasksData = {};
|
|
13402
|
+
for (const [id, task] of this.tasks.entries()) {
|
|
13403
|
+
tasksData[id] = task;
|
|
13404
|
+
}
|
|
13405
|
+
writeFileSync(
|
|
13406
|
+
this.storageFile,
|
|
13407
|
+
JSON.stringify(tasksData, null, 2),
|
|
13408
|
+
"utf-8"
|
|
13409
|
+
);
|
|
13410
|
+
} catch (error45) {
|
|
13411
|
+
this.debug(
|
|
13412
|
+
"system",
|
|
13413
|
+
`Failed to save tasks: ${error45 instanceof Error ? error45.message : String(error45)}`
|
|
13414
|
+
);
|
|
13415
|
+
}
|
|
13416
|
+
}
|
|
13417
|
+
/**
|
|
13418
|
+
* Start periodic monitoring of running processes
|
|
13419
|
+
*/
|
|
13420
|
+
startMonitoring() {
|
|
13421
|
+
const MONITOR_INTERVAL_MS = 5e3;
|
|
13422
|
+
this.monitoringInterval = setInterval(() => {
|
|
13423
|
+
this.monitorRunningProcesses();
|
|
13424
|
+
}, MONITOR_INTERVAL_MS);
|
|
13425
|
+
if (this.monitoringInterval) {
|
|
13426
|
+
this.monitoringInterval.unref();
|
|
13427
|
+
}
|
|
13428
|
+
}
|
|
13429
|
+
/**
|
|
13430
|
+
* Stop monitoring
|
|
13431
|
+
*/
|
|
13432
|
+
stopMonitoring() {
|
|
13433
|
+
if (this.monitoringInterval) {
|
|
13434
|
+
clearInterval(this.monitoringInterval);
|
|
13435
|
+
this.monitoringInterval = void 0;
|
|
13436
|
+
}
|
|
13437
|
+
}
|
|
13438
|
+
/**
|
|
13439
|
+
* Monitor running processes and detect zombie processes
|
|
13440
|
+
*/
|
|
13441
|
+
monitorRunningProcesses() {
|
|
13442
|
+
const now = Date.now();
|
|
13443
|
+
let hasRunningTasks = false;
|
|
13444
|
+
for (const [id, task] of this.tasks.entries()) {
|
|
13445
|
+
if (task.status !== "running") continue;
|
|
13446
|
+
hasRunningTasks = true;
|
|
13447
|
+
if (task.process && task.process.pid) {
|
|
13448
|
+
const pid = task.process.pid;
|
|
13449
|
+
try {
|
|
13450
|
+
process.kill(pid, 0);
|
|
13451
|
+
} catch (error45) {
|
|
13452
|
+
task.status = "error";
|
|
13453
|
+
task.errorOutput += `
|
|
13454
|
+
Process disappeared (PID ${pid})`;
|
|
13455
|
+
task.endTime = Date.now();
|
|
13456
|
+
task.exitCode = null;
|
|
13457
|
+
task.process = void 0;
|
|
13458
|
+
this.saveToDisk();
|
|
13459
|
+
this.debug(id, `Process dead (PID ${pid}), marked as error`);
|
|
13460
|
+
}
|
|
13461
|
+
} else if (task.process) {
|
|
13462
|
+
task.status = "error";
|
|
13463
|
+
task.errorOutput += "\nProcess reference lost";
|
|
13464
|
+
task.endTime = Date.now();
|
|
13465
|
+
task.exitCode = null;
|
|
13466
|
+
this.saveToDisk();
|
|
13467
|
+
this.debug(id, "Process reference lost, marked as error");
|
|
13468
|
+
}
|
|
13469
|
+
}
|
|
13470
|
+
if (!hasRunningTasks && this.monitoringInterval) {
|
|
13471
|
+
this.stopMonitoring();
|
|
13472
|
+
}
|
|
13473
|
+
}
|
|
13474
|
+
/**
|
|
13475
|
+
* Debug logging helper
|
|
13476
|
+
*/
|
|
13477
|
+
debug(taskId, message) {
|
|
13478
|
+
if (this.debugMode) {
|
|
13479
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().substring(11, 23);
|
|
13480
|
+
console.log(`[BG-DEBUG ${timestamp}] ${taskId}: ${message}`);
|
|
13481
|
+
}
|
|
13482
|
+
}
|
|
13483
|
+
/**
|
|
13484
|
+
* Run a command in the background
|
|
13485
|
+
*/
|
|
13486
|
+
run(options) {
|
|
13487
|
+
const id = this.generateId();
|
|
13488
|
+
const { command, cwd = process.cwd(), timeout = 3e5, label } = options;
|
|
13489
|
+
const isWindows = process.platform === "win32";
|
|
13490
|
+
const shell = isWindows ? "cmd.exe" : "/bin/sh";
|
|
13491
|
+
const shellFlag = isWindows ? "/c" : "-c";
|
|
13492
|
+
const task = {
|
|
13493
|
+
id,
|
|
13494
|
+
command,
|
|
13495
|
+
args: [shellFlag, command],
|
|
13496
|
+
cwd,
|
|
13497
|
+
label,
|
|
13498
|
+
status: "running",
|
|
13499
|
+
output: "",
|
|
13500
|
+
errorOutput: "",
|
|
13501
|
+
exitCode: null,
|
|
13502
|
+
startTime: Date.now(),
|
|
13503
|
+
timeout
|
|
13504
|
+
};
|
|
13505
|
+
this.tasks.set(id, task);
|
|
13506
|
+
this.saveToDisk();
|
|
13507
|
+
this.debug(id, `Starting: ${command} (cwd: ${cwd})`);
|
|
13508
|
+
try {
|
|
13509
|
+
const proc = spawn2(shell, task.args, {
|
|
13510
|
+
cwd,
|
|
13511
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
13512
|
+
detached: false
|
|
13513
|
+
});
|
|
13514
|
+
task.process = proc;
|
|
13515
|
+
proc.stdout?.on("data", (data) => {
|
|
13516
|
+
const text = data.toString();
|
|
13517
|
+
task.output += text;
|
|
13518
|
+
this.debug(
|
|
13519
|
+
id,
|
|
13520
|
+
`stdout: ${text.substring(0, 100)}${text.length > 100 ? "..." : ""}`
|
|
13521
|
+
);
|
|
13522
|
+
});
|
|
13523
|
+
proc.stderr?.on("data", (data) => {
|
|
13524
|
+
const text = data.toString();
|
|
13525
|
+
task.errorOutput += text;
|
|
13526
|
+
this.debug(
|
|
13527
|
+
id,
|
|
13528
|
+
`stderr: ${text.substring(0, 100)}${text.length > 100 ? "..." : ""}`
|
|
13529
|
+
);
|
|
13530
|
+
});
|
|
13531
|
+
proc.on("close", (code) => {
|
|
13532
|
+
task.exitCode = code;
|
|
13533
|
+
task.endTime = Date.now();
|
|
13534
|
+
task.status = code === 0 ? "done" : "error";
|
|
13535
|
+
task.process = void 0;
|
|
13536
|
+
this.saveToDisk();
|
|
13537
|
+
const duration3 = ((task.endTime - task.startTime) / 1e3).toFixed(2);
|
|
13538
|
+
this.debug(id, `Completed with code ${code} in ${duration3}s`);
|
|
13539
|
+
});
|
|
13540
|
+
proc.on("error", (err) => {
|
|
13541
|
+
task.status = "error";
|
|
13542
|
+
task.errorOutput += `
|
|
13543
|
+
Process error: ${err.message}`;
|
|
13544
|
+
task.endTime = Date.now();
|
|
13545
|
+
task.process = void 0;
|
|
13546
|
+
this.saveToDisk();
|
|
13547
|
+
this.debug(id, `Error: ${err.message}`);
|
|
13548
|
+
});
|
|
13549
|
+
setTimeout(() => {
|
|
13550
|
+
if (task.status === "running" && task.process) {
|
|
13551
|
+
this.debug(id, `Timeout after ${timeout}ms, killing process`);
|
|
13552
|
+
task.process.kill("SIGKILL");
|
|
13553
|
+
task.status = "timeout";
|
|
13554
|
+
task.endTime = Date.now();
|
|
13555
|
+
task.errorOutput += `
|
|
13556
|
+
Process killed: timeout after ${timeout}ms`;
|
|
13557
|
+
this.saveToDisk();
|
|
13558
|
+
}
|
|
13559
|
+
}, timeout);
|
|
13560
|
+
} catch (err) {
|
|
13561
|
+
task.status = "error";
|
|
13562
|
+
task.errorOutput = `Failed to spawn: ${err instanceof Error ? err.message : String(err)}`;
|
|
13563
|
+
task.endTime = Date.now();
|
|
13564
|
+
this.saveToDisk();
|
|
13565
|
+
this.debug(id, `Spawn failed: ${task.errorOutput}`);
|
|
13566
|
+
}
|
|
13567
|
+
return task;
|
|
13568
|
+
}
|
|
13569
|
+
/**
|
|
13570
|
+
* Get task by ID
|
|
13571
|
+
*/
|
|
13572
|
+
get(taskId) {
|
|
13573
|
+
return this.tasks.get(taskId);
|
|
13574
|
+
}
|
|
13575
|
+
/**
|
|
13576
|
+
* Get all tasks
|
|
13577
|
+
*/
|
|
13578
|
+
getAll() {
|
|
13579
|
+
return Array.from(this.tasks.values());
|
|
13580
|
+
}
|
|
13581
|
+
/**
|
|
13582
|
+
* Get tasks by status
|
|
13583
|
+
*/
|
|
13584
|
+
getByStatus(status) {
|
|
13585
|
+
return this.getAll().filter((t) => t.status === status);
|
|
13586
|
+
}
|
|
13587
|
+
/**
|
|
13588
|
+
* Clean up tasks by session ID
|
|
13589
|
+
*/
|
|
13590
|
+
cleanupBySession(sessionID) {
|
|
13591
|
+
let count = 0;
|
|
13592
|
+
for (const [id, task] of this.tasks) {
|
|
13593
|
+
if (task.sessionID === sessionID) {
|
|
13594
|
+
if (task.process && task.status === "running") {
|
|
13595
|
+
task.process.kill("SIGKILL");
|
|
13596
|
+
}
|
|
13597
|
+
this.tasks.delete(id);
|
|
13598
|
+
count++;
|
|
13599
|
+
this.debug(id, `Cleaned up for session ${sessionID}`);
|
|
13600
|
+
}
|
|
13601
|
+
}
|
|
13602
|
+
this.saveToDisk();
|
|
13603
|
+
return count;
|
|
13604
|
+
}
|
|
13605
|
+
/**
|
|
13606
|
+
* Clear completed/failed tasks
|
|
13607
|
+
*/
|
|
13608
|
+
clearCompleted() {
|
|
13609
|
+
let count = 0;
|
|
13610
|
+
for (const [id, task] of this.tasks) {
|
|
13611
|
+
if (task.status !== "running" && task.status !== "pending") {
|
|
13612
|
+
this.tasks.delete(id);
|
|
13613
|
+
count++;
|
|
13614
|
+
}
|
|
13615
|
+
}
|
|
13616
|
+
this.saveToDisk();
|
|
13617
|
+
return count;
|
|
13618
|
+
}
|
|
13619
|
+
/**
|
|
13620
|
+
* Kill a running task
|
|
13621
|
+
*/
|
|
13622
|
+
kill(taskId) {
|
|
13623
|
+
const task = this.tasks.get(taskId);
|
|
13624
|
+
if (task?.process) {
|
|
13625
|
+
task.process.kill("SIGKILL");
|
|
13626
|
+
task.status = "error";
|
|
13627
|
+
task.errorOutput += "\nKilled by user";
|
|
13628
|
+
task.endTime = Date.now();
|
|
13629
|
+
this.saveToDisk();
|
|
13630
|
+
this.debug(taskId, "Killed by user");
|
|
13631
|
+
return true;
|
|
13632
|
+
}
|
|
13633
|
+
return false;
|
|
13634
|
+
}
|
|
13635
|
+
/**
|
|
13636
|
+
* Format duration for display
|
|
13637
|
+
*/
|
|
13638
|
+
formatDuration(task) {
|
|
13639
|
+
const end = task.endTime || Date.now();
|
|
13640
|
+
const seconds = (end - task.startTime) / 1e3;
|
|
13641
|
+
if (seconds < 60) {
|
|
13642
|
+
return `${seconds.toFixed(1)}s`;
|
|
13643
|
+
}
|
|
13644
|
+
const minutes = Math.floor(seconds / 60);
|
|
13645
|
+
const remainingSeconds = seconds % 60;
|
|
13646
|
+
return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
|
|
13647
|
+
}
|
|
13648
|
+
/**
|
|
13649
|
+
* Get status emoji
|
|
13650
|
+
*/
|
|
13651
|
+
getStatusEmoji(status) {
|
|
13652
|
+
switch (status) {
|
|
13653
|
+
case "pending":
|
|
13654
|
+
return "\u23F8\uFE0F";
|
|
13655
|
+
case "running":
|
|
13656
|
+
return "\u23F3";
|
|
13657
|
+
case "done":
|
|
13658
|
+
return "\u2705";
|
|
13659
|
+
case "error":
|
|
13660
|
+
return "\u274C";
|
|
13661
|
+
case "timeout":
|
|
13662
|
+
return "\u23F0";
|
|
13663
|
+
default:
|
|
13664
|
+
return "\u2753";
|
|
13665
|
+
}
|
|
13666
|
+
}
|
|
13667
|
+
};
|
|
13668
|
+
var backgroundTaskManager = BackgroundTaskManager.instance;
|
|
13669
|
+
|
|
13322
13670
|
// src/tools/background.ts
|
|
13323
13671
|
var runBackgroundTool = tool({
|
|
13324
|
-
description: `Run a shell command in background and get a task ID.
|
|
13672
|
+
description: `Run a shell command in the background and get a task ID.
|
|
13325
13673
|
|
|
13326
13674
|
<purpose>
|
|
13327
13675
|
Execute long-running commands (builds, tests, etc.) without blocking.
|
|
@@ -13344,10 +13692,31 @@ The command runs asynchronously - use check_background to get results.
|
|
|
13344
13692
|
command: tool.schema.string().describe("Shell command to execute"),
|
|
13345
13693
|
cwd: tool.schema.string().optional().describe("Working directory (default: project root)"),
|
|
13346
13694
|
timeout: tool.schema.number().optional().describe("Timeout in milliseconds (default: 300000 = 5 min)"),
|
|
13347
|
-
label: tool.schema.string().optional().describe("Human-readable label for this task")
|
|
13695
|
+
label: tool.schema.string().optional().describe("Human-readable label for this task"),
|
|
13696
|
+
sessionID: tool.schema.string().optional().describe("Session ID for automatic cleanup on session deletion")
|
|
13348
13697
|
},
|
|
13349
13698
|
async execute(args) {
|
|
13350
|
-
|
|
13699
|
+
const { command, cwd, timeout, label, sessionID } = args;
|
|
13700
|
+
const task = backgroundTaskManager.run({
|
|
13701
|
+
command,
|
|
13702
|
+
cwd: cwd || process.cwd(),
|
|
13703
|
+
timeout: timeout || 3e5,
|
|
13704
|
+
label,
|
|
13705
|
+
sessionID
|
|
13706
|
+
});
|
|
13707
|
+
const displayLabel = label ? ` (${label})` : "";
|
|
13708
|
+
return `\u{1F680} **Background Task Started**${displayLabel}
|
|
13709
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
13710
|
+
| Property | Value |
|
|
13711
|
+
|----------|-------|
|
|
13712
|
+
| **Task ID** | \`${task.id}\` |
|
|
13713
|
+
| **Command** | \`${command}\` |
|
|
13714
|
+
| **Status** | ${backgroundTaskManager.getStatusEmoji(task.status)} ${task.status} |
|
|
13715
|
+
| **Working Dir** | ${task.cwd} |
|
|
13716
|
+
| **Timeout** | ${(task.timeout / 1e3).toFixed(0)}s |
|
|
13717
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
13718
|
+
|
|
13719
|
+
\u{1F4CC} **Next Step**: Use \`check_background\` with task ID \`${task.id}\` to get results.`;
|
|
13351
13720
|
}
|
|
13352
13721
|
});
|
|
13353
13722
|
var checkBackgroundTool = tool({
|
|
@@ -13365,11 +13734,75 @@ Use this after run_background to get results.
|
|
|
13365
13734
|
- Full output (stdout + stderr)
|
|
13366
13735
|
</output_includes>`,
|
|
13367
13736
|
args: {
|
|
13368
|
-
|
|
13369
|
-
|
|
13737
|
+
taskId: tool.schema.string().describe("Task ID from run_background (e.g., job_a1b2c3d4)"),
|
|
13738
|
+
tailLines: tool.schema.number().optional().describe("Limit output to last N lines (default: show all)")
|
|
13370
13739
|
},
|
|
13371
13740
|
async execute(args) {
|
|
13372
|
-
|
|
13741
|
+
const { taskId, tailLines } = args;
|
|
13742
|
+
const task = backgroundTaskManager.get(taskId);
|
|
13743
|
+
if (!task) {
|
|
13744
|
+
const allTasks = backgroundTaskManager.getAll();
|
|
13745
|
+
if (allTasks.length === 0) {
|
|
13746
|
+
return `\u274C Task \`${taskId}\` not found. No background tasks exist.`;
|
|
13747
|
+
}
|
|
13748
|
+
const taskList = allTasks.map((t) => `- \`${t.id}\`: ${t.command.substring(0, 30)}...`).join("\n");
|
|
13749
|
+
return `\u274C Task \`${taskId}\` not found.
|
|
13750
|
+
|
|
13751
|
+
**Available tasks:**
|
|
13752
|
+
${taskList}`;
|
|
13753
|
+
}
|
|
13754
|
+
const duration3 = backgroundTaskManager.formatDuration(task);
|
|
13755
|
+
const statusEmoji = backgroundTaskManager.getStatusEmoji(task.status);
|
|
13756
|
+
let output = task.output;
|
|
13757
|
+
let stderr = task.errorOutput;
|
|
13758
|
+
if (tailLines && tailLines > 0) {
|
|
13759
|
+
const outputLines = output.split("\n");
|
|
13760
|
+
const stderrLines = stderr.split("\n");
|
|
13761
|
+
output = outputLines.slice(-tailLines).join("\n");
|
|
13762
|
+
stderr = stderrLines.slice(-tailLines).join("\n");
|
|
13763
|
+
}
|
|
13764
|
+
const maxLen = 1e4;
|
|
13765
|
+
if (output.length > maxLen) {
|
|
13766
|
+
output = `[...truncated ${output.length - maxLen} chars...]
|
|
13767
|
+
` + output.substring(output.length - maxLen);
|
|
13768
|
+
}
|
|
13769
|
+
if (stderr.length > maxLen) {
|
|
13770
|
+
stderr = `[...truncated ${stderr.length - maxLen} chars...]
|
|
13771
|
+
` + stderr.substring(stderr.length - maxLen);
|
|
13772
|
+
}
|
|
13773
|
+
const labelDisplay = task.label ? ` (${task.label})` : "";
|
|
13774
|
+
let result = `${statusEmoji} **Task ${task.id}**${labelDisplay}
|
|
13775
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
13776
|
+
| Property | Value |
|
|
13777
|
+
|----------|-------|
|
|
13778
|
+
| **Command** | \`${task.command}\` |
|
|
13779
|
+
| **Status** | ${statusEmoji} **${task.status.toUpperCase()}** |
|
|
13780
|
+
| **Duration** | ${duration3}${task.status === "running" ? " (ongoing)" : ""} |
|
|
13781
|
+
${task.exitCode !== null ? `| **Exit Code** | ${task.exitCode} |` : ""}
|
|
13782
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`;
|
|
13783
|
+
if (output.trim()) {
|
|
13784
|
+
result += `
|
|
13785
|
+
|
|
13786
|
+
\u{1F4E4} **Output (stdout)**:
|
|
13787
|
+
\`\`\`
|
|
13788
|
+
${output.trim()}
|
|
13789
|
+
\`\`\``;
|
|
13790
|
+
}
|
|
13791
|
+
if (stderr.trim()) {
|
|
13792
|
+
result += `
|
|
13793
|
+
|
|
13794
|
+
\u26A0\uFE0F **Errors (stderr)**:
|
|
13795
|
+
\`\`\`
|
|
13796
|
+
${stderr.trim()}
|
|
13797
|
+
\`\`\``;
|
|
13798
|
+
}
|
|
13799
|
+
if (task.status === "running") {
|
|
13800
|
+
result += `
|
|
13801
|
+
|
|
13802
|
+
\u23F3 Task still running... Check again later with:
|
|
13803
|
+
\`check_background({ taskId: "${task.id}" })\``;
|
|
13804
|
+
}
|
|
13805
|
+
return result;
|
|
13373
13806
|
}
|
|
13374
13807
|
});
|
|
13375
13808
|
var listBackgroundTool = tool({
|
|
@@ -13383,7 +13816,39 @@ Useful to check what's in progress before starting new tasks.
|
|
|
13383
13816
|
status: tool.schema.enum(["all", "running", "done", "error"]).optional().describe("Filter by status (default: all)")
|
|
13384
13817
|
},
|
|
13385
13818
|
async execute(args) {
|
|
13386
|
-
|
|
13819
|
+
const { status = "all" } = args;
|
|
13820
|
+
let tasks;
|
|
13821
|
+
if (status === "all") {
|
|
13822
|
+
tasks = backgroundTaskManager.getAll();
|
|
13823
|
+
} else {
|
|
13824
|
+
tasks = backgroundTaskManager.getByStatus(status);
|
|
13825
|
+
}
|
|
13826
|
+
if (tasks.length === 0) {
|
|
13827
|
+
return `\u{1F4CB} **No background tasks** ${status !== "all" ? `with status "${status}"` : ""}
|
|
13828
|
+
|
|
13829
|
+
Use \`run_background\` to start a new background task.`;
|
|
13830
|
+
}
|
|
13831
|
+
tasks.sort((a, b) => b.startTime - a.startTime);
|
|
13832
|
+
const rows = tasks.map((task) => {
|
|
13833
|
+
const emoji3 = backgroundTaskManager.getStatusEmoji(task.status);
|
|
13834
|
+
const duration3 = backgroundTaskManager.formatDuration(task);
|
|
13835
|
+
const cmdShort = task.command.length > 25 ? task.command.substring(0, 22) + "..." : task.command;
|
|
13836
|
+
const labelPart = task.label ? ` [${task.label}]` : "";
|
|
13837
|
+
return `| \`${task.id}\` | ${emoji3} ${task.status.padEnd(7)} | ${cmdShort.padEnd(25)}${labelPart} | ${duration3.padStart(8)} |`;
|
|
13838
|
+
}).join("\n");
|
|
13839
|
+
const runningCount = tasks.filter((t) => t.status === "running").length;
|
|
13840
|
+
const doneCount = tasks.filter((t) => t.status === "done").length;
|
|
13841
|
+
const errorCount = tasks.filter((t) => t.status === "error" || t.status === "timeout").length;
|
|
13842
|
+
return `\u{1F4CB} **Background Tasks** (${tasks.length} total)
|
|
13843
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
13844
|
+
| \u23F3 Running: ${runningCount} | \u2705 Done: ${doneCount} | \u274C Error/Timeout: ${errorCount} |
|
|
13845
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
13846
|
+
|
|
13847
|
+
| Task ID | Status | Command | Duration |
|
|
13848
|
+
|---------|--------|---------|----------|
|
|
13849
|
+
${rows}
|
|
13850
|
+
|
|
13851
|
+
\u{1F4A1} Use \`check_background({ taskId: "job_xxxxx" })\` to see full output.`;
|
|
13387
13852
|
}
|
|
13388
13853
|
});
|
|
13389
13854
|
var killBackgroundTool = tool({
|
|
@@ -13393,10 +13858,24 @@ var killBackgroundTool = tool({
|
|
|
13393
13858
|
Stop a background task that is taking too long or no longer needed.
|
|
13394
13859
|
</purpose>`,
|
|
13395
13860
|
args: {
|
|
13396
|
-
|
|
13861
|
+
taskId: tool.schema.string().describe("Task ID to kill (e.g., job_a1b2c3d4)")
|
|
13397
13862
|
},
|
|
13398
13863
|
async execute(args) {
|
|
13399
|
-
|
|
13864
|
+
const { taskId } = args;
|
|
13865
|
+
const task = backgroundTaskManager.get(taskId);
|
|
13866
|
+
if (!task) {
|
|
13867
|
+
return `\u274C Task \`${taskId}\` not found.`;
|
|
13868
|
+
}
|
|
13869
|
+
if (task.status !== "running") {
|
|
13870
|
+
return `\u26A0\uFE0F Task \`${taskId}\` is not running (status: ${task.status}).`;
|
|
13871
|
+
}
|
|
13872
|
+
const killed = backgroundTaskManager.kill(taskId);
|
|
13873
|
+
if (killed) {
|
|
13874
|
+
return `\u{1F6D1} Task \`${taskId}\` has been killed.
|
|
13875
|
+
Command: \`${task.command}\`
|
|
13876
|
+
Duration before kill: ${backgroundTaskManager.formatDuration(task)}`;
|
|
13877
|
+
}
|
|
13878
|
+
return `\u26A0\uFE0F Could not kill task \`${taskId}\`. It may have already finished.`;
|
|
13400
13879
|
}
|
|
13401
13880
|
});
|
|
13402
13881
|
|
|
@@ -13922,10 +14401,14 @@ var createDelegateTaskTool = (manager, client) => tool({
|
|
|
13922
14401
|
- Auto-cleanup: 5 minutes after completion
|
|
13923
14402
|
</safety>`,
|
|
13924
14403
|
args: {
|
|
13925
|
-
agent: tool.schema.string().describe(
|
|
14404
|
+
agent: tool.schema.string().describe(
|
|
14405
|
+
`Agent name (e.g., '${AGENT_NAMES.BUILDER}', '${AGENT_NAMES.INSPECTOR}', '${AGENT_NAMES.ARCHITECT}')`
|
|
14406
|
+
),
|
|
13926
14407
|
description: tool.schema.string().describe("Short task description"),
|
|
13927
14408
|
prompt: tool.schema.string().describe("Full prompt/instructions for the agent"),
|
|
13928
|
-
background: tool.schema.boolean().describe(
|
|
14409
|
+
background: tool.schema.boolean().describe(
|
|
14410
|
+
"true=async (returns task_id), false=sync (waits for result). REQUIRED."
|
|
14411
|
+
)
|
|
13929
14412
|
},
|
|
13930
14413
|
async execute(args, context) {
|
|
13931
14414
|
const { agent, description, prompt, background } = args;
|
|
@@ -13946,7 +14429,9 @@ var createDelegateTaskTool = (manager, client) => tool({
|
|
|
13946
14429
|
});
|
|
13947
14430
|
const runningCount = manager.getRunningTasks().length;
|
|
13948
14431
|
const pendingCount = manager.getPendingCount(ctx.sessionID);
|
|
13949
|
-
console.log(
|
|
14432
|
+
console.log(
|
|
14433
|
+
`[parallel] \u{1F680} SPAWNED ${task.id} \u2192 ${agent}: ${description}`
|
|
14434
|
+
);
|
|
13950
14435
|
return `
|
|
13951
14436
|
## \u{1F680} BACKGROUND TASK SPAWNED
|
|
13952
14437
|
|
|
@@ -14025,7 +14510,9 @@ Session ID: ${sessionID}`;
|
|
|
14025
14510
|
continue;
|
|
14026
14511
|
}
|
|
14027
14512
|
if (Date.now() - startTime < MIN_STABILITY_MS2) continue;
|
|
14028
|
-
const messagesResult2 = await session.messages({
|
|
14513
|
+
const messagesResult2 = await session.messages({
|
|
14514
|
+
path: { id: sessionID }
|
|
14515
|
+
});
|
|
14029
14516
|
const messages2 = messagesResult2.data ?? [];
|
|
14030
14517
|
const currentMsgCount = messages2.length;
|
|
14031
14518
|
if (currentMsgCount === lastMsgCount) {
|
|
@@ -14036,7 +14523,9 @@ Session ID: ${sessionID}`;
|
|
|
14036
14523
|
lastMsgCount = currentMsgCount;
|
|
14037
14524
|
}
|
|
14038
14525
|
}
|
|
14039
|
-
const messagesResult = await session.messages({
|
|
14526
|
+
const messagesResult = await session.messages({
|
|
14527
|
+
path: { id: sessionID }
|
|
14528
|
+
});
|
|
14040
14529
|
const messages = messagesResult.data ?? [];
|
|
14041
14530
|
const assistantMsgs = messages.filter((m) => m.info?.role === "assistant").reverse();
|
|
14042
14531
|
const lastMsg = assistantMsgs[0];
|
|
@@ -14050,7 +14539,9 @@ Session ID: ${sessionID}`;
|
|
|
14050
14539
|
) ?? [];
|
|
14051
14540
|
const textContent = textParts.map((p) => p.text ?? "").filter(Boolean).join("\n");
|
|
14052
14541
|
const duration3 = Math.floor((Date.now() - startTime) / 1e3);
|
|
14053
|
-
console.log(
|
|
14542
|
+
console.log(
|
|
14543
|
+
`[delegate] \u2705 COMPLETED ${agent}: ${description} (${duration3}s)`
|
|
14544
|
+
);
|
|
14054
14545
|
return `\u2705 **Task Completed** (${duration3}s)
|
|
14055
14546
|
|
|
14056
14547
|
Agent: ${agent}
|
|
@@ -14085,7 +14576,9 @@ Wait for the "All Complete" notification before checking.
|
|
|
14085
14576
|
Use \`list_tasks\` to see available tasks.`;
|
|
14086
14577
|
}
|
|
14087
14578
|
if (task.status === "running") {
|
|
14088
|
-
const elapsed = Math.floor(
|
|
14579
|
+
const elapsed = Math.floor(
|
|
14580
|
+
(Date.now() - task.startedAt.getTime()) / 1e3
|
|
14581
|
+
);
|
|
14089
14582
|
return `\u23F3 **Task Still Running**
|
|
14090
14583
|
|
|
14091
14584
|
| Property | Value |
|
|
@@ -14167,7 +14660,9 @@ Use \`delegate_task({ ..., background: true })\` to spawn background tasks.`;
|
|
|
14167
14660
|
}
|
|
14168
14661
|
};
|
|
14169
14662
|
const rows = tasks.map((t) => {
|
|
14170
|
-
const elapsed = Math.floor(
|
|
14663
|
+
const elapsed = Math.floor(
|
|
14664
|
+
(Date.now() - t.startedAt.getTime()) / 1e3
|
|
14665
|
+
);
|
|
14171
14666
|
const desc = t.description.length > 25 ? t.description.slice(0, 22) + "..." : t.description;
|
|
14172
14667
|
return `| \`${t.id}\` | ${statusIcon(t.status)} ${t.status} | ${t.agent} | ${desc} | ${elapsed}s |`;
|
|
14173
14668
|
}).join("\n");
|
|
@@ -14352,11 +14847,11 @@ var PLUGIN_VERSION = "0.2.4";
|
|
|
14352
14847
|
var DEFAULT_MAX_STEPS = 500;
|
|
14353
14848
|
var TASK_COMMAND_MAX_STEPS = 1e3;
|
|
14354
14849
|
var AGENT_EMOJI2 = {
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14850
|
+
[AGENT_NAMES.ARCHITECT]: "\u{1F3D7}\uFE0F",
|
|
14851
|
+
[AGENT_NAMES.BUILDER]: "\u{1F528}",
|
|
14852
|
+
[AGENT_NAMES.INSPECTOR]: "\u{1F50D}",
|
|
14853
|
+
[AGENT_NAMES.RECORDER]: "\u{1F4BE}",
|
|
14854
|
+
[AGENT_NAMES.COMMANDER]: "\u{1F3AF}"
|
|
14360
14855
|
};
|
|
14361
14856
|
var CONTINUE_INSTRUCTION = `<auto_continue>
|
|
14362
14857
|
<status>Mission not complete. Keep executing.</status>
|
|
@@ -14415,7 +14910,7 @@ var OrchestratorPlugin = async (input) => {
|
|
|
14415
14910
|
}
|
|
14416
14911
|
const orchestratorAgents = {
|
|
14417
14912
|
Commander: {
|
|
14418
|
-
name:
|
|
14913
|
+
name: AGENT_NAMES.COMMANDER,
|
|
14419
14914
|
description: "Autonomous orchestrator - executes until mission complete",
|
|
14420
14915
|
systemPrompt: AGENTS.commander.systemPrompt
|
|
14421
14916
|
}
|
|
@@ -14435,7 +14930,7 @@ var OrchestratorPlugin = async (input) => {
|
|
|
14435
14930
|
const parsed = detectSlashCommand(originalText);
|
|
14436
14931
|
const sessionID = msgInput.sessionID;
|
|
14437
14932
|
const agentName = (msgInput.agent || "").toLowerCase();
|
|
14438
|
-
if (agentName ===
|
|
14933
|
+
if (agentName === AGENT_NAMES.COMMANDER && !sessions.has(sessionID)) {
|
|
14439
14934
|
const now = Date.now();
|
|
14440
14935
|
sessions.set(sessionID, {
|
|
14441
14936
|
active: true,
|
|
@@ -14712,8 +15207,13 @@ ${stateSession.graph.getTaskSummary()}`;
|
|
|
14712
15207
|
if (event.type === "session.deleted") {
|
|
14713
15208
|
const props = event.properties;
|
|
14714
15209
|
if (props?.info?.id) {
|
|
14715
|
-
|
|
14716
|
-
|
|
15210
|
+
const sessionID = props.info.id;
|
|
15211
|
+
sessions.delete(sessionID);
|
|
15212
|
+
state.sessions.delete(sessionID);
|
|
15213
|
+
const cleanedCount = backgroundTaskManager.cleanupBySession(sessionID);
|
|
15214
|
+
if (cleanedCount > 0) {
|
|
15215
|
+
console.log(`[background] Cleaned up ${cleanedCount} tasks for deleted session ${sessionID}`);
|
|
15216
|
+
}
|
|
14717
15217
|
}
|
|
14718
15218
|
}
|
|
14719
15219
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Task Tools for OpenCode Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* These tools allow the AI to run commands in the background and check their results later.
|
|
5
|
+
* This is useful for long-running builds, tests, or other operations.
|
|
6
|
+
*/
|
|
1
7
|
export declare const runBackgroundTool: {
|
|
2
8
|
description: string;
|
|
3
9
|
args: {
|
|
@@ -5,23 +11,25 @@ export declare const runBackgroundTool: {
|
|
|
5
11
|
cwd: import("zod").ZodOptional<import("zod").ZodString>;
|
|
6
12
|
timeout: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
7
13
|
label: import("zod").ZodOptional<import("zod").ZodString>;
|
|
14
|
+
sessionID: import("zod").ZodOptional<import("zod").ZodString>;
|
|
8
15
|
};
|
|
9
16
|
execute(args: {
|
|
10
17
|
command: string;
|
|
11
18
|
cwd?: string | undefined;
|
|
12
19
|
timeout?: number | undefined;
|
|
13
20
|
label?: string | undefined;
|
|
21
|
+
sessionID?: string | undefined;
|
|
14
22
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
15
23
|
};
|
|
16
24
|
export declare const checkBackgroundTool: {
|
|
17
25
|
description: string;
|
|
18
26
|
args: {
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
taskId: import("zod").ZodString;
|
|
28
|
+
tailLines: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
21
29
|
};
|
|
22
30
|
execute(args: {
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
taskId: string;
|
|
32
|
+
tailLines?: number | undefined;
|
|
25
33
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
26
34
|
};
|
|
27
35
|
export declare const listBackgroundTool: {
|
|
@@ -29,21 +37,21 @@ export declare const listBackgroundTool: {
|
|
|
29
37
|
args: {
|
|
30
38
|
status: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
31
39
|
running: "running";
|
|
32
|
-
all: "all";
|
|
33
40
|
done: "done";
|
|
34
41
|
error: "error";
|
|
42
|
+
all: "all";
|
|
35
43
|
}>>;
|
|
36
44
|
};
|
|
37
45
|
execute(args: {
|
|
38
|
-
status?: "running" | "
|
|
46
|
+
status?: "running" | "done" | "error" | "all" | undefined;
|
|
39
47
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
40
48
|
};
|
|
41
49
|
export declare const killBackgroundTool: {
|
|
42
50
|
description: string;
|
|
43
51
|
args: {
|
|
44
|
-
|
|
52
|
+
taskId: import("zod").ZodString;
|
|
45
53
|
};
|
|
46
54
|
execute(args: {
|
|
47
|
-
|
|
55
|
+
taskId: string;
|
|
48
56
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
49
57
|
};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "opencode-orchestrator",
|
|
3
3
|
"displayName": "OpenCode Orchestrator",
|
|
4
4
|
"description": "Distributed Cognitive Architecture for OpenCode. Turns simple prompts into specialized multi-agent workflows (Planner, Coder, Reviewer).",
|
|
5
|
-
"version": "0.4.
|
|
5
|
+
"version": "0.4.13",
|
|
6
6
|
"author": "agnusdei1207",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|