opencode-orchestrator 0.5.3 → 0.5.5

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.
Files changed (77) hide show
  1. package/README.md +10 -67
  2. package/dist/agents/definitions.d.ts +1 -1
  3. package/dist/agents/orchestrator.d.ts +1 -1
  4. package/dist/agents/subagents/architect.d.ts +1 -1
  5. package/dist/agents/subagents/builder.d.ts +1 -1
  6. package/dist/agents/subagents/inspector.d.ts +1 -1
  7. package/dist/agents/subagents/recorder.d.ts +1 -1
  8. package/dist/core/agents/concurrency.d.ts +36 -0
  9. package/dist/core/agents/config.d.ts +9 -0
  10. package/dist/core/agents/format.d.ts +9 -0
  11. package/dist/core/agents/index.d.ts +7 -0
  12. package/dist/core/agents/interfaces/index.d.ts +5 -0
  13. package/dist/core/agents/interfaces/launch-input.d.ts +9 -0
  14. package/dist/core/agents/interfaces/parallel-task.d.ts +26 -0
  15. package/dist/core/agents/logger.d.ts +4 -0
  16. package/dist/core/agents/manager.d.ts +76 -0
  17. package/dist/core/agents/task-store.d.ts +28 -0
  18. package/dist/core/agents/types/index.d.ts +4 -0
  19. package/dist/core/agents/types/parallel-task-status.d.ts +4 -0
  20. package/dist/core/commands/index.d.ts +6 -0
  21. package/dist/core/commands/interfaces/background-task.d.ts +20 -0
  22. package/dist/core/commands/interfaces/index.d.ts +5 -0
  23. package/dist/core/commands/interfaces/run-background-options.d.ts +9 -0
  24. package/dist/core/commands/manager.d.ts +27 -0
  25. package/dist/core/commands/types/background-task-status.d.ts +4 -0
  26. package/dist/core/commands/types/index.d.ts +4 -0
  27. package/dist/core/orchestrator/index.d.ts +7 -0
  28. package/dist/core/orchestrator/interfaces/index.d.ts +5 -0
  29. package/dist/core/{state.d.ts → orchestrator/interfaces/session-state.d.ts} +4 -7
  30. package/dist/core/orchestrator/interfaces/task.d.ts +17 -0
  31. package/dist/core/orchestrator/state.d.ts +10 -0
  32. package/dist/core/orchestrator/task-graph.d.ts +17 -0
  33. package/dist/core/orchestrator/types/index.d.ts +5 -0
  34. package/dist/core/orchestrator/types/task-status.d.ts +4 -0
  35. package/dist/core/orchestrator/types/task-type.d.ts +4 -0
  36. package/dist/index.d.ts +2 -24
  37. package/dist/index.js +871 -2561
  38. package/dist/shared/{contracts/names.d.ts → agent.d.ts} +12 -0
  39. package/dist/shared/constants.d.ts +56 -0
  40. package/dist/tools/background-cmd/check.d.ts +14 -0
  41. package/dist/tools/background-cmd/index.d.ts +7 -0
  42. package/dist/tools/background-cmd/kill.d.ts +12 -0
  43. package/dist/tools/background-cmd/list.d.ts +17 -0
  44. package/dist/tools/background-cmd/run.d.ts +18 -0
  45. package/dist/tools/parallel/cancel-task.d.ts +13 -0
  46. package/dist/tools/parallel/delegate-task.d.ts +21 -0
  47. package/dist/tools/parallel/get-task-result.d.ts +13 -0
  48. package/dist/tools/parallel/index.d.ts +7 -0
  49. package/dist/tools/parallel/list-tasks.d.ts +13 -0
  50. package/dist/tools/search.d.ts +2 -9
  51. package/dist/utils/sanity.d.ts +22 -0
  52. package/package.json +18 -12
  53. package/dist/agents/subagents/frontend-designer.d.ts +0 -2
  54. package/dist/constants/agent.d.ts +0 -8
  55. package/dist/constants/index.d.ts +0 -7
  56. package/dist/constants/prompts.d.ts +0 -10
  57. package/dist/constants/task.d.ts +0 -12
  58. package/dist/constants/time.d.ts +0 -34
  59. package/dist/context/enforcer.d.ts +0 -47
  60. package/dist/core/async-agent.d.ts +0 -112
  61. package/dist/core/background.d.ts +0 -111
  62. package/dist/core/batch-processor.d.ts +0 -62
  63. package/dist/core/config.d.ts +0 -55
  64. package/dist/core/session-manager.d.ts +0 -39
  65. package/dist/core/tasks.d.ts +0 -30
  66. package/dist/parallel/optimizer.d.ts +0 -47
  67. package/dist/profiler/execution.d.ts +0 -40
  68. package/dist/prompts/shared.d.ts +0 -2
  69. package/dist/shared/contracts/interfaces.d.ts +0 -7
  70. package/dist/tools/async-agent.d.ts +0 -70
  71. package/dist/tools/background.d.ts +0 -57
  72. package/dist/tools/batch.d.ts +0 -53
  73. package/dist/tools/config.d.ts +0 -60
  74. package/dist/tools/git.d.ts +0 -48
  75. package/dist/utils/formatting.d.ts +0 -13
  76. package/dist/utils/index.d.ts +0 -8
  77. package/dist/utils/task.d.ts +0 -8
package/dist/index.js CHANGED
@@ -1,183 +1,18 @@
1
1
  var __defProp = Object.defineProperty;
2
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
- }) : x)(function(x) {
5
- if (typeof require !== "undefined") return require.apply(this, arguments);
6
- throw Error('Dynamic require of "' + x + '" is not supported');
7
- });
8
2
  var __export = (target, all) => {
9
3
  for (var name in all)
10
4
  __defProp(target, name, { get: all[name], enumerable: true });
11
5
  };
12
6
 
13
- // src/shared/contracts/names.ts
7
+ // src/shared/agent.ts
14
8
  var AGENT_NAMES = {
15
- // Core Agents (5)
16
9
  COMMANDER: "commander",
17
- // Orchestrator - ReAct loop controller
18
10
  ARCHITECT: "architect",
19
- // Planner + Strategist - Plan-and-Execute
20
11
  BUILDER: "builder",
21
- // Coder + Visualist combined (full-stack)
22
12
  INSPECTOR: "inspector",
23
- // Reviewer + Fixer combined (quality + fix)
24
13
  RECORDER: "recorder"
25
- // Persistent context - saves/loads session state
26
14
  };
27
15
 
28
- // src/constants/prompts.ts
29
- var REASONING_CONSTRAINTS = `
30
- <constraints>
31
- 1. Reasoning MUST be in English for maximum stability
32
- 2. If reasoning collapses into gibberish, stop and output: "ERROR: REASONING_COLLAPSE"
33
- 3. Never suppress type errors with 'as any', '@ts-ignore', '@ts-expect-error'
34
- 4. Never leave code in broken state
35
- 5. Always verify with evidence before claiming completion
36
- </constraints>
37
- `;
38
- var LANGUAGE_RULE = `
39
- <language_rule>
40
- THINK and REASON in English for maximum model stability.
41
-
42
- FINAL RESPONSE LANGUAGE:
43
- - Detect user's language from their request
44
- - Respond in SAME language
45
- - Korean \u2192 Korean, English \u2192 English, Japanese \u2192 Japanese, Chinese \u2192 Chinese
46
- - Default to English if unclear
47
- </language_rule>
48
- `;
49
- var ANTI_PATTERNS = `
50
- <anti_patterns>
51
- \u274C Delegate without environment/codebase context
52
- \u274C Leave code broken or with LSP errors
53
- \u274C Make random changes without understanding root cause
54
- \u274C Use 'as any', '@ts-ignore', or '@ts-expect-error'
55
- \u274C Suppress errors instead of fixing them
56
- </anti_patterns>
57
- `;
58
- var WORKFLOW = `
59
- <workflow>
60
- 1. THINK - Reason about the task
61
- 2. ACT - Execute the work
62
- 3. OBSERVE - Check the result
63
- 4. ADJUST - Fix if needed
64
- 5. VERIFY - Prove success with evidence
65
- </workflow>
66
- `;
67
- var BASE_PROMPT = `
68
- ${REASONING_CONSTRAINTS}
69
-
70
- ${LANGUAGE_RULE}
71
-
72
- ${ANTI_PATTERNS}
73
-
74
- ${WORKFLOW}
75
- `;
76
-
77
- // src/utils/sanity.ts
78
- function checkOutputSanity(text) {
79
- if (!text || text.length < 50) {
80
- return { isHealthy: true, severity: "ok" };
81
- }
82
- if (/(.)\1{15,}/.test(text)) {
83
- return {
84
- isHealthy: false,
85
- reason: "Single character repetition detected",
86
- severity: "critical"
87
- };
88
- }
89
- if (/(.{2,6})\1{8,}/.test(text)) {
90
- return {
91
- isHealthy: false,
92
- reason: "Pattern loop detected",
93
- severity: "critical"
94
- };
95
- }
96
- if (text.length > 200) {
97
- const cleanText = text.replace(/\s/g, "");
98
- if (cleanText.length > 100) {
99
- const uniqueChars = new Set(cleanText).size;
100
- const ratio = uniqueChars / cleanText.length;
101
- if (ratio < 0.02) {
102
- return {
103
- isHealthy: false,
104
- reason: "Low information density",
105
- severity: "critical"
106
- };
107
- }
108
- }
109
- }
110
- const boxChars = (text.match(/[\u2500-\u257f\u2580-\u259f\u2800-\u28ff]/g) || []).length;
111
- if (boxChars > 100 && boxChars / text.length > 0.3) {
112
- return {
113
- isHealthy: false,
114
- reason: "Visual gibberish detected",
115
- severity: "critical"
116
- };
117
- }
118
- const lines = text.split("\n").filter((l) => l.trim().length > 10);
119
- if (lines.length > 10) {
120
- const lineSet = new Set(lines);
121
- if (lineSet.size < lines.length * 0.2) {
122
- return {
123
- isHealthy: false,
124
- reason: "Excessive line repetition",
125
- severity: "warning"
126
- };
127
- }
128
- }
129
- const cjkChars = (text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length;
130
- if (cjkChars > 200) {
131
- const uniqueCjk = new Set(
132
- text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []
133
- ).size;
134
- if (uniqueCjk < 10 && cjkChars / uniqueCjk > 20) {
135
- return {
136
- isHealthy: false,
137
- reason: "CJK character spam detected",
138
- severity: "critical"
139
- };
140
- }
141
- }
142
- return { isHealthy: true, severity: "ok" };
143
- }
144
- var RECOVERY_PROMPT = `<anomaly_recovery>
145
- \u26A0\uFE0F SYSTEM NOTICE: Previous output was malformed (gibberish/loop detected).
146
-
147
- <recovery_protocol>
148
- 1. DISCARD the corrupted output completely - do not reference it
149
- 2. RECALL the original mission objective
150
- 3. IDENTIFY the last confirmed successful step
151
- 4. RESTART with a simpler, more focused approach
152
- </recovery_protocol>
153
-
154
- <instructions>
155
- - If a sub-agent produced bad output: try a different agent or simpler task
156
- - If stuck in a loop: break down the task into smaller pieces
157
- - If context seems corrupted: call recorder to restore context
158
- - THINK in English for maximum stability
159
- </instructions>
160
-
161
- What was the original task? Proceed from the last known good state.
162
- </anomaly_recovery>`;
163
- var ESCALATION_PROMPT = `<critical_anomaly>
164
- \u{1F6A8} CRITICAL: Multiple consecutive malformed outputs detected.
165
-
166
- <emergency_protocol>
167
- 1. STOP current execution path immediately
168
- 2. DO NOT continue with the same approach - it is failing
169
- 3. CALL architect for a completely new strategy
170
- 4. If architect also fails: report status to user and await guidance
171
- </emergency_protocol>
172
-
173
- <diagnosis>
174
- The current approach is producing corrupted output.
175
- This may indicate: context overload, model instability, or task complexity.
176
- </diagnosis>
177
-
178
- Request a fresh plan from architect with reduced scope.
179
- </critical_anomaly>`;
180
-
181
16
  // src/agents/orchestrator.ts
182
17
  var orchestrator = {
183
18
  id: AGENT_NAMES.COMMANDER,
@@ -186,10 +21,6 @@ var orchestrator = {
186
21
  You are Commander. Complete missions autonomously. Never stop until done.
187
22
  </role>
188
23
 
189
- ${REASONING_CONSTRAINTS}
190
-
191
- ${LANGUAGE_RULE}
192
-
193
24
  <core_rules>
194
25
  1. Never stop until "\u2705 MISSION COMPLETE"
195
26
  2. Never wait for user during execution
@@ -255,8 +86,8 @@ PREFER background=true (PARALLEL):
255
86
  EXAMPLE - PARALLEL:
256
87
  \`\`\`
257
88
  // Multiple tasks in parallel
258
- delegate_task({ agent: "${AGENT_NAMES.BUILDER}", description: "Implement X", prompt: "...", background: true })
259
- delegate_task({ agent: "${AGENT_NAMES.INSPECTOR}", description: "Review Y", prompt: "...", background: true })
89
+ delegate_task({ agent: "builder", description: "Implement X", prompt: "...", background: true })
90
+ delegate_task({ agent: "inspector", description: "Review Y", prompt: "...", background: true })
260
91
 
261
92
  // Continue other work (don't wait!)
262
93
 
@@ -267,7 +98,7 @@ get_task_result({ taskId: "task_xxx" })
267
98
  EXAMPLE - SYNC (rare):
268
99
  \`\`\`
269
100
  // Only when you absolutely need the result now
270
- const result = delegate_task({ agent: "${AGENT_NAMES.BUILDER}", ..., background: false })
101
+ const result = delegate_task({ agent: "builder", ..., background: false })
271
102
  // Result is immediately available
272
103
  \`\`\`
273
104
  </agent_calling>
@@ -294,10 +125,10 @@ During implementation:
294
125
  PARALLEL EXECUTION TOOLS:
295
126
 
296
127
  1. **spawn_agent** - Launch agents in parallel sessions
297
- spawn_agent({ agent: "${AGENT_NAMES.BUILDER}", description: "Implement X", prompt: "..." })
298
- spawn_agent({ agent: "${AGENT_NAMES.INSPECTOR}", description: "Review Y", prompt: "..." })
299
- \u2192 Agents run concurrently, system notifies when ALL complete
300
- \u2192 Use get_task_result({ taskId }) to retrieve results
128
+ spawn_agent({ agent: "builder", description: "Implement X", prompt: "..." })
129
+ spawn_agent({ agent: "inspector", description: "Review Y", prompt: "..." })
130
+ \u2192 Agents run concurrently, system notifies when ALL complete
131
+ \u2192 Use get_task_result({ taskId }) to retrieve results
301
132
 
302
133
  2. **run_background** - Run shell commands asynchronously
303
134
  run_background({ command: "npm run build" })
@@ -348,10 +179,10 @@ WORKFLOW:
348
179
  <empty_responses>
349
180
  | Agent Empty (or Gibberish) | Action |
350
181
  |----------------------------|--------|
351
- | ${AGENT_NAMES.RECORDER} | Fresh start. Proceed to survey. |
352
- | ${AGENT_NAMES.ARCHITECT} | Try simpler plan yourself. |
353
- | ${AGENT_NAMES.BUILDER} | Call inspector to diagnose. |
354
- | ${AGENT_NAMES.INSPECTOR} | Retry with more context. |
182
+ | recorder | Fresh start. Proceed to survey. |
183
+ | architect | Try simpler plan yourself. |
184
+ | builder | Call inspector to diagnose. |
185
+ | inspector | Retry with more context. |
355
186
  </empty_responses>
356
187
 
357
188
  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.
@@ -363,299 +194,93 @@ STRICT RULE: If any agent output contains gibberish, mixed-language hallucinatio
363
194
  \u274C Make random changes without understanding root cause
364
195
  </anti_patterns>
365
196
 
366
- ${WORKFLOW}
197
+ <completion>
198
+ Done when: Request fulfilled + lsp clean + build/test/audit pass.
367
199
 
368
- <phase_0 name="TRIAGE">
369
- Evaluate the complexity of the request:
200
+ <output_format>
201
+ \u2705 MISSION COMPLETE
202
+ Summary: [what was done]
203
+ Evidence: [Specific build/test/audit results]
204
+ </output_format>
205
+ </completion>`,
206
+ canWrite: true,
207
+ canBash: true
208
+ };
370
209
 
371
- | Level | Signal | Track |
372
- |-------|--------|-------|
373
- | \u{1F7E2} L1: Simple | One file, clear fix, no dependencies | **FAST TRACK** |
374
- | \u{1F7E1} L2: Feature | New functionality, clear patterns | **NORMAL TRACK** |
375
- | \u{1F534} L3: Complex | Refactoring, infra change, unknown scope | **DEEP TRACK** |
376
- </phase_0>
210
+ // src/agents/subagents/architect.ts
211
+ var architect = {
212
+ id: AGENT_NAMES.ARCHITECT,
213
+ description: "Architect - task decomposition and strategic planning",
214
+ systemPrompt: `<role>
215
+ You are Architect. Break complex tasks into atomic pieces.
216
+ </role>
377
217
 
378
- <phase_1 name="CONTEXT_GATHERING">
379
- IF FAST TRACK (L1):
380
- - Scan ONLY the target file and its immediate imports.
381
- - Skip broad infra/domain/doc scans unless an error occurs.
382
- - Proceed directly to execution.
218
+ <constraints>
383
219
 
384
- IF NORMAL/DEEP TRACK (L2/L3):
385
- - **Deep Scan Required**: Execute the full "MANDATORY ENVIRONMENT SCAN".
386
- - 1. Infra check (Docker/OS)
387
- - 2. Domain & Stack check
388
- - 3. Pattern check
220
+ If your reasoning collapses into gibberish, stop and output "ERROR: REASONING_COLLAPSE".
221
+ </constraints>
389
222
 
390
- RECORD findings if on Deep Track.
391
- </phase_1>
223
+ <scalable_planning>
224
+ - **Fast Track**: Skip JSON overhead. Just acknowledge simple task.
225
+ - **Deep Track**: Create detailed JSON DAG with parallel groups.
226
+ </scalable_planning>
392
227
 
393
- <phase_2 name="TOOL_AGENT_SELECTION">
394
- | Track | Strategy |
395
- |-------|----------|
396
- | Fast | Use \`builder\` directly. Skip \`architect\`. |
397
- | Normal | Call \`architect\` for lightweight plan. |
398
- | Deep | Full \`architect\` DAG + \`recorder\` state tracking. |
228
+ <modes>
229
+ - PLAN: New task \u2192 create task list
230
+ - STRATEGY: 3+ failures \u2192 analyze and fix approach
231
+ </modes>
399
232
 
400
- DEFAULT to Deep Track if unsure to act safely.
401
- </phase_2>
233
+ <plan_mode>
234
+ 1. List tasks, one action each
235
+ 2. Group independent tasks (run in parallel)
236
+ 3. Sequence dependent tasks
237
+ 4. Assign: builder (code) or inspector (verify)
402
238
 
403
- <phase_3 name="DELEGATION">
404
- <agent_calling>
405
- CRITICAL: USE delegate_task FOR ALL DELEGATION
239
+ <output_format>
240
+ MISSION: [goal in one line]
406
241
 
407
- delegate_task has TWO MODES:
408
- - background=true: Non-blocking, parallel execution
409
- - background=false: Blocking, waits for result
242
+ T1: [action] | builder | [file] | group:1 | success:[how to verify]
243
+ T2: [action] | builder | [file] | group:1 | success:[how to verify]
244
+ T3: [action] | inspector | [files] | group:2 | depends:T1,T2 | success:[verify method]
245
+ </output_format>
246
+ </plan_mode>
410
247
 
411
- | Situation | How to Call |
412
- |-----------|-------------|
413
- | Multiple independent tasks | \`delegate_task({ ..., background: true })\` for each |
414
- | Single task, continue working | \`delegate_task({ ..., background: true })\` |
415
- | Need result for VERY next step | \`delegate_task({ ..., background: false })\` |
248
+ <strategy_mode trigger="failures > 2">
249
+ <output_format>
250
+ FAILED ATTEMPTS:
251
+ - [what was tried] \u2192 [why failed]
416
252
 
417
- PREFER background=true (PARALLEL):
418
- - Run multiple agents simultaneously
419
- - Continue analysis while they work
420
- - System notifies when ALL complete
253
+ ROOT CAUSE: [actual problem]
421
254
 
422
- EXAMPLE - PARALLEL:
423
- \`\`\`
424
- // Multiple tasks in parallel
425
- delegate_task({ agent: "${AGENT_NAMES.BUILDER}", description: "Implement X", prompt: "...", background: true })
426
- delegate_task({ agent: "${AGENT_NAMES.INSPECTOR}", description: "Review Y", prompt: "...", background: true })
255
+ NEW APPROACH: [different strategy]
427
256
 
428
- // Continue other work (don't wait!)
257
+ REVISED TASKS:
258
+ T1: ...
259
+ </output_format>
260
+ </strategy_mode>
429
261
 
430
- // When notified "All Complete":
431
- get_task_result({ taskId: "task_xxx" })
432
- \`\`\`
262
+ <rules>
263
+ - One action per task
264
+ - Always end with inspector task
265
+ - Group unrelated tasks (parallel)
266
+ - Be specific about files and verification
267
+ </rules>`,
268
+ canWrite: false,
269
+ canBash: false
270
+ };
433
271
 
434
- EXAMPLE - SYNC (rare):
435
- \`\`\`
436
- // Only when you absolutely need the result now
437
- const result = delegate_task({ agent: "${AGENT_NAMES.BUILDER}", ..., background: false })
438
- // Result is immediately available
439
- \`\`\`
440
- </agent_calling>
272
+ // src/agents/subagents/builder.ts
273
+ var builder = {
274
+ id: AGENT_NAMES.BUILDER,
275
+ description: "Builder - full-stack implementation specialist",
276
+ systemPrompt: `<role>
277
+ You are Builder. Write code that works.
278
+ </role>
441
279
 
442
- <delegation_template>
443
- AGENT: [name]
444
- TASK: [one atomic action]
445
- ENVIRONMENT:
446
- - Infra: [e.g. Docker + Volume mount]
447
- - Stack: [e.g. Next.js + PostgreSQL]
448
- - Patterns: [existing code conventions to follow]
449
- MUST: [Specific requirements]
450
- AVOID: [Restrictions]
451
- VERIFY: [Success criteria with evidence]
452
- </delegation_template>
453
- </phase_3>
280
+ <constraints>
454
281
 
455
- <phase_4 name="EXECUTION_VERIFICATION">
456
- During implementation:
457
- - Match existing codebase style exactly
458
- - Run lsp_diagnostics after each change
459
-
460
- <background_parallel_execution>
461
- PARALLEL EXECUTION TOOLS:
462
-
463
- 1. **spawn_agent** - Launch agents in parallel sessions
464
- spawn_agent({ agent: "${AGENT_NAMES.BUILDER}", description: "Implement X", prompt: "..." })
465
- spawn_agent({ agent: "${AGENT_NAMES.INSPECTOR}", description: "Review Y", prompt: "..." })
466
- \u2192 Agents run concurrently, system notifies when ALL complete
467
- \u2192 Use get_task_result({ taskId }) to retrieve results
468
-
469
- 2. **run_background** - Run shell commands asynchronously
470
- run_background({ command: "npm run build" })
471
- \u2192 Use check_background({ taskId }) for results
472
-
473
- SAFETY FEATURES:
474
- - Queue-based concurrency: Max 3 per agent type (extras queue automatically)
475
- - Auto-timeout: 30 minutes max runtime
476
- - Auto-cleanup: Removed from memory 5 min after completion
477
- - Batched notifications: Notifies when ALL tasks complete (not individually)
478
-
479
- MANAGEMENT TOOLS:
480
- - list_tasks: View all parallel tasks and status
481
- - cancel_task: Stop a running task (frees concurrency slot)
482
-
483
- SAFE PATTERNS:
484
- \u2705 Builder on file A + Inspector on file B (different files)
485
- \u2705 Multiple research agents (read-only)
486
- \u2705 Build command + Test command (independent)
487
-
488
- UNSAFE PATTERNS:
489
- \u274C Multiple builders editing SAME FILE (conflict!)
490
-
491
- WORKFLOW:
492
- 1. list_tasks: Check current status first
493
- 2. spawn_agent: Launch for INDEPENDENT tasks
494
- 3. Continue working (NO WAITING)
495
- 4. Wait for "All Complete" notification
496
- 5. get_task_result: Retrieve each result
497
- </background_parallel_execution>
498
-
499
- <verification_methods>
500
- | Infra | Proof Method |
501
- |-------|--------------|
502
- | OS-Native | npm run build, cargo build, specific test runs |
503
- | Container | Docker syntax check + config validation |
504
- | Live API | curl /health if reachable, check logs |
505
- | Generic | Manual audit by Inspector with logic summary |
506
- </verification_methods>
507
- </phase_4>
508
-
509
- <failure_recovery>
510
- | Failures | Action |
511
- |----------|--------|
512
- | 1-2 | Adjust approach, retry |
513
- | 3+ | STOP. Call architect for new strategy |
514
-
515
- <empty_responses>
516
- | Agent Empty (or Gibberish) | Action |
517
- |----------------------------|--------|
518
- | ${AGENT_NAMES.RECORDER} | Fresh start. Proceed to survey. |
519
- | ${AGENT_NAMES.ARCHITECT} | Try simpler plan yourself. |
520
- | ${AGENT_NAMES.BUILDER} | Call inspector to diagnose. |
521
- | ${AGENT_NAMES.INSPECTOR} | Retry with more context. |
522
- </empty_responses>
523
-
524
- 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.
525
- </failure_recovery>
526
-
527
- ${ANTI_PATTERNS}
528
-
529
- <completion>
530
- Done when: Request fulfilled + lsp clean + build/test/audit pass.
531
-
532
- <output_format>
533
- \u2705 MISSION COMPLETE
534
- Summary: [what was done]
535
- Evidence: [Specific build/test/audit results]
536
- </output_format>
537
- </completion>`,
538
- canWrite: true,
539
- canBash: true
540
- };
541
-
542
- // src/agents/subagents/architect.ts
543
- var architect = {
544
- id: AGENT_NAMES.ARCHITECT,
545
- description: "Architect - task decomposition and strategic planning",
546
- systemPrompt: `<role>
547
- You are Architect. Break complex tasks into atomic pieces.
548
- </role>
549
-
550
- ${REASONING_CONSTRAINTS}
551
-
552
- ${WORKFLOW}
553
-
554
- <scalable_planning>
555
- - **Fast Track**: Skip JSON overhead. Just acknowledge simple task.
556
- - **Deep Track**: Create detailed JSON DAG with parallel groups.
557
- </scalable_planning>
558
-
559
- <modes>
560
- - PLAN: New task \u2192 create task list
561
- - STRATEGY: 3+ failures \u2192 analyze and fix approach
562
- </modes>
563
-
564
- <plan_mode>
565
- 1. List tasks, one action each
566
- 2. Group independent tasks (run in parallel)
567
- 3. Sequence dependent tasks
568
- 4. Assign: builder (code) or inspector (verify)
569
-
570
- <output_format>
571
- MUST output valid JSON block wrapped in backticks:
572
-
573
- <json_output>
574
- {
575
- "mission": "goal in one line",
576
- "tasks": [
577
- {
578
- "id": "T1",
579
- "action": "one atomic action",
580
- "agent": "builder",
581
- "file": "path/to/file",
582
- "parallel_group": 1,
583
- "dependencies": [],
584
- "success_criteria": "how to verify"
585
- },
586
- {
587
- "id": "T2",
588
- "action": "one atomic action",
589
- "agent": "builder",
590
- "file": "path/to/file",
591
- "parallel_group": 1,
592
- "dependencies": [],
593
- "success_criteria": "how to verify"
594
- },
595
- {
596
- "id": "T3",
597
- "action": "one atomic action",
598
- "agent": "inspector",
599
- "file": "path/to/file",
600
- "parallel_group": 2,
601
- "dependencies": ["T1", "T2"],
602
- "success_criteria": "verify method"
603
- }
604
- ]
605
- }
606
- </json_output>
607
- </output_format>
608
- </plan_mode>
609
-
610
- <strategy_mode trigger="failures > 2">
611
- <output_format>
612
- <json_output>
613
- {
614
- "failed_attempts": [
615
- {"attempt": "what was tried", "reason": "why failed"}
616
- ],
617
- "root_cause": "actual problem",
618
- "new_approach": "different strategy",
619
- "revised_tasks": [
620
- {
621
- "id": "T1",
622
- "action": "one atomic action",
623
- "agent": "builder",
624
- "file": "path/to/file",
625
- "parallel_group": 1,
626
- "dependencies": [],
627
- "success_criteria": "how to verify"
628
- }
629
- ]
630
- }
631
- </json_output>
632
- </output_format>
633
- </strategy_mode>
634
-
635
- <rules>
636
- - One action per task
637
- - Always end with inspector task
638
- - Group unrelated tasks (parallel)
639
- - Be specific about files and verification
640
- - Output MUST be valid JSON wrapped in <json_output> tags
641
- </rules>`,
642
- canWrite: false,
643
- canBash: false
644
- };
645
-
646
- // src/agents/subagents/builder.ts
647
- var builder = {
648
- id: AGENT_NAMES.BUILDER,
649
- description: "Builder - full-stack implementation specialist",
650
- systemPrompt: `<role>
651
- You are Builder. Write code that works.
652
- </role>
653
-
654
- ${REASONING_CONSTRAINTS}
655
-
656
- ${WORKFLOW}
657
-
658
- ${ANTI_PATTERNS}
282
+ If your reasoning collapses into gibberish, stop and output "ERROR: REASONING_COLLAPSE".
283
+ </constraints>
659
284
 
660
285
  <scalable_attention>
661
286
  - **Simple Fix (L1)**: Read file \u2192 Implement fix directly. Efficiency first.
@@ -718,11 +343,10 @@ var inspector = {
718
343
  You are Inspector. Prove failure or success with evidence.
719
344
  </role>
720
345
 
721
- ${REASONING_CONSTRAINTS}
722
-
723
- ${WORKFLOW}
346
+ <constraints>
724
347
 
725
- ${ANTI_PATTERNS}
348
+ If your reasoning collapses into gibberish, stop and output "ERROR: REASONING_COLLAPSE".
349
+ </constraints>
726
350
 
727
351
  <scalable_audit>
728
352
  - **Fast Track**: Verify syntax + quick logic check.
@@ -786,7 +410,10 @@ var recorder = {
786
410
  You are Recorder. Save and load work progress.
787
411
  </role>
788
412
 
789
- ${REASONING_CONSTRAINTS}
413
+ <constraints>
414
+
415
+ If your reasoning collapses into gibberish, stop and output "ERROR: REASONING_COLLAPSE".
416
+ </constraints>
790
417
 
791
418
  <purpose>
792
419
  Context can be lost between sessions. You save it to disk.
@@ -851,7 +478,7 @@ var AGENTS = {
851
478
  [AGENT_NAMES.RECORDER]: recorder
852
479
  };
853
480
 
854
- // src/core/tasks.ts
481
+ // src/core/orchestrator/task-graph.ts
855
482
  var TaskGraph = class _TaskGraph {
856
483
  tasks = /* @__PURE__ */ new Map();
857
484
  constructor(tasks) {
@@ -892,7 +519,7 @@ var TaskGraph = class _TaskGraph {
892
519
  const notCompleted = tasks.filter((t) => t.status !== "completed");
893
520
  let summary = "\u{1F4CB} **Mission Status**\n";
894
521
  if (completed.length > 0) {
895
- summary += `\u2705 Completed: ${completed.length} tasks (Hidden to save tokens)
522
+ summary += `\u2705 Completed: ${completed.length} tasks
896
523
  `;
897
524
  }
898
525
  for (const task of notCompleted) {
@@ -909,27 +536,17 @@ var TaskGraph = class _TaskGraph {
909
536
  try {
910
537
  const tasks = JSON.parse(json2);
911
538
  return new _TaskGraph(tasks);
912
- } catch (e) {
913
- console.error("Failed to parse TaskGraph JSON:", e);
539
+ } catch {
914
540
  return new _TaskGraph();
915
541
  }
916
542
  }
917
543
  };
918
544
 
919
- // src/constants/time.ts
920
- var TASK_TTL_MS = 30 * 60 * 1e3;
921
- var BACKGROUND_TASK_TIMEOUT_MS = 5 * 60 * 1e3;
922
- var SYNC_TASK_MAX_POLL_MS = 10 * 60 * 1e3;
923
- var MIN_STABILITY_MS = 5 * 1e3;
924
- var CLEANUP_DELAY_MS = 5 * 60 * 1e3;
925
- var MAX_TASK_RETRIES = 3;
926
-
927
- // src/core/state.ts
545
+ // src/core/orchestrator/state.ts
928
546
  var state = {
929
547
  missionActive: false,
930
548
  maxIterations: 1e3,
931
- // Effectively infinite - "Relentless" mode
932
- maxRetries: MAX_TASK_RETRIES,
549
+ maxRetries: 3,
933
550
  sessions: /* @__PURE__ */ new Map()
934
551
  };
935
552
 
@@ -13355,113 +12972,6 @@ function tool(input) {
13355
12972
  }
13356
12973
  tool.schema = external_exports;
13357
12974
 
13358
- // src/context/enforcer.ts
13359
- var ContextEnforcer = class {
13360
- requirements = /* @__PURE__ */ new Map([
13361
- ["builder", {
13362
- agent: "builder",
13363
- required: ["infra", "stack", "patterns", "files"],
13364
- optional: ["examples", "tests", "environment"]
13365
- }],
13366
- ["inspector", {
13367
- agent: "inspector",
13368
- required: ["infra", "verification_method", "changed_files"],
13369
- optional: ["test_results", "build_logs", "environment"]
13370
- }],
13371
- ["architect", {
13372
- agent: "architect",
13373
- required: ["mission", "scope", "complexity"],
13374
- optional: ["environment", "constraints", "patterns"]
13375
- }],
13376
- ["recorder", {
13377
- agent: "recorder",
13378
- required: [],
13379
- optional: ["mission", "progress", "context"]
13380
- }]
13381
- ]);
13382
- /**
13383
- * Validate context for agent delegation
13384
- */
13385
- validate(agent, context) {
13386
- const req = this.requirements.get(agent);
13387
- if (!req) {
13388
- return { valid: true, errors: [], warnings: [] };
13389
- }
13390
- const errors = [];
13391
- const warnings = [];
13392
- const contextLower = context.toLowerCase();
13393
- for (const key of req.required) {
13394
- if (!contextLower.includes(key.toLowerCase())) {
13395
- errors.push("Missing required context: " + key);
13396
- }
13397
- }
13398
- for (const key of req.optional) {
13399
- if (!contextLower.includes(key.toLowerCase())) {
13400
- warnings.push("Optional context missing: " + key);
13401
- }
13402
- }
13403
- return {
13404
- valid: errors.length === 0,
13405
- errors,
13406
- warnings
13407
- };
13408
- }
13409
- /**
13410
- * Get requirements for an agent
13411
- */
13412
- getRequirements(agent) {
13413
- return this.requirements.get(agent);
13414
- }
13415
- /**
13416
- * Add or update requirements for an agent
13417
- */
13418
- setRequirements(agent, requirements) {
13419
- this.requirements.set(agent, requirements);
13420
- }
13421
- /**
13422
- * Format validation result as user-friendly message
13423
- */
13424
- formatValidation(result) {
13425
- if (result.valid && result.warnings.length === 0) {
13426
- return "Valid context with all required information.";
13427
- }
13428
- let message = "";
13429
- if (result.errors.length > 0) {
13430
- message += "Errors:\n" + result.errors.map((e) => " \u274C " + e).join("\n") + "\n";
13431
- }
13432
- if (result.warnings.length > 0) {
13433
- message += "\nWarnings:\n" + result.warnings.map((w) => " \u26A0\uFE0F " + w).join("\n");
13434
- }
13435
- return message;
13436
- }
13437
- /**
13438
- * Check if context has minimal required information
13439
- */
13440
- hasMinimumContext(agent, context) {
13441
- const req = this.requirements.get(agent);
13442
- if (!req || req.required.length === 0) {
13443
- return true;
13444
- }
13445
- const presentCount = req.required.filter(
13446
- (key) => context.toLowerCase().includes(key.toLowerCase())
13447
- ).length;
13448
- return presentCount > 0;
13449
- }
13450
- /**
13451
- * Suggest missing context based on agent type
13452
- */
13453
- suggestContext(agent, context) {
13454
- const req = this.requirements.get(agent);
13455
- if (!req) {
13456
- return [];
13457
- }
13458
- return req.required.filter(
13459
- (key) => !context.toLowerCase().includes(key.toLowerCase())
13460
- );
13461
- }
13462
- };
13463
- var contextEnforcer = new ContextEnforcer();
13464
-
13465
12975
  // src/tools/callAgent.ts
13466
12976
  var AGENT_EMOJI = {
13467
12977
  [AGENT_NAMES.ARCHITECT]: "\u{1F3D7}\uFE0F",
@@ -13500,19 +13010,13 @@ var callAgentTool = tool({
13500
13010
  async execute(args) {
13501
13011
  const agentDef = AGENTS[args.agent];
13502
13012
  if (!agentDef) {
13503
- return "Error: Unknown agent: " + args.agent;
13013
+ return `\u274C Error: Unknown agent: ${args.agent}`;
13504
13014
  }
13505
13015
  const emoji3 = AGENT_EMOJI[args.agent] || "\u{1F916}";
13506
- if (args.context) {
13507
- const validation = contextEnforcer.validate(args.agent, args.context);
13508
- if (!validation.valid) {
13509
- return "Context Validation Failed:\n" + contextEnforcer.formatValidation(validation);
13510
- }
13511
- }
13512
13016
  const prompt = `
13513
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
13017
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
13514
13018
  ${emoji3} ${agentDef.id.toUpperCase()} :: ${agentDef.description}
13515
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
13019
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
13516
13020
 
13517
13021
  <system>
13518
13022
  ${agentDef.systemPrompt}
@@ -13522,7 +13026,9 @@ ${agentDef.systemPrompt}
13522
13026
  ${args.task}
13523
13027
  </task>
13524
13028
 
13525
- ${args.context ? "<context>\n" + args.context + "\n</context>" : ""}
13029
+ ${args.context ? `<context>
13030
+ ${args.context}
13031
+ </context>` : ""}
13526
13032
 
13527
13033
  <execution>
13528
13034
  Follow this pattern:
@@ -13547,10 +13053,6 @@ var COMMANDS = {
13547
13053
  You are Commander. Complete this mission. Never stop until 100% done.
13548
13054
  </role>
13549
13055
 
13550
- <constraints>
13551
- Reasoning MUST be in English for model stability. Final report in Korean.
13552
- </constraints>
13553
-
13554
13056
  <phase_1 name="MANDATORY_ENVIRONMENT_SCAN">
13555
13057
  Before any planning or coding, you MUST understand:
13556
13058
  1. INFRA: OS-native? Container? Docker-compose? Volume-mounted?
@@ -13768,255 +13270,72 @@ var globSearchTool = (directory) => tool({
13768
13270
  }
13769
13271
  });
13770
13272
  var mgrepTool = (directory) => tool({
13771
- description: `Search multiple patterns in parallel (high-performance).
13772
-
13773
- <purpose>
13774
- Search for multiple regex patterns simultaneously using Rust's parallel execution.
13775
- Much faster than running grep multiple times sequentially.
13776
- </purpose>
13777
-
13778
- <examples>
13779
- - patterns: ["useState", "useEffect", "useContext"] \u2192 Find all React hooks usage
13780
- - patterns: ["TODO", "FIXME", "HACK"] \u2192 Find all code annotations
13781
- - patterns: ["import.*lodash", "require.*lodash"] \u2192 Find all lodash imports
13782
- </examples>
13783
-
13784
- <output>
13785
- Returns matches grouped by pattern, with file paths and line numbers.
13786
- </output>`,
13787
- args: {
13788
- patterns: tool.schema.array(tool.schema.string()).describe("Array of regex patterns to search for"),
13789
- dir: tool.schema.string().optional().describe("Directory to search (defaults to project root)"),
13790
- max_results_per_pattern: tool.schema.number().optional().describe("Max results per pattern (default: 50)")
13791
- },
13792
- async execute(args) {
13793
- return callRustTool("mgrep", {
13794
- patterns: args.patterns,
13795
- directory: args.dir || directory,
13796
- max_results_per_pattern: args.max_results_per_pattern || 50
13797
- });
13798
- }
13799
- });
13800
-
13801
- // src/tools/git.ts
13802
- var gitBranchTool = (directory) => tool({
13803
- description: `Get Git branch information and status.
13804
-
13805
- <purpose>
13806
- Analyze current Git workspace context including:
13807
- - Current branch name
13808
- - All branches (local and remote)
13809
- - Branch relationships (upstream, ahead/behind)
13810
- - Staged, unstaged, and untracked files
13811
- - Recent commits
13812
- </purpose>
13813
-
13814
- <examples>
13815
- - Get current branch: Returns branch name and status
13816
- - List all branches: Shows local and remote branches
13817
- - Check status: Shows modified files
13818
- - Recent commits: Last 5 commits with file changes
13819
- </examples>
13820
-
13821
- <output>
13822
- Returns structured Git information for better code decisions.
13823
- </output>`,
13273
+ description: `Search multiple patterns (runs grep for each pattern).`,
13824
13274
  args: {
13825
- action: tool.schema.enum([
13826
- "current",
13827
- "list",
13828
- "status",
13829
- "diff",
13830
- "recent",
13831
- "all"
13832
- ]).describe("Action to perform: current, list, status, diff, recent, all"),
13833
- baseBranch: tool.schema.string().optional().describe("Base branch for comparison (e.g., 'main', 'develop'). Required for diff action.")
13275
+ patterns: tool.schema.array(tool.schema.string()).describe("Array of regex patterns"),
13276
+ dir: tool.schema.string().optional().describe("Directory (defaults to project root)")
13834
13277
  },
13835
13278
  async execute(args) {
13836
- const { action, baseBranch } = args;
13837
- try {
13838
- switch (action) {
13839
- case "current":
13840
- return await getCurrentBranch(directory);
13841
- case "list":
13842
- return await listBranches(directory);
13843
- case "status":
13844
- return await getGitStatus(directory);
13845
- case "diff":
13846
- return await getDiff(directory, baseBranch);
13847
- case "recent":
13848
- return await getRecentCommits(directory, 5);
13849
- case "all":
13850
- return await getAllInfo(directory);
13851
- default:
13852
- return await getCurrentBranch(directory);
13853
- }
13854
- } catch (error45) {
13855
- return "\u274C Git error: " + (error45 instanceof Error ? error45.message : String(error45));
13279
+ const results = {};
13280
+ const dir = args.dir || directory;
13281
+ for (const pattern of args.patterns) {
13282
+ const result = await callRustTool("grep_search", { pattern, directory: dir });
13283
+ results[pattern] = result;
13856
13284
  }
13285
+ return JSON.stringify(results, null, 2);
13857
13286
  }
13858
13287
  });
13859
- async function execGit(directory, args) {
13860
- const { execSync } = await import("child_process");
13861
- try {
13862
- return execSync("git " + args.join(" "), {
13863
- cwd: directory,
13864
- encoding: "utf-8",
13865
- stdio: ["ignore", "pipe", "pipe"],
13866
- maxBuffer: 10 * 1024 * 1024
13867
- // 10MB
13868
- }).toString();
13869
- } catch (error45) {
13870
- if (error45.status === 128) {
13871
- throw new Error("Not a git repository");
13872
- }
13873
- throw error45;
13874
- }
13875
- }
13876
- async function getAheadBehind(directory, branch) {
13877
- try {
13878
- const output = await execGit(directory, ["rev-list", "--left-right", "--count", branch + "...@{u}"]);
13879
- const parts = output.trim().split(/\s+/);
13880
- const ahead = parseInt(parts[0], 10);
13881
- const behind = parseInt(parts[1] || "0", 10);
13882
- const resultParts = [];
13883
- if (ahead > 0) resultParts.push(ahead + " ahead");
13884
- if (behind > 0) resultParts.push(behind + " behind");
13885
- return resultParts.length > 0 ? "| **Sync** | " + resultParts.join(", ") + " |" : "";
13886
- } catch {
13887
- return "";
13888
- }
13889
- }
13890
- async function getCurrentBranch(directory) {
13891
- const output = await execGit(directory, ["branch", "--show-current"]);
13892
- const current = output.trim() || "HEAD (detached)";
13893
- const upstream = await execGit(directory, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]).catch(() => "");
13894
- return "\u{1F33F} **Current Branch**: `" + current + "`\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n" + (upstream ? "| **Upstream** | `" + upstream + "` |" : "") + await getAheadBehind(directory, current);
13895
- }
13896
- async function listBranches(directory) {
13897
- const branches = await execGit(directory, ["branch", "-vv"]);
13898
- const branchList = branches.split("\n").filter((b) => b.trim()).map((b) => {
13899
- const isCurrent = b.startsWith("*");
13900
- const name = isCurrent ? b.substring(2).trim() : b.trim();
13901
- const parts = name.split(/\s+/);
13902
- const branchName = parts[0];
13903
- const upstream = parts[1] ? parts[1].match(/\[([^\]]+)\]/)?.[1] : void 0;
13904
- const icon = isCurrent ? "\u{1F33F}" : "\u{1F4C2}";
13905
- const status = isCurrent ? "(current)" : "";
13906
- const upstreamInfo = upstream ? "\u2192 " + upstream : "";
13907
- return icon + " `" + branchName + "` " + status + " " + upstreamInfo;
13908
- }).join("\n");
13909
- return "\u{1F33F} **All Branches**\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n" + branchList;
13910
- }
13911
- async function getGitStatus(directory) {
13912
- const status = await execGit(directory, ["status", "--porcelain"]);
13913
- const lines = status.split("\n").filter((l) => l.trim());
13914
- if (lines.length === 0) {
13915
- return "\u2705 Working directory clean";
13916
- }
13917
- const staged = [];
13918
- const unstaged = [];
13919
- const untracked = [];
13920
- const conflicted = [];
13921
- for (const line of lines) {
13922
- if (!line || line.length < 2) continue;
13923
- const statusCode = line.substring(0, 2);
13924
- const filename = line.substring(3);
13925
- if (statusCode === "??") {
13926
- untracked.push(filename);
13927
- } else if (statusCode.startsWith("U") || statusCode === "AA") {
13928
- conflicted.push(filename);
13929
- } else if (statusCode[0] !== " ") {
13930
- staged.push(filename);
13931
- }
13932
- if (statusCode[1] !== " " && statusCode[1] !== "?") {
13933
- unstaged.push(filename);
13934
- }
13935
- }
13936
- const current = await execGit(directory, ["branch", "--show-current"]).catch(() => "unknown");
13937
- let result = "\u{1F33F} **Branch**: `" + current + "`\n\n";
13938
- if (staged.length > 0) {
13939
- result += "\u2705 **Staged** (" + staged.length + ")\n" + staged.map((f) => " + " + f).join("\n") + "\n\n";
13940
- }
13941
- if (unstaged.length > 0) {
13942
- result += "\u{1F4DD} **Modified** (" + unstaged.length + ")\n" + unstaged.map((f) => " ~ " + f).join("\n") + "\n\n";
13943
- }
13944
- if (untracked.length > 0) {
13945
- result += "\u2795 **Untracked** (" + untracked.length + ")\n" + untracked.slice(0, 10).map((f) => " ? " + f).join("\n") + (untracked.length > 10 ? "\n ... and " + (untracked.length - 10) + " more" : "") + "\n\n";
13946
- }
13947
- if (conflicted.length > 0) {
13948
- result += "\u26A0\uFE0F **Conflicts** (" + conflicted.length + ")\n" + conflicted.map((f) => " ! " + f).join("\n") + "\n\n";
13949
- }
13950
- return result.trim();
13951
- }
13952
- async function getDiff(directory, baseBranch) {
13953
- const current = await execGit(directory, ["branch", "--show-current"]).catch(() => "HEAD");
13954
- const base = baseBranch || "main";
13955
- const diff = await execGit(directory, ["diff", base + "..." + current, "--stat"]);
13956
- if (!diff.trim()) {
13957
- return "\u2705 No differences between `" + current + "` and `" + base + "`";
13958
- }
13959
- const files = await execGit(directory, ["diff", base + "..." + current, "--name-only"]);
13960
- const fileList = files.split("\n").filter((f) => f.trim());
13961
- return "\u{1F4CA} **Diff**: `" + current + "` vs `" + base + "`\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n" + diff + "\n\n\u{1F4C1} **Changed Files** (" + fileList.length + "):\n" + fileList.map((f, i) => " " + (i + 1) + ". " + f).join("\n");
13962
- }
13963
- async function getRecentCommits(directory, count = 5) {
13964
- const log2 = await execGit(directory, [
13965
- "log",
13966
- "-" + count,
13967
- "--pretty=format:%H|%an|%ad|%s",
13968
- "--date=short",
13969
- "--name-only"
13970
- ]);
13971
- const blocks = log2.split("\n\n").filter((b) => b.trim());
13972
- const commits = [];
13973
- for (const block of blocks) {
13974
- const lines = block.split("\n").filter((l) => l.trim());
13975
- if (lines.length < 2) continue;
13976
- const [hash2, author, date5, message] = lines[0].split("|");
13977
- const files = lines.slice(1);
13978
- commits.push({
13979
- hash: hash2.substring(0, 7),
13980
- author,
13981
- date: date5,
13982
- message,
13983
- files
13984
- });
13985
- }
13986
- let result = "\u{1F4DC} **Recent Commits** (last " + commits.length + ")\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n";
13987
- for (const commit of commits) {
13988
- result += "\n\u{1F539} `" + commit.hash + "` - " + commit.message + "\n";
13989
- result += " \u{1F464} " + commit.author + " | \u{1F4C5} " + commit.date + "\n";
13990
- result += " \u{1F4C1} " + commit.files.length + " file" + (commit.files.length > 1 ? "s" : "") + "\n";
13991
- }
13992
- return result;
13993
- }
13994
- async function getAllInfo(directory) {
13995
- const current = await getCurrentBranch(directory);
13996
- const status = await getGitStatus(directory);
13997
- const recent = await getRecentCommits(directory, 3);
13998
- return "\u{1F50D} **Full Git Context**\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n" + current + "\n\n" + status + "\n\n" + recent;
13999
- }
14000
13288
 
14001
- // src/core/background.ts
13289
+ // src/core/commands/manager.ts
14002
13290
  import { spawn as spawn2 } from "child_process";
14003
13291
  import { randomBytes } from "crypto";
14004
- import { mkdirSync, readFileSync, writeFileSync, existsSync as existsSync3 } from "fs";
14005
- import { join as join2 } from "path";
14006
- import { homedir } from "os";
13292
+
13293
+ // src/shared/constants.ts
13294
+ var TIME = {
13295
+ SECOND: 1e3,
13296
+ MINUTE: 60 * 1e3,
13297
+ HOUR: 60 * 60 * 1e3
13298
+ };
13299
+ var ID_PREFIX = {
13300
+ /** Parallel agent task ID (e.g., task_a1b2c3d4) */
13301
+ TASK: "task_",
13302
+ /** Background command job ID (e.g., job_a1b2c3d4) */
13303
+ JOB: "job_",
13304
+ /** Session ID prefix */
13305
+ SESSION: "session_"
13306
+ };
13307
+ var PARALLEL_TASK = {
13308
+ TTL_MS: 30 * TIME.MINUTE,
13309
+ CLEANUP_DELAY_MS: 5 * TIME.MINUTE,
13310
+ MIN_STABILITY_MS: 5 * TIME.SECOND,
13311
+ POLL_INTERVAL_MS: 2e3,
13312
+ DEFAULT_CONCURRENCY: 3,
13313
+ MAX_CONCURRENCY: 10
13314
+ };
13315
+ var BACKGROUND_TASK = {
13316
+ DEFAULT_TIMEOUT_MS: 5 * TIME.MINUTE,
13317
+ MAX_OUTPUT_LENGTH: 1e4
13318
+ };
13319
+ var STATUS_EMOJI = {
13320
+ pending: "\u23F3",
13321
+ running: "\u{1F504}",
13322
+ completed: "\u2705",
13323
+ done: "\u2705",
13324
+ error: "\u274C",
13325
+ timeout: "\u23F0",
13326
+ cancelled: "\u{1F6AB}"
13327
+ };
13328
+ function getStatusEmoji(status) {
13329
+ return STATUS_EMOJI[status] ?? "\u2753";
13330
+ }
13331
+
13332
+ // src/core/commands/manager.ts
14007
13333
  var BackgroundTaskManager = class _BackgroundTaskManager {
14008
13334
  static _instance;
14009
13335
  tasks = /* @__PURE__ */ new Map();
14010
- debugMode = true;
14011
- // Enable debug mode
14012
- storageDir;
14013
- storageFile;
14014
- monitoringInterval;
13336
+ debugMode = process.env.DEBUG_BG_TASK === "true";
13337
+ // Disabled by default
14015
13338
  constructor() {
14016
- this.storageDir = join2(homedir(), ".opencode-orchestrator");
14017
- this.storageFile = join2(this.storageDir, "tasks.json");
14018
- this.loadFromDisk();
14019
- this.startMonitoring();
14020
13339
  }
14021
13340
  static get instance() {
14022
13341
  if (!_BackgroundTaskManager._instance) {
@@ -14024,144 +13343,15 @@ var BackgroundTaskManager = class _BackgroundTaskManager {
14024
13343
  }
14025
13344
  return _BackgroundTaskManager._instance;
14026
13345
  }
14027
- /**
14028
- * Generate a unique task ID in the format job_xxxxxxxx
14029
- */
14030
13346
  generateId() {
14031
- const hex3 = randomBytes(4).toString("hex");
14032
- return `job_${hex3}`;
14033
- }
14034
- /**
14035
- * Ensure storage directory exists
14036
- */
14037
- ensureStorageDir() {
14038
- if (!existsSync3(this.storageDir)) {
14039
- mkdirSync(this.storageDir, { recursive: true });
14040
- this.debug("system", `Created storage directory: ${this.storageDir}`);
14041
- }
14042
- }
14043
- /**
14044
- * Load tasks from disk on startup
14045
- */
14046
- loadFromDisk() {
14047
- this.ensureStorageDir();
14048
- if (!existsSync3(this.storageFile)) {
14049
- this.debug("system", "No existing task data on disk");
14050
- return;
14051
- }
14052
- try {
14053
- const data = readFileSync(this.storageFile, "utf-8");
14054
- const tasksData = JSON.parse(data);
14055
- for (const [id, taskData] of Object.entries(tasksData)) {
14056
- const task = taskData;
14057
- task.process = void 0;
14058
- if (task.status === "running") {
14059
- task.status = "error";
14060
- task.errorOutput += "\n[Process lost on restart]";
14061
- task.endTime = Date.now();
14062
- task.exitCode = null;
14063
- }
14064
- this.tasks.set(id, task);
14065
- }
14066
- this.debug("system", `Loaded ${this.tasks.size} tasks from disk`);
14067
- } catch (error45) {
14068
- this.debug(
14069
- "system",
14070
- `Failed to load tasks: ${error45 instanceof Error ? error45.message : String(error45)}`
14071
- );
14072
- }
13347
+ return `${ID_PREFIX.JOB}${randomBytes(4).toString("hex")}`;
14073
13348
  }
14074
- /**
14075
- * Save tasks to disk
14076
- */
14077
- saveToDisk() {
14078
- this.ensureStorageDir();
14079
- try {
14080
- const tasksData = {};
14081
- for (const [id, task] of this.tasks.entries()) {
14082
- tasksData[id] = task;
14083
- }
14084
- writeFileSync(
14085
- this.storageFile,
14086
- JSON.stringify(tasksData, null, 2),
14087
- "utf-8"
14088
- );
14089
- } catch (error45) {
14090
- this.debug(
14091
- "system",
14092
- `Failed to save tasks: ${error45 instanceof Error ? error45.message : String(error45)}`
14093
- );
13349
+ debug(taskId, message) {
13350
+ if (this.debugMode) {
13351
+ const ts = (/* @__PURE__ */ new Date()).toISOString().substring(11, 23);
13352
+ console.log(`[BG ${ts}] ${taskId}: ${message}`);
14094
13353
  }
14095
13354
  }
14096
- /**
14097
- * Start periodic monitoring of running processes
14098
- */
14099
- startMonitoring() {
14100
- const MONITOR_INTERVAL_MS = 5e3;
14101
- this.monitoringInterval = setInterval(() => {
14102
- this.monitorRunningProcesses();
14103
- }, MONITOR_INTERVAL_MS);
14104
- if (this.monitoringInterval) {
14105
- this.monitoringInterval.unref();
14106
- }
14107
- }
14108
- /**
14109
- * Stop monitoring
14110
- */
14111
- stopMonitoring() {
14112
- if (this.monitoringInterval) {
14113
- clearInterval(this.monitoringInterval);
14114
- this.monitoringInterval = void 0;
14115
- }
14116
- }
14117
- /**
14118
- * Monitor running processes and detect zombie processes
14119
- */
14120
- monitorRunningProcesses() {
14121
- const now = Date.now();
14122
- let hasRunningTasks = false;
14123
- for (const [id, task] of this.tasks.entries()) {
14124
- if (task.status !== "running") continue;
14125
- hasRunningTasks = true;
14126
- if (task.process && task.process.pid) {
14127
- const pid = task.process.pid;
14128
- try {
14129
- process.kill(pid, 0);
14130
- } catch (error45) {
14131
- task.status = "error";
14132
- task.errorOutput += `
14133
- Process disappeared (PID ${pid})`;
14134
- task.endTime = Date.now();
14135
- task.exitCode = null;
14136
- task.process = void 0;
14137
- this.saveToDisk();
14138
- this.debug(id, `Process dead (PID ${pid}), marked as error`);
14139
- }
14140
- } else if (task.process) {
14141
- task.status = "error";
14142
- task.errorOutput += "\nProcess reference lost";
14143
- task.endTime = Date.now();
14144
- task.exitCode = null;
14145
- this.saveToDisk();
14146
- this.debug(id, "Process reference lost, marked as error");
14147
- }
14148
- }
14149
- if (!hasRunningTasks && this.monitoringInterval) {
14150
- this.stopMonitoring();
14151
- }
14152
- }
14153
- /**
14154
- * Debug logging helper
14155
- */
14156
- debug(taskId, message) {
14157
- if (this.debugMode) {
14158
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().substring(11, 23);
14159
- console.log(`[BG-DEBUG ${timestamp}] ${taskId}: ${message}`);
14160
- }
14161
- }
14162
- /**
14163
- * Run a command in the background
14164
- */
14165
13355
  run(options) {
14166
13356
  const id = this.generateId();
14167
13357
  const { command, cwd = process.cwd(), timeout = 3e5, label } = options;
@@ -14182,8 +13372,7 @@ Process disappeared (PID ${pid})`;
14182
13372
  timeout
14183
13373
  };
14184
13374
  this.tasks.set(id, task);
14185
- this.saveToDisk();
14186
- this.debug(id, `Starting: ${command} (cwd: ${cwd})`);
13375
+ this.debug(id, `Starting: ${command}`);
14187
13376
  try {
14188
13377
  const proc = spawn2(shell, task.args, {
14189
13378
  cwd,
@@ -14192,29 +13381,17 @@ Process disappeared (PID ${pid})`;
14192
13381
  });
14193
13382
  task.process = proc;
14194
13383
  proc.stdout?.on("data", (data) => {
14195
- const text = data.toString();
14196
- task.output += text;
14197
- this.debug(
14198
- id,
14199
- `stdout: ${text.substring(0, 100)}${text.length > 100 ? "..." : ""}`
14200
- );
13384
+ task.output += data.toString();
14201
13385
  });
14202
13386
  proc.stderr?.on("data", (data) => {
14203
- const text = data.toString();
14204
- task.errorOutput += text;
14205
- this.debug(
14206
- id,
14207
- `stderr: ${text.substring(0, 100)}${text.length > 100 ? "..." : ""}`
14208
- );
13387
+ task.errorOutput += data.toString();
14209
13388
  });
14210
13389
  proc.on("close", (code) => {
14211
13390
  task.exitCode = code;
14212
13391
  task.endTime = Date.now();
14213
13392
  task.status = code === 0 ? "done" : "error";
14214
13393
  task.process = void 0;
14215
- this.saveToDisk();
14216
- const duration3 = ((task.endTime - task.startTime) / 1e3).toFixed(2);
14217
- this.debug(id, `Completed with code ${code} in ${duration3}s`);
13394
+ this.debug(id, `Done (code=${code})`);
14218
13395
  });
14219
13396
  proc.on("error", (err) => {
14220
13397
  task.status = "error";
@@ -14222,68 +13399,31 @@ Process disappeared (PID ${pid})`;
14222
13399
  Process error: ${err.message}`;
14223
13400
  task.endTime = Date.now();
14224
13401
  task.process = void 0;
14225
- this.saveToDisk();
14226
- this.debug(id, `Error: ${err.message}`);
14227
13402
  });
14228
13403
  setTimeout(() => {
14229
13404
  if (task.status === "running" && task.process) {
14230
- this.debug(id, `Timeout after ${timeout}ms, killing process`);
14231
13405
  task.process.kill("SIGKILL");
14232
13406
  task.status = "timeout";
14233
13407
  task.endTime = Date.now();
14234
- task.errorOutput += `
14235
- Process killed: timeout after ${timeout}ms`;
14236
- this.saveToDisk();
13408
+ this.debug(id, "Timeout");
14237
13409
  }
14238
13410
  }, timeout);
14239
13411
  } catch (err) {
14240
13412
  task.status = "error";
14241
- task.errorOutput = `Failed to spawn: ${err instanceof Error ? err.message : String(err)}`;
13413
+ task.errorOutput = `Spawn failed: ${err instanceof Error ? err.message : String(err)}`;
14242
13414
  task.endTime = Date.now();
14243
- this.saveToDisk();
14244
- this.debug(id, `Spawn failed: ${task.errorOutput}`);
14245
13415
  }
14246
13416
  return task;
14247
13417
  }
14248
- /**
14249
- * Get task by ID
14250
- */
14251
13418
  get(taskId) {
14252
13419
  return this.tasks.get(taskId);
14253
13420
  }
14254
- /**
14255
- * Get all tasks
14256
- */
14257
13421
  getAll() {
14258
13422
  return Array.from(this.tasks.values());
14259
13423
  }
14260
- /**
14261
- * Get tasks by status
14262
- */
14263
13424
  getByStatus(status) {
14264
13425
  return this.getAll().filter((t) => t.status === status);
14265
13426
  }
14266
- /**
14267
- * Clean up tasks by session ID
14268
- */
14269
- cleanupBySession(sessionID) {
14270
- let count = 0;
14271
- for (const [id, task] of this.tasks) {
14272
- if (task.sessionID === sessionID) {
14273
- if (task.process && task.status === "running") {
14274
- task.process.kill("SIGKILL");
14275
- }
14276
- this.tasks.delete(id);
14277
- count++;
14278
- this.debug(id, `Cleaned up for session ${sessionID}`);
14279
- }
14280
- }
14281
- this.saveToDisk();
14282
- return count;
14283
- }
14284
- /**
14285
- * Clear completed/failed tasks
14286
- */
14287
13427
  clearCompleted() {
14288
13428
  let count = 0;
14289
13429
  for (const [id, task] of this.tasks) {
@@ -14292,12 +13432,8 @@ Process killed: timeout after ${timeout}ms`;
14292
13432
  count++;
14293
13433
  }
14294
13434
  }
14295
- this.saveToDisk();
14296
13435
  return count;
14297
13436
  }
14298
- /**
14299
- * Kill a running task
14300
- */
14301
13437
  kill(taskId) {
14302
13438
  const task = this.tasks.get(taskId);
14303
13439
  if (task?.process) {
@@ -14305,116 +13441,60 @@ Process killed: timeout after ${timeout}ms`;
14305
13441
  task.status = "error";
14306
13442
  task.errorOutput += "\nKilled by user";
14307
13443
  task.endTime = Date.now();
14308
- this.saveToDisk();
14309
- this.debug(taskId, "Killed by user");
14310
13444
  return true;
14311
13445
  }
14312
13446
  return false;
14313
13447
  }
14314
- /**
14315
- * Format duration for display
14316
- */
14317
13448
  formatDuration(task) {
14318
13449
  const end = task.endTime || Date.now();
14319
13450
  const seconds = (end - task.startTime) / 1e3;
14320
- if (seconds < 60) {
14321
- return `${seconds.toFixed(1)}s`;
14322
- }
14323
- const minutes = Math.floor(seconds / 60);
14324
- const remainingSeconds = seconds % 60;
14325
- return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
13451
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
13452
+ return `${Math.floor(seconds / 60)}m ${(seconds % 60).toFixed(0)}s`;
14326
13453
  }
14327
- /**
14328
- * Get status emoji
14329
- */
14330
13454
  getStatusEmoji(status) {
14331
- switch (status) {
14332
- case "pending":
14333
- return "\u23F8\uFE0F";
14334
- case "running":
14335
- return "\u23F3";
14336
- case "done":
14337
- return "\u2705";
14338
- case "error":
14339
- return "\u274C";
14340
- case "timeout":
14341
- return "\u23F0";
14342
- default:
14343
- return "\u2753";
14344
- }
13455
+ return getStatusEmoji(status);
14345
13456
  }
14346
13457
  };
14347
13458
  var backgroundTaskManager = BackgroundTaskManager.instance;
14348
13459
 
14349
- // src/tools/background.ts
13460
+ // src/tools/background-cmd/run.ts
14350
13461
  var runBackgroundTool = tool({
14351
13462
  description: `Run a shell command in the background and get a task ID.
14352
13463
 
14353
13464
  <purpose>
14354
- Execute long-running commands (builds, tests, etc.) without blocking.
14355
- The command runs asynchronously - use check_background to get results.
14356
- </purpose>
14357
-
14358
- <examples>
14359
- - "npm run build" \u2192 Build project in background
14360
- - "cargo test" \u2192 Run Rust tests
14361
- - "sleep 10 && echo done" \u2192 Delayed execution
14362
- </examples>
14363
-
14364
- <flow>
14365
- 1. Call run_background with command
14366
- 2. Get task ID immediately (e.g., job_a1b2c3d4)
14367
- 3. Continue other work
14368
- 4. Call check_background with task ID to get results
14369
- </flow>`,
13465
+ Execute long-running commands (builds, tests) without blocking.
13466
+ Use check_background to get results.
13467
+ </purpose>`,
14370
13468
  args: {
14371
13469
  command: tool.schema.string().describe("Shell command to execute"),
14372
- cwd: tool.schema.string().optional().describe("Working directory (default: project root)"),
14373
- timeout: tool.schema.number().optional().describe("Timeout in milliseconds (default: 300000 = 5 min)"),
14374
- label: tool.schema.string().optional().describe("Human-readable label for this task"),
14375
- sessionID: tool.schema.string().optional().describe("Session ID for automatic cleanup on session deletion")
13470
+ cwd: tool.schema.string().optional().describe("Working directory"),
13471
+ timeout: tool.schema.number().optional().describe("Timeout in ms (default: 300000)"),
13472
+ label: tool.schema.string().optional().describe("Task label")
14376
13473
  },
14377
13474
  async execute(args) {
14378
- const { command, cwd, timeout, label, sessionID } = args;
13475
+ const { command, cwd, timeout, label } = args;
14379
13476
  const task = backgroundTaskManager.run({
14380
13477
  command,
14381
13478
  cwd: cwd || process.cwd(),
14382
13479
  timeout: timeout || 3e5,
14383
- label,
14384
- sessionID
13480
+ label
14385
13481
  });
14386
13482
  const displayLabel = label ? ` (${label})` : "";
14387
13483
  return `\u{1F680} **Background Task Started**${displayLabel}
14388
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
14389
- | Property | Value |
14390
- |----------|-------|
14391
- | **Task ID** | \`${task.id}\` |
14392
- | **Command** | \`${command}\` |
14393
- | **Status** | ${backgroundTaskManager.getStatusEmoji(task.status)} ${task.status} |
14394
- | **Working Dir** | ${task.cwd} |
14395
- | **Timeout** | ${(task.timeout / 1e3).toFixed(0)}s |
14396
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
13484
+ | Task ID | \`${task.id}\` |
13485
+ | Command | \`${command}\` |
13486
+ | Status | ${backgroundTaskManager.getStatusEmoji(task.status)} ${task.status} |
14397
13487
 
14398
- \u{1F4CC} **Next Step**: Use \`check_background\` with task ID \`${task.id}\` to get results.`;
13488
+ \u{1F4CC} Use \`check_background({ taskId: "${task.id}" })\` to get results.`;
14399
13489
  }
14400
13490
  });
14401
- var checkBackgroundTool = tool({
14402
- description: `Check the status and output of a background task.
14403
-
14404
- <purpose>
14405
- Retrieve the current status and output of a previously started background task.
14406
- Use this after run_background to get results.
14407
- </purpose>
14408
13491
 
14409
- <output_includes>
14410
- - Status: running/done/error/timeout
14411
- - Exit code (if completed)
14412
- - Duration
14413
- - Full output (stdout + stderr)
14414
- </output_includes>`,
13492
+ // src/tools/background-cmd/check.ts
13493
+ var checkBackgroundTool = tool({
13494
+ description: `Check the status and output of a background task.`,
14415
13495
  args: {
14416
- taskId: tool.schema.string().describe("Task ID from run_background (e.g., job_a1b2c3d4)"),
14417
- tailLines: tool.schema.number().optional().describe("Limit output to last N lines (default: show all)")
13496
+ taskId: tool.schema.string().describe("Task ID from run_background"),
13497
+ tailLines: tool.schema.number().optional().describe("Limit output to last N lines")
14418
13498
  },
14419
13499
  async execute(args) {
14420
13500
  const { taskId, tailLines } = args;
@@ -14427,7 +13507,7 @@ Use this after run_background to get results.
14427
13507
  const taskList = allTasks.map((t) => `- \`${t.id}\`: ${t.command.substring(0, 30)}...`).join("\n");
14428
13508
  return `\u274C Task \`${taskId}\` not found.
14429
13509
 
14430
- **Available tasks:**
13510
+ **Available:**
14431
13511
  ${taskList}`;
14432
13512
  }
14433
13513
  const duration3 = backgroundTaskManager.formatDuration(task);
@@ -14435,64 +13515,41 @@ ${taskList}`;
14435
13515
  let output = task.output;
14436
13516
  let stderr = task.errorOutput;
14437
13517
  if (tailLines && tailLines > 0) {
14438
- const outputLines = output.split("\n");
14439
- const stderrLines = stderr.split("\n");
14440
- output = outputLines.slice(-tailLines).join("\n");
14441
- stderr = stderrLines.slice(-tailLines).join("\n");
13518
+ output = output.split("\n").slice(-tailLines).join("\n");
13519
+ stderr = stderr.split("\n").slice(-tailLines).join("\n");
14442
13520
  }
14443
13521
  const maxLen = 1e4;
14444
- if (output.length > maxLen) {
14445
- output = `[...truncated ${output.length - maxLen} chars...]
14446
- ` + output.substring(output.length - maxLen);
14447
- }
14448
- if (stderr.length > maxLen) {
14449
- stderr = `[...truncated ${stderr.length - maxLen} chars...]
14450
- ` + stderr.substring(stderr.length - maxLen);
14451
- }
14452
- const labelDisplay = task.label ? ` (${task.label})` : "";
14453
- let result = `${statusEmoji} **Task ${task.id}**${labelDisplay}
14454
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
14455
- | Property | Value |
14456
- |----------|-------|
14457
- | **Command** | \`${task.command}\` |
14458
- | **Status** | ${statusEmoji} **${task.status.toUpperCase()}** |
14459
- | **Duration** | ${duration3}${task.status === "running" ? " (ongoing)" : ""} |
14460
- ${task.exitCode !== null ? `| **Exit Code** | ${task.exitCode} |` : ""}
14461
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`;
14462
- if (output.trim()) {
14463
- result += `
14464
-
14465
- \u{1F4E4} **Output (stdout)**:
13522
+ if (output.length > maxLen) output = `[...truncated...]\\n` + output.slice(-maxLen);
13523
+ if (stderr.length > maxLen) stderr = `[...truncated...]\\n` + stderr.slice(-maxLen);
13524
+ let result = `${statusEmoji} **Task ${task.id}**${task.label ? ` (${task.label})` : ""}
13525
+ | Command | \`${task.command}\` |
13526
+ | Status | ${statusEmoji} **${task.status.toUpperCase()}** |
13527
+ | Duration | ${duration3}${task.status === "running" ? " (ongoing)" : ""} |
13528
+ ${task.exitCode !== null ? `| Exit Code | ${task.exitCode} |` : ""}`;
13529
+ if (output.trim()) result += `
13530
+
13531
+ \u{1F4E4} **stdout:**
14466
13532
  \`\`\`
14467
13533
  ${output.trim()}
14468
13534
  \`\`\``;
14469
- }
14470
- if (stderr.trim()) {
14471
- result += `
13535
+ if (stderr.trim()) result += `
14472
13536
 
14473
- \u26A0\uFE0F **Errors (stderr)**:
13537
+ \u26A0\uFE0F **stderr:**
14474
13538
  \`\`\`
14475
13539
  ${stderr.trim()}
14476
13540
  \`\`\``;
14477
- }
14478
- if (task.status === "running") {
14479
- result += `
13541
+ if (task.status === "running") result += `
14480
13542
 
14481
- \u23F3 Task still running... Check again later with:
14482
- \`check_background({ taskId: "${task.id}" })\``;
14483
- }
13543
+ \u23F3 Still running... check again.`;
14484
13544
  return result;
14485
13545
  }
14486
13546
  });
14487
- var listBackgroundTool = tool({
14488
- description: `List all background tasks and their current status.
14489
13547
 
14490
- <purpose>
14491
- Get an overview of all running and completed background tasks.
14492
- Useful to check what's in progress before starting new tasks.
14493
- </purpose>`,
13548
+ // src/tools/background-cmd/list.ts
13549
+ var listBackgroundTool = tool({
13550
+ description: `List all background tasks and their status.`,
14494
13551
  args: {
14495
- status: tool.schema.enum(["all", "running", "done", "error"]).optional().describe("Filter by status (default: all)")
13552
+ status: tool.schema.enum(["all", "running", "done", "error"]).optional().describe("Filter by status")
14496
13553
  },
14497
13554
  async execute(args) {
14498
13555
  const { status = "all" } = args;
@@ -14503,344 +13560,102 @@ Useful to check what's in progress before starting new tasks.
14503
13560
  tasks = backgroundTaskManager.getByStatus(status);
14504
13561
  }
14505
13562
  if (tasks.length === 0) {
14506
- return `\u{1F4CB} **No background tasks** ${status !== "all" ? `with status "${status}"` : ""}
14507
-
14508
- Use \`run_background\` to start a new background task.`;
13563
+ return `\u{1F4CB} No background tasks${status !== "all" ? ` with status "${status}"` : ""}`;
14509
13564
  }
14510
13565
  tasks.sort((a, b) => b.startTime - a.startTime);
14511
- const rows = tasks.map((task) => {
14512
- const emoji3 = backgroundTaskManager.getStatusEmoji(task.status);
14513
- const duration3 = backgroundTaskManager.formatDuration(task);
14514
- const cmdShort = task.command.length > 25 ? task.command.substring(0, 22) + "..." : task.command;
14515
- const labelPart = task.label ? ` [${task.label}]` : "";
14516
- return `| \`${task.id}\` | ${emoji3} ${task.status.padEnd(7)} | ${cmdShort.padEnd(25)}${labelPart} | ${duration3.padStart(8)} |`;
13566
+ const rows = tasks.map((t) => {
13567
+ const emoji3 = backgroundTaskManager.getStatusEmoji(t.status);
13568
+ const cmd = t.command.length > 25 ? t.command.slice(0, 22) + "..." : t.command;
13569
+ const label = t.label ? ` [${t.label}]` : "";
13570
+ return `| \`${t.id}\` | ${emoji3} ${t.status} | ${cmd}${label} | ${backgroundTaskManager.formatDuration(t)} |`;
14517
13571
  }).join("\n");
14518
- const runningCount = tasks.filter((t) => t.status === "running").length;
14519
- const doneCount = tasks.filter((t) => t.status === "done").length;
14520
- const errorCount = tasks.filter((t) => t.status === "error" || t.status === "timeout").length;
14521
- return `\u{1F4CB} **Background Tasks** (${tasks.length} total)
14522
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
14523
- | \u23F3 Running: ${runningCount} | \u2705 Done: ${doneCount} | \u274C Error/Timeout: ${errorCount} |
14524
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
13572
+ const running = tasks.filter((t) => t.status === "running").length;
13573
+ const done = tasks.filter((t) => t.status === "done").length;
13574
+ const error45 = tasks.filter((t) => t.status === "error" || t.status === "timeout").length;
13575
+ return `\u{1F4CB} **Background Tasks** (${tasks.length})
13576
+ \u23F3 Running: ${running} | \u2705 Done: ${done} | \u274C Error: ${error45}
14525
13577
 
14526
- | Task ID | Status | Command | Duration |
14527
- |---------|--------|---------|----------|
14528
- ${rows}
14529
-
14530
- \u{1F4A1} Use \`check_background({ taskId: "job_xxxxx" })\` to see full output.`;
13578
+ | ID | Status | Command | Duration |
13579
+ |----|--------|---------|----------|
13580
+ ${rows}`;
14531
13581
  }
14532
13582
  });
14533
- var killBackgroundTool = tool({
14534
- description: `Kill a running background task.
14535
13583
 
14536
- <purpose>
14537
- Stop a background task that is taking too long or no longer needed.
14538
- </purpose>`,
13584
+ // src/tools/background-cmd/kill.ts
13585
+ var killBackgroundTool = tool({
13586
+ description: `Kill a running background task.`,
14539
13587
  args: {
14540
- taskId: tool.schema.string().describe("Task ID to kill (e.g., job_a1b2c3d4)")
13588
+ taskId: tool.schema.string().describe("Task ID to kill")
14541
13589
  },
14542
13590
  async execute(args) {
14543
13591
  const { taskId } = args;
14544
13592
  const task = backgroundTaskManager.get(taskId);
14545
- if (!task) {
14546
- return `\u274C Task \`${taskId}\` not found.`;
14547
- }
14548
- if (task.status !== "running") {
14549
- return `\u26A0\uFE0F Task \`${taskId}\` is not running (status: ${task.status}).`;
14550
- }
13593
+ if (!task) return `\u274C Task \`${taskId}\` not found.`;
13594
+ if (task.status !== "running") return `\u26A0\uFE0F Task \`${taskId}\` is not running (${task.status}).`;
14551
13595
  const killed = backgroundTaskManager.kill(taskId);
14552
13596
  if (killed) {
14553
- return `\u{1F6D1} Task \`${taskId}\` has been killed.
13597
+ return `\u{1F6D1} Task \`${taskId}\` killed.
14554
13598
  Command: \`${task.command}\`
14555
- Duration before kill: ${backgroundTaskManager.formatDuration(task)}`;
13599
+ Duration: ${backgroundTaskManager.formatDuration(task)}`;
14556
13600
  }
14557
- return `\u26A0\uFE0F Could not kill task \`${taskId}\`. It may have already finished.`;
13601
+ return `\u26A0\uFE0F Could not kill task \`${taskId}\`.`;
14558
13602
  }
14559
13603
  });
14560
13604
 
14561
- // src/core/config.ts
14562
- var DEFAULT_PARALLEL_AGENT = {
14563
- taskTtlMs: 30 * 60 * 1e3,
14564
- // 30 minutes
14565
- cleanupDelayMs: 5 * 60 * 1e3,
14566
- // 5 minutes
14567
- minStabilityMs: 5 * 1e3,
14568
- // 5 seconds
14569
- pollIntervalMs: 2 * 1e3,
14570
- // 2 seconds
14571
- defaultConcurrency: 3,
14572
- maxConcurrency: 10,
14573
- enableDebug: false,
14574
- enableDetailedLogs: false
14575
- };
14576
- var DEFAULT_BACKGROUND_TASK = {
14577
- monitorIntervalMs: 5 * 1e3,
14578
- // 5 seconds
14579
- storageDir: process.env.OPENCODE_ORCHESTRATOR_DIR || `${__require("os").homedir()}/.opencode-orchestrator`,
14580
- defaultTimeoutMs: 5 * 60 * 1e3,
14581
- // 5 minutes
14582
- maxCompletedTasksToKeep: 100,
14583
- enableDebug: true
14584
- };
14585
- var DEFAULT_SESSION = {
14586
- defaultMaxSteps: 50,
14587
- taskCommandMaxSteps: 100
13605
+ // src/core/agents/concurrency.ts
13606
+ var DEBUG = process.env.DEBUG_PARALLEL_AGENT === "true";
13607
+ var log = (...args) => {
13608
+ if (DEBUG) console.log("[concurrency]", ...args);
14588
13609
  };
14589
- var ConfigManager = class _ConfigManager {
14590
- static _instance;
14591
- parallelAgentConfig;
14592
- backgroundTaskConfig;
14593
- sessionConfig;
14594
- constructor() {
14595
- this.parallelAgentConfig = this.loadParallelAgentConfig();
14596
- this.backgroundTaskConfig = this.loadBackgroundTaskConfig();
14597
- this.sessionConfig = this.loadSessionConfig();
14598
- }
14599
- static getInstance() {
14600
- if (!_ConfigManager._instance) {
14601
- _ConfigManager._instance = new _ConfigManager();
14602
- }
14603
- return _ConfigManager._instance;
14604
- }
14605
- // ------------------------------------------------------------------------
14606
- // Parallel Agent Config
14607
- // ------------------------------------------------------------------------
14608
- loadParallelAgentConfig() {
14609
- return {
14610
- ...DEFAULT_PARALLEL_AGENT,
14611
- taskTtlMs: this.parseEnvInt(
14612
- "OPENCODE_TASK_TTL_MS",
14613
- DEFAULT_PARALLEL_AGENT.taskTtlMs
14614
- ),
14615
- cleanupDelayMs: this.parseEnvInt(
14616
- "OPENCODE_CLEANUP_DELAY_MS",
14617
- DEFAULT_PARALLEL_AGENT.cleanupDelayMs
14618
- ),
14619
- minStabilityMs: this.parseEnvInt(
14620
- "OPENCODE_MIN_STABILITY_MS",
14621
- DEFAULT_PARALLEL_AGENT.minStabilityMs
14622
- ),
14623
- pollIntervalMs: this.parseEnvInt(
14624
- "OPENCODE_POLL_INTERVAL_MS",
14625
- DEFAULT_PARALLEL_AGENT.pollIntervalMs
14626
- ),
14627
- defaultConcurrency: this.parseEnvInt(
14628
- "OPENCODE_DEFAULT_CONCURRENCY",
14629
- DEFAULT_PARALLEL_AGENT.defaultConcurrency
14630
- ),
14631
- maxConcurrency: this.parseEnvInt(
14632
- "OPENCODE_MAX_CONCURRENCY",
14633
- DEFAULT_PARALLEL_AGENT.maxConcurrency
14634
- ),
14635
- enableDebug: process.env.OPENCODE_DEBUG_PARALLEL === "true",
14636
- enableDetailedLogs: process.env.OPENCODE_DETAILED_LOGS === "true"
14637
- };
14638
- }
14639
- getParallelAgentConfig() {
14640
- return this.parallelAgentConfig;
14641
- }
14642
- // ------------------------------------------------------------------------
14643
- // Background Task Config
14644
- // ------------------------------------------------------------------------
14645
- loadBackgroundTaskConfig() {
14646
- return {
14647
- ...DEFAULT_BACKGROUND_TASK,
14648
- monitorIntervalMs: this.parseEnvInt(
14649
- "OPENCODE_MONITOR_INTERVAL_MS",
14650
- DEFAULT_BACKGROUND_TASK.monitorIntervalMs
14651
- ),
14652
- defaultTimeoutMs: this.parseEnvInt(
14653
- "OPENCODE_DEFAULT_TIMEOUT_MS",
14654
- DEFAULT_BACKGROUND_TASK.defaultTimeoutMs
14655
- ),
14656
- maxCompletedTasksToKeep: this.parseEnvInt(
14657
- "OPENCODE_MAX_COMPLETED_TASKS",
14658
- DEFAULT_BACKGROUND_TASK.maxCompletedTasksToKeep
14659
- ),
14660
- enableDebug: process.env.OPENCODE_DEBUG_BACKGROUND === "true"
14661
- };
14662
- }
14663
- getBackgroundTaskConfig() {
14664
- return this.backgroundTaskConfig;
14665
- }
14666
- // ------------------------------------------------------------------------
14667
- // Session Config
14668
- // ------------------------------------------------------------------------
14669
- loadSessionConfig() {
14670
- return {
14671
- ...DEFAULT_SESSION,
14672
- defaultMaxSteps: this.parseEnvInt(
14673
- "OPENCODE_DEFAULT_MAX_STEPS",
14674
- DEFAULT_SESSION.defaultMaxSteps
14675
- ),
14676
- taskCommandMaxSteps: this.parseEnvInt(
14677
- "OPENCODE_TASK_MAX_STEPS",
14678
- DEFAULT_SESSION.taskCommandMaxSteps
14679
- )
14680
- };
13610
+ var ConcurrencyController = class {
13611
+ counts = /* @__PURE__ */ new Map();
13612
+ queues = /* @__PURE__ */ new Map();
13613
+ limits = /* @__PURE__ */ new Map();
13614
+ config;
13615
+ constructor(config2) {
13616
+ this.config = config2 ?? {};
14681
13617
  }
14682
- getSessionConfig() {
14683
- return this.sessionConfig;
13618
+ setLimit(key, limit) {
13619
+ this.limits.set(key, limit);
14684
13620
  }
14685
- // ------------------------------------------------------------------------
14686
- // Runtime Updates (Dynamic Configuration)
14687
- // ------------------------------------------------------------------------
14688
13621
  /**
14689
- * Update configuration at runtime
14690
- * Useful for adaptive behavior or user preferences
13622
+ * Get concurrency limit for a key.
13623
+ * Priority: explicit limit > model > provider > agent > default
14691
13624
  */
14692
- updateParallelAgentConfig(updates) {
14693
- this.parallelAgentConfig = {
14694
- ...this.parallelAgentConfig,
14695
- ...updates
14696
- };
14697
- this.validateParallelAgentConfig();
14698
- }
14699
- updateBackgroundTaskConfig(updates) {
14700
- this.backgroundTaskConfig = {
14701
- ...this.backgroundTaskConfig,
14702
- ...updates
14703
- };
14704
- this.validateBackgroundTaskConfig();
14705
- }
14706
- updateSessionConfig(updates) {
14707
- this.sessionConfig = {
14708
- ...this.sessionConfig,
14709
- ...updates
14710
- };
14711
- this.validateSessionConfig();
14712
- }
14713
- // ------------------------------------------------------------------------
14714
- // Validation
14715
- // ------------------------------------------------------------------------
14716
- validateParallelAgentConfig() {
14717
- const { taskTtlMs, cleanupDelayMs, minStabilityMs, pollIntervalMs, defaultConcurrency, maxConcurrency } = this.parallelAgentConfig;
14718
- if (taskTtlMs < 60 * 1e3) {
14719
- console.warn("[Config] TASK_TTL_MS too low (< 1min), using 1min minimum");
14720
- this.parallelAgentConfig.taskTtlMs = 60 * 1e3;
14721
- }
14722
- if (cleanupDelayMs > taskTtlMs) {
14723
- console.warn("[Config] CLEANUP_DELAY_MS cannot exceed TASK_TTL_MS");
14724
- this.parallelAgentConfig.cleanupDelayMs = Math.floor(taskTtlMs / 2);
14725
- }
14726
- if (minStabilityMs < 1e3) {
14727
- console.warn("[Config] MIN_STABILITY_MS too low (< 1s), using 1s minimum");
14728
- this.parallelAgentConfig.minStabilityMs = 1e3;
14729
- }
14730
- if (pollIntervalMs < 500) {
14731
- console.warn("[Config] POLL_INTERVAL_MS too low (< 500ms), using 500ms minimum");
14732
- this.parallelAgentConfig.pollIntervalMs = 500;
14733
- }
14734
- if (defaultConcurrency < 1 || defaultConcurrency > maxConcurrency) {
14735
- console.warn(`[Config] DEFAULT_CONCURRENCY must be 1-${maxConcurrency}`);
14736
- this.parallelAgentConfig.defaultConcurrency = Math.min(
14737
- Math.max(defaultConcurrency, 1),
14738
- maxConcurrency
14739
- );
14740
- }
14741
- }
14742
- validateBackgroundTaskConfig() {
14743
- const { monitorIntervalMs, defaultTimeoutMs, maxCompletedTasksToKeep } = this.backgroundTaskConfig;
14744
- if (monitorIntervalMs < 1e3) {
14745
- console.warn("[Config] MONITOR_INTERVAL_MS too low (< 1s), using 1s minimum");
14746
- this.backgroundTaskConfig.monitorIntervalMs = 1e3;
14747
- }
14748
- if (defaultTimeoutMs < 10 * 1e3) {
14749
- console.warn("[Config] DEFAULT_TIMEOUT_MS too low (< 10s), using 10s minimum");
14750
- this.backgroundTaskConfig.defaultTimeoutMs = 10 * 1e3;
13625
+ getConcurrencyLimit(key) {
13626
+ const explicitLimit = this.limits.get(key);
13627
+ if (explicitLimit !== void 0) {
13628
+ return explicitLimit === 0 ? Infinity : explicitLimit;
14751
13629
  }
14752
- if (maxCompletedTasksToKeep < 0) {
14753
- console.warn("[Config] MAX_COMPLETED_TASKS must be >= 0, using 0");
14754
- this.backgroundTaskConfig.maxCompletedTasksToKeep = 0;
13630
+ if (this.config.modelConcurrency?.[key] !== void 0) {
13631
+ const limit = this.config.modelConcurrency[key];
13632
+ return limit === 0 ? Infinity : limit;
14755
13633
  }
14756
- }
14757
- validateSessionConfig() {
14758
- const { defaultMaxSteps, taskCommandMaxSteps } = this.sessionConfig;
14759
- if (defaultMaxSteps < 1) {
14760
- console.warn("[Config] DEFAULT_MAX_STEPS must be >= 1, using 1");
14761
- this.sessionConfig.defaultMaxSteps = 1;
13634
+ const provider = key.split("/")[0];
13635
+ if (this.config.providerConcurrency?.[provider] !== void 0) {
13636
+ const limit = this.config.providerConcurrency[provider];
13637
+ return limit === 0 ? Infinity : limit;
14762
13638
  }
14763
- if (taskCommandMaxSteps < defaultMaxSteps) {
14764
- console.warn("[Config] TASK_MAX_STEPS should be >= DEFAULT_MAX_STEPS");
14765
- this.sessionConfig.taskCommandMaxSteps = defaultMaxSteps;
13639
+ if (this.config.agentConcurrency?.[key] !== void 0) {
13640
+ const limit = this.config.agentConcurrency[key];
13641
+ return limit === 0 ? Infinity : limit;
14766
13642
  }
13643
+ return this.config.defaultConcurrency ?? PARALLEL_TASK.DEFAULT_CONCURRENCY;
14767
13644
  }
14768
- // ------------------------------------------------------------------------
14769
- // Helpers
14770
- // ------------------------------------------------------------------------
14771
- parseEnvInt(key, defaultValue) {
14772
- const value = process.env[key];
14773
- if (value === void 0) return defaultValue;
14774
- const parsed = parseInt(value, 10);
14775
- if (isNaN(parsed)) {
14776
- console.warn(`[Config] Invalid ${key} value: "${value}", using default ${defaultValue}`);
14777
- return defaultValue;
14778
- }
14779
- return parsed;
14780
- }
14781
- // ------------------------------------------------------------------------
14782
- // Export / Debug
14783
- // ------------------------------------------------------------------------
14784
- exportConfigs() {
14785
- console.log("\n\u{1F4CB} Current Configuration:\n");
14786
- console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
14787
- console.log("Parallel Agent:");
14788
- console.log(` Task TTL: ${this.parallelAgentConfig.taskTtlMs / 1e3}s`);
14789
- console.log(` Cleanup Delay: ${this.parallelAgentConfig.cleanupDelayMs / 1e3}s`);
14790
- console.log(` Min Stability: ${this.parallelAgentConfig.minStabilityMs / 1e3}s`);
14791
- console.log(` Poll Interval: ${this.parallelAgentConfig.pollIntervalMs / 1e3}s`);
14792
- console.log(` Default Concurrency: ${this.parallelAgentConfig.defaultConcurrency}`);
14793
- console.log(` Max Concurrency: ${this.parallelAgentConfig.maxConcurrency}`);
14794
- console.log(` Debug: ${this.parallelAgentConfig.enableDebug}`);
14795
- console.log("");
14796
- console.log("Background Task:");
14797
- console.log(` Monitor Interval: ${this.backgroundTaskConfig.monitorIntervalMs / 1e3}s`);
14798
- console.log(` Default Timeout: ${this.backgroundTaskConfig.defaultTimeoutMs / 1e3}s`);
14799
- console.log(` Max Completed Tasks: ${this.backgroundTaskConfig.maxCompletedTasksToKeep}`);
14800
- console.log(` Storage Dir: ${this.backgroundTaskConfig.storageDir}`);
14801
- console.log(` Debug: ${this.backgroundTaskConfig.enableDebug}`);
14802
- console.log("");
14803
- console.log("Session:");
14804
- console.log(` Default Max Steps: ${this.sessionConfig.defaultMaxSteps}`);
14805
- console.log(` Task Max Steps: ${this.sessionConfig.taskCommandMaxSteps}`);
14806
- console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
14807
- }
14808
- };
14809
- var configManager = ConfigManager.getInstance();
14810
-
14811
- // src/core/async-agent.ts
14812
- var TASK_TTL_MS2 = configManager.getParallelAgentConfig().taskTtlMs;
14813
- var CLEANUP_DELAY_MS2 = configManager.getParallelAgentConfig().cleanupDelayMs;
14814
- var MIN_STABILITY_MS2 = configManager.getParallelAgentConfig().minStabilityMs;
14815
- var POLL_INTERVAL_MS = configManager.getParallelAgentConfig().pollIntervalMs;
14816
- var DEFAULT_CONCURRENCY = configManager.getParallelAgentConfig().defaultConcurrency;
14817
- var MAX_CONCURRENCY = configManager.getParallelAgentConfig().maxConcurrency;
14818
- var DEBUG = configManager.getParallelAgentConfig().enableDebug;
14819
- var log = (...args) => {
14820
- if (DEBUG) console.log("[parallel-agent]", ...args);
14821
- };
14822
- var ConcurrencyController = class {
14823
- counts = /* @__PURE__ */ new Map();
14824
- queues = /* @__PURE__ */ new Map();
14825
- limits = /* @__PURE__ */ new Map();
14826
- setLimit(key, limit) {
14827
- const cappedLimit = Math.min(limit, MAX_CONCURRENCY);
14828
- this.limits.set(key, cappedLimit);
14829
- log(`Set limit for ${key}: ${cappedLimit}`);
14830
- }
13645
+ // Backwards compatible alias
14831
13646
  getLimit(key) {
14832
- return this.limits.get(key) ?? DEFAULT_CONCURRENCY;
13647
+ return this.getConcurrencyLimit(key);
14833
13648
  }
14834
13649
  async acquire(key) {
14835
- const limit = this.getLimit(key);
14836
- if (limit === 0) return;
13650
+ const limit = this.getConcurrencyLimit(key);
13651
+ if (limit === Infinity) return;
14837
13652
  const current = this.counts.get(key) ?? 0;
14838
13653
  if (current < limit) {
14839
13654
  this.counts.set(key, current + 1);
14840
- log(`Acquired slot for ${key}: ${current + 1}/${limit}`);
13655
+ log(`Acquired ${key}: ${current + 1}/${limit}`);
14841
13656
  return;
14842
13657
  }
14843
- log(`Queueing for ${key}: ${current}/${limit} (waiting...)`);
13658
+ log(`Queueing ${key}: ${current}/${limit}`);
14844
13659
  return new Promise((resolve) => {
14845
13660
  const queue = this.queues.get(key) ?? [];
14846
13661
  queue.push(resolve);
@@ -14848,97 +13663,196 @@ var ConcurrencyController = class {
14848
13663
  });
14849
13664
  }
14850
13665
  release(key) {
14851
- const limit = this.getLimit(key);
14852
- if (limit === 0) return;
13666
+ const limit = this.getConcurrencyLimit(key);
13667
+ if (limit === Infinity) return;
14853
13668
  const queue = this.queues.get(key);
14854
13669
  if (queue && queue.length > 0) {
14855
13670
  const next = queue.shift();
14856
- log(`Released slot for ${key}: next in queue`);
13671
+ log(`Released ${key}: next in queue`);
14857
13672
  next();
14858
13673
  } else {
14859
13674
  const current = this.counts.get(key) ?? 0;
14860
13675
  if (current > 0) {
14861
13676
  this.counts.set(key, current - 1);
14862
- log(`Released slot for ${key}: ${current - 1}`);
13677
+ log(`Released ${key}: ${current - 1}/${limit}`);
14863
13678
  }
14864
13679
  }
14865
13680
  }
14866
13681
  getQueueLength(key) {
14867
13682
  return this.queues.get(key)?.length ?? 0;
14868
13683
  }
14869
- updateConcurrency(agentType, newLimit) {
14870
- this.setLimit(agentType, newLimit);
14871
- log(`Updated concurrency for ${agentType}: ${newLimit}`);
13684
+ getActiveCount(key) {
13685
+ return this.counts.get(key) ?? 0;
14872
13686
  }
14873
- getStats() {
14874
- const stats = {};
14875
- for (const [agentType, limit] of this.limits.entries()) {
14876
- stats[agentType] = {
14877
- running: this.counts.get(agentType) ?? 0,
14878
- queued: this.getQueueLength(agentType),
14879
- limit
14880
- };
14881
- }
14882
- return stats;
13687
+ /**
13688
+ * Get formatted concurrency info string (e.g., "2/5 slots")
13689
+ */
13690
+ getConcurrencyInfo(key) {
13691
+ const active = this.getActiveCount(key);
13692
+ const limit = this.getConcurrencyLimit(key);
13693
+ if (limit === Infinity) return "";
13694
+ return ` (${active}/${limit} slots)`;
14883
13695
  }
14884
13696
  };
14885
- var ParallelAgentManager = class _ParallelAgentManager {
14886
- static _instance;
14887
- // Core state
13697
+
13698
+ // src/core/agents/task-store.ts
13699
+ var TaskStore = class {
14888
13700
  tasks = /* @__PURE__ */ new Map();
14889
13701
  pendingByParent = /* @__PURE__ */ new Map();
14890
13702
  notifications = /* @__PURE__ */ new Map();
14891
- // Dependencies
13703
+ set(id, task) {
13704
+ this.tasks.set(id, task);
13705
+ }
13706
+ get(id) {
13707
+ return this.tasks.get(id);
13708
+ }
13709
+ getAll() {
13710
+ return Array.from(this.tasks.values());
13711
+ }
13712
+ getRunning() {
13713
+ return this.getAll().filter((t) => t.status === "running");
13714
+ }
13715
+ getByParent(parentSessionID) {
13716
+ return this.getAll().filter((t) => t.parentSessionID === parentSessionID);
13717
+ }
13718
+ delete(id) {
13719
+ return this.tasks.delete(id);
13720
+ }
13721
+ clear() {
13722
+ this.tasks.clear();
13723
+ this.pendingByParent.clear();
13724
+ this.notifications.clear();
13725
+ }
13726
+ // Pending tracking
13727
+ trackPending(parentSessionID, taskId) {
13728
+ const pending = this.pendingByParent.get(parentSessionID) ?? /* @__PURE__ */ new Set();
13729
+ pending.add(taskId);
13730
+ this.pendingByParent.set(parentSessionID, pending);
13731
+ }
13732
+ untrackPending(parentSessionID, taskId) {
13733
+ const pending = this.pendingByParent.get(parentSessionID);
13734
+ if (pending) {
13735
+ pending.delete(taskId);
13736
+ if (pending.size === 0) {
13737
+ this.pendingByParent.delete(parentSessionID);
13738
+ }
13739
+ }
13740
+ }
13741
+ getPendingCount(parentSessionID) {
13742
+ return this.pendingByParent.get(parentSessionID)?.size ?? 0;
13743
+ }
13744
+ hasPending(parentSessionID) {
13745
+ return this.getPendingCount(parentSessionID) > 0;
13746
+ }
13747
+ // Notifications
13748
+ queueNotification(task) {
13749
+ const queue = this.notifications.get(task.parentSessionID) ?? [];
13750
+ queue.push(task);
13751
+ this.notifications.set(task.parentSessionID, queue);
13752
+ }
13753
+ getNotifications(parentSessionID) {
13754
+ return this.notifications.get(parentSessionID) ?? [];
13755
+ }
13756
+ clearNotifications(parentSessionID) {
13757
+ this.notifications.delete(parentSessionID);
13758
+ }
13759
+ cleanEmptyNotifications() {
13760
+ for (const [sessionID, queue] of this.notifications.entries()) {
13761
+ if (queue.length === 0) {
13762
+ this.notifications.delete(sessionID);
13763
+ }
13764
+ }
13765
+ }
13766
+ /**
13767
+ * Remove a specific task from all notification queues
13768
+ */
13769
+ clearNotificationsForTask(taskId) {
13770
+ for (const [sessionID, tasks] of this.notifications.entries()) {
13771
+ const filtered = tasks.filter((t) => t.id !== taskId);
13772
+ if (filtered.length === 0) {
13773
+ this.notifications.delete(sessionID);
13774
+ } else if (filtered.length !== tasks.length) {
13775
+ this.notifications.set(sessionID, filtered);
13776
+ }
13777
+ }
13778
+ }
13779
+ };
13780
+
13781
+ // src/core/agents/config.ts
13782
+ var CONFIG = {
13783
+ TASK_TTL_MS: PARALLEL_TASK.TTL_MS,
13784
+ CLEANUP_DELAY_MS: PARALLEL_TASK.CLEANUP_DELAY_MS,
13785
+ MIN_STABILITY_MS: PARALLEL_TASK.MIN_STABILITY_MS,
13786
+ POLL_INTERVAL_MS: PARALLEL_TASK.POLL_INTERVAL_MS
13787
+ };
13788
+
13789
+ // src/core/agents/logger.ts
13790
+ var DEBUG2 = process.env.DEBUG_PARALLEL_AGENT === "true";
13791
+ function log2(...args) {
13792
+ if (DEBUG2) console.log("[parallel-agent]", ...args);
13793
+ }
13794
+
13795
+ // src/core/agents/format.ts
13796
+ function formatDuration(start, end) {
13797
+ const duration3 = (end ?? /* @__PURE__ */ new Date()).getTime() - start.getTime();
13798
+ const seconds = Math.floor(duration3 / 1e3);
13799
+ const minutes = Math.floor(seconds / 60);
13800
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
13801
+ return `${seconds}s`;
13802
+ }
13803
+ function buildNotificationMessage(tasks) {
13804
+ const summary = tasks.map((t) => {
13805
+ const status = t.status === "completed" ? "\u2705" : "\u274C";
13806
+ return `${status} \`${t.id}\`: ${t.description}`;
13807
+ }).join("\n");
13808
+ return `<system-notification>
13809
+ **All Parallel Tasks Complete**
13810
+
13811
+ ${summary}
13812
+
13813
+ Use \`get_task_result({ taskId: "task_xxx" })\` to retrieve results.
13814
+ </system-notification>`;
13815
+ }
13816
+
13817
+ // src/core/agents/manager.ts
13818
+ var ParallelAgentManager = class _ParallelAgentManager {
13819
+ static _instance;
13820
+ store = new TaskStore();
14892
13821
  client;
14893
13822
  directory;
14894
- concurrency;
14895
- // Polling
13823
+ concurrency = new ConcurrencyController();
14896
13824
  pollingInterval;
14897
13825
  constructor(client, directory) {
14898
13826
  this.client = client;
14899
13827
  this.directory = directory;
14900
- this.concurrency = new ConcurrencyController();
14901
13828
  }
14902
13829
  static getInstance(client, directory) {
14903
13830
  if (!_ParallelAgentManager._instance) {
14904
13831
  if (!client || !directory) {
14905
- throw new Error(
14906
- "ParallelAgentManager requires client and directory on first call"
14907
- );
13832
+ throw new Error("ParallelAgentManager requires client and directory on first call");
14908
13833
  }
14909
- _ParallelAgentManager._instance = new _ParallelAgentManager(
14910
- client,
14911
- directory
14912
- );
13834
+ _ParallelAgentManager._instance = new _ParallelAgentManager(client, directory);
14913
13835
  }
14914
13836
  return _ParallelAgentManager._instance;
14915
13837
  }
14916
13838
  // ========================================================================
14917
13839
  // Public API
14918
13840
  // ========================================================================
14919
- /**
14920
- * Launch an agent in a new session (async, non-blocking)
14921
- */
14922
13841
  async launch(input) {
14923
13842
  const concurrencyKey = input.agent;
14924
13843
  await this.concurrency.acquire(concurrencyKey);
14925
13844
  this.pruneExpiredTasks();
14926
13845
  try {
14927
13846
  const createResult = await this.client.session.create({
14928
- body: {
14929
- parentID: input.parentSessionID,
14930
- title: `Parallel: ${input.description}`
14931
- },
14932
- query: {
14933
- directory: this.directory
14934
- }
13847
+ body: { parentID: input.parentSessionID, title: `Parallel: ${input.description}` },
13848
+ query: { directory: this.directory }
14935
13849
  });
14936
13850
  if (createResult.error) {
14937
13851
  this.concurrency.release(concurrencyKey);
14938
13852
  throw new Error(`Failed to create session: ${createResult.error}`);
14939
13853
  }
14940
13854
  const sessionID = createResult.data.id;
14941
- const taskId = `task_${crypto.randomUUID().slice(0, 8)}`;
13855
+ const taskId = `${ID_PREFIX.TASK}${crypto.randomUUID().slice(0, 8)}`;
14942
13856
  const task = {
14943
13857
  id: taskId,
14944
13858
  sessionID,
@@ -14949,191 +13863,172 @@ var ParallelAgentManager = class _ParallelAgentManager {
14949
13863
  startedAt: /* @__PURE__ */ new Date(),
14950
13864
  concurrencyKey
14951
13865
  };
14952
- this.tasks.set(taskId, task);
14953
- this.trackPending(input.parentSessionID, taskId);
13866
+ this.store.set(taskId, task);
13867
+ this.store.trackPending(input.parentSessionID, taskId);
14954
13868
  this.startPolling();
14955
13869
  this.client.session.prompt({
14956
13870
  path: { id: sessionID },
14957
- body: {
14958
- agent: input.agent,
14959
- parts: [{ type: "text", text: input.prompt }]
14960
- }
13871
+ body: { agent: input.agent, parts: [{ type: "text", text: input.prompt }] }
14961
13872
  }).catch((error45) => {
14962
- log(`Prompt error for ${taskId}:`, error45);
13873
+ log2(`Prompt error for ${taskId}:`, error45);
14963
13874
  this.handleTaskError(taskId, error45);
14964
13875
  });
14965
- log(`Launched ${taskId} in session ${sessionID}`);
13876
+ log2(`Launched ${taskId} in session ${sessionID}`);
14966
13877
  return task;
14967
13878
  } catch (error45) {
14968
13879
  this.concurrency.release(concurrencyKey);
14969
13880
  throw error45;
14970
13881
  }
14971
13882
  }
14972
- /**
14973
- * Get task by ID
14974
- */
14975
13883
  getTask(id) {
14976
- return this.tasks.get(id);
13884
+ return this.store.get(id);
14977
13885
  }
14978
- /**
14979
- * Get all running tasks
14980
- */
14981
13886
  getRunningTasks() {
14982
- return Array.from(this.tasks.values()).filter(
14983
- (t) => t.status === "running"
14984
- );
13887
+ return this.store.getRunning();
14985
13888
  }
14986
- /**
14987
- * Get all tasks
14988
- */
14989
13889
  getAllTasks() {
14990
- return Array.from(this.tasks.values());
13890
+ return this.store.getAll();
14991
13891
  }
14992
- /**
14993
- * Get tasks by parent session
14994
- */
14995
13892
  getTasksByParent(parentSessionID) {
14996
- return Array.from(this.tasks.values()).filter(
14997
- (t) => t.parentSessionID === parentSessionID
14998
- );
13893
+ return this.store.getByParent(parentSessionID);
14999
13894
  }
15000
- /**
15001
- * Cancel a running task
15002
- */
15003
13895
  async cancelTask(taskId) {
15004
- const task = this.tasks.get(taskId);
15005
- if (!task || task.status !== "running") {
15006
- return false;
15007
- }
13896
+ const task = this.store.get(taskId);
13897
+ if (!task || task.status !== "running") return false;
15008
13898
  task.status = "error";
15009
13899
  task.error = "Cancelled by user";
15010
13900
  task.completedAt = /* @__PURE__ */ new Date();
15011
- if (task.concurrencyKey) {
15012
- this.concurrency.release(task.concurrencyKey);
15013
- }
15014
- this.untrackPending(task.parentSessionID, taskId);
13901
+ if (task.concurrencyKey) this.concurrency.release(task.concurrencyKey);
13902
+ this.store.untrackPending(task.parentSessionID, taskId);
15015
13903
  try {
15016
- await this.client.session.delete({
15017
- path: { id: task.sessionID }
15018
- });
15019
- console.log(
15020
- `[parallel] \u{1F5D1}\uFE0F Session ${task.sessionID.slice(0, 8)}... deleted`
15021
- );
13904
+ await this.client.session.delete({ path: { id: task.sessionID } });
13905
+ log2(`Session ${task.sessionID.slice(0, 8)}... deleted`);
15022
13906
  } catch {
15023
- console.log(
15024
- `[parallel] \u{1F5D1}\uFE0F Session ${task.sessionID.slice(0, 8)}... already gone`
15025
- );
13907
+ log2(`Session ${task.sessionID.slice(0, 8)}... already gone`);
15026
13908
  }
15027
13909
  this.scheduleCleanup(taskId);
15028
- console.log(`[parallel] \u{1F6D1} CANCELLED ${taskId}`);
15029
- log(`Cancelled ${taskId}`);
13910
+ log2(`Cancelled ${taskId}`);
15030
13911
  return true;
15031
13912
  }
15032
- /**
15033
- * Get result from completed task
15034
- */
15035
13913
  async getResult(taskId) {
15036
- const task = this.tasks.get(taskId);
13914
+ const task = this.store.get(taskId);
15037
13915
  if (!task) return null;
15038
13916
  if (task.result) return task.result;
15039
13917
  if (task.status === "error") return `Error: ${task.error}`;
15040
13918
  if (task.status === "running") return null;
15041
13919
  try {
15042
- const messagesResult = await this.client.session.messages({
15043
- path: { id: task.sessionID }
15044
- });
15045
- if (messagesResult.error) {
15046
- return `Error: ${messagesResult.error}`;
15047
- }
15048
- const messages = messagesResult.data ?? [];
15049
- const assistantMsgs = messages.filter((m) => m.info?.role === "assistant").reverse();
15050
- const lastMsg = assistantMsgs[0];
13920
+ const result = await this.client.session.messages({ path: { id: task.sessionID } });
13921
+ if (result.error) return `Error: ${result.error}`;
13922
+ const messages = result.data ?? [];
13923
+ const lastMsg = messages.filter((m) => m.info?.role === "assistant").reverse()[0];
15051
13924
  if (!lastMsg) return "(No response)";
15052
- const textParts = lastMsg.parts?.filter(
15053
- (p) => p.type === "text" || p.type === "reasoning"
15054
- ) ?? [];
15055
- const result = textParts.map((p) => p.text ?? "").filter(Boolean).join("\n");
15056
- task.result = result;
15057
- return result;
13925
+ const text = lastMsg.parts?.filter((p) => p.type === "text" || p.type === "reasoning").map((p) => p.text ?? "").filter(Boolean).join("\n") ?? "";
13926
+ task.result = text;
13927
+ return text;
15058
13928
  } catch (error45) {
15059
13929
  return `Error: ${error45 instanceof Error ? error45.message : String(error45)}`;
15060
13930
  }
15061
13931
  }
15062
- /**
15063
- * Set concurrency limit for agent type
15064
- */
15065
13932
  setConcurrencyLimit(agentType, limit) {
15066
13933
  this.concurrency.setLimit(agentType, limit);
15067
13934
  }
15068
- /**
15069
- * Get concurrency statistics for all agent types
15070
- */
15071
- getConcurrencyStats() {
15072
- return this.concurrency.getStats();
15073
- }
15074
- /**
15075
- * Update concurrency limit dynamically at runtime
15076
- */
15077
- updateConcurrency(agentType, newLimit) {
15078
- this.concurrency.updateConcurrency(agentType, newLimit);
15079
- }
15080
- /**
15081
- * Get pending notification count
15082
- */
15083
13935
  getPendingCount(parentSessionID) {
15084
- return this.pendingByParent.get(parentSessionID)?.size ?? 0;
13936
+ return this.store.getPendingCount(parentSessionID);
15085
13937
  }
15086
- /**
15087
- * Cleanup all state
15088
- */
15089
13938
  cleanup() {
15090
13939
  this.stopPolling();
15091
- this.tasks.clear();
15092
- this.pendingByParent.clear();
15093
- this.notifications.clear();
13940
+ this.store.clear();
15094
13941
  }
13942
+ formatDuration = formatDuration;
15095
13943
  // ========================================================================
15096
- // Internal: Tracking
13944
+ // Event Handling (from OpenCode hooks)
15097
13945
  // ========================================================================
15098
- trackPending(parentSessionID, taskId) {
15099
- const pending = this.pendingByParent.get(parentSessionID) ?? /* @__PURE__ */ new Set();
15100
- pending.add(taskId);
15101
- this.pendingByParent.set(parentSessionID, pending);
15102
- }
15103
- untrackPending(parentSessionID, taskId) {
15104
- const pending = this.pendingByParent.get(parentSessionID);
15105
- if (pending) {
15106
- pending.delete(taskId);
15107
- if (pending.size === 0) {
15108
- this.pendingByParent.delete(parentSessionID);
13946
+ /**
13947
+ * Handle OpenCode session events for proper resource cleanup.
13948
+ * Call this from your plugin's event hook.
13949
+ */
13950
+ handleEvent(event) {
13951
+ const props = event.properties;
13952
+ if (event.type === "session.idle") {
13953
+ const sessionID = props?.sessionID;
13954
+ if (!sessionID) return;
13955
+ const task = this.findBySession(sessionID);
13956
+ if (!task || task.status !== "running") return;
13957
+ this.handleSessionIdle(task).catch((err) => {
13958
+ log2("Error handling session.idle:", err);
13959
+ });
13960
+ }
13961
+ if (event.type === "session.deleted") {
13962
+ const sessionID = props?.info?.id ?? props?.sessionID;
13963
+ if (!sessionID) return;
13964
+ const task = this.findBySession(sessionID);
13965
+ if (!task) return;
13966
+ log2(`Session deleted event for task ${task.id}`);
13967
+ if (task.status === "running") {
13968
+ task.status = "error";
13969
+ task.error = "Session deleted";
13970
+ task.completedAt = /* @__PURE__ */ new Date();
15109
13971
  }
13972
+ if (task.concurrencyKey) {
13973
+ this.concurrency.release(task.concurrencyKey);
13974
+ task.concurrencyKey = void 0;
13975
+ }
13976
+ this.store.untrackPending(task.parentSessionID, task.id);
13977
+ this.store.clearNotificationsForTask(task.id);
13978
+ this.store.delete(task.id);
13979
+ log2(`Cleaned up deleted session task: ${task.id}`);
15110
13980
  }
15111
13981
  }
13982
+ /**
13983
+ * Find task by session ID
13984
+ */
13985
+ findBySession(sessionID) {
13986
+ return this.store.getAll().find((t) => t.sessionID === sessionID);
13987
+ }
13988
+ /**
13989
+ * Handle session.idle event - validate and complete task
13990
+ */
13991
+ async handleSessionIdle(task) {
13992
+ const elapsed = Date.now() - task.startedAt.getTime();
13993
+ if (elapsed < CONFIG.MIN_STABILITY_MS) {
13994
+ log2(`Session idle but too early for ${task.id}, waiting...`);
13995
+ return;
13996
+ }
13997
+ const hasOutput = await this.validateSessionHasOutput(task.sessionID);
13998
+ if (!hasOutput) {
13999
+ log2(`Session idle but no output for ${task.id}, waiting...`);
14000
+ return;
14001
+ }
14002
+ task.status = "completed";
14003
+ task.completedAt = /* @__PURE__ */ new Date();
14004
+ if (task.concurrencyKey) {
14005
+ this.concurrency.release(task.concurrencyKey);
14006
+ task.concurrencyKey = void 0;
14007
+ }
14008
+ this.store.untrackPending(task.parentSessionID, task.id);
14009
+ this.store.queueNotification(task);
14010
+ await this.notifyParentIfAllComplete(task.parentSessionID);
14011
+ this.scheduleCleanup(task.id);
14012
+ log2(`Task ${task.id} completed via session.idle event (${formatDuration(task.startedAt, task.completedAt)})`);
14013
+ }
15112
14014
  // ========================================================================
15113
- // Internal: Error Handling
14015
+ // Internal
15114
14016
  // ========================================================================
15115
14017
  handleTaskError(taskId, error45) {
15116
- const task = this.tasks.get(taskId);
14018
+ const task = this.store.get(taskId);
15117
14019
  if (!task) return;
15118
14020
  task.status = "error";
15119
14021
  task.error = error45 instanceof Error ? error45.message : String(error45);
15120
14022
  task.completedAt = /* @__PURE__ */ new Date();
15121
- if (task.concurrencyKey) {
15122
- this.concurrency.release(task.concurrencyKey);
15123
- }
15124
- this.untrackPending(task.parentSessionID, taskId);
15125
- this.queueNotification(task);
14023
+ if (task.concurrencyKey) this.concurrency.release(task.concurrencyKey);
14024
+ this.store.untrackPending(task.parentSessionID, taskId);
14025
+ this.store.queueNotification(task);
15126
14026
  this.notifyParentIfAllComplete(task.parentSessionID);
15127
14027
  this.scheduleCleanup(taskId);
15128
14028
  }
15129
- // ========================================================================
15130
- // Internal: Polling
15131
- // ========================================================================
15132
14029
  startPolling() {
15133
14030
  if (this.pollingInterval) return;
15134
- this.pollingInterval = setInterval(() => {
15135
- this.pollRunningTasks();
15136
- }, POLL_INTERVAL_MS);
14031
+ this.pollingInterval = setInterval(() => this.pollRunningTasks(), CONFIG.POLL_INTERVAL_MS);
15137
14032
  this.pollingInterval.unref();
15138
14033
  }
15139
14034
  stopPolling() {
@@ -15144,1003 +14039,322 @@ var ParallelAgentManager = class _ParallelAgentManager {
15144
14039
  }
15145
14040
  async pollRunningTasks() {
15146
14041
  this.pruneExpiredTasks();
15147
- const runningTasks = this.getRunningTasks();
15148
- if (runningTasks.length === 0) {
14042
+ const running = this.store.getRunning();
14043
+ if (running.length === 0) {
15149
14044
  this.stopPolling();
15150
14045
  return;
15151
14046
  }
15152
14047
  try {
15153
14048
  const statusResult = await this.client.session.status();
15154
14049
  const allStatuses = statusResult.data ?? {};
15155
- for (const task of runningTasks) {
15156
- const sessionStatus = allStatuses[task.sessionID];
15157
- if (sessionStatus?.type === "idle") {
14050
+ for (const task of running) {
14051
+ try {
14052
+ const sessionStatus = allStatuses[task.sessionID];
14053
+ if (sessionStatus?.type === "idle") {
14054
+ const elapsed2 = Date.now() - task.startedAt.getTime();
14055
+ if (elapsed2 < CONFIG.MIN_STABILITY_MS) continue;
14056
+ if (!await this.validateSessionHasOutput(task.sessionID)) continue;
14057
+ await this.completeTask(task);
14058
+ continue;
14059
+ }
14060
+ await this.updateTaskProgress(task);
15158
14061
  const elapsed = Date.now() - task.startedAt.getTime();
15159
- if (elapsed < MIN_STABILITY_MS2) continue;
15160
- const hasOutput = await this.validateSessionHasOutput(task.sessionID);
15161
- if (!hasOutput) continue;
15162
- task.status = "completed";
15163
- task.completedAt = /* @__PURE__ */ new Date();
15164
- if (task.concurrencyKey) {
15165
- this.concurrency.release(task.concurrencyKey);
14062
+ if (elapsed >= CONFIG.MIN_STABILITY_MS && task.stablePolls && task.stablePolls >= 3) {
14063
+ if (await this.validateSessionHasOutput(task.sessionID)) {
14064
+ log2(`Task ${task.id} stable for 3 polls, completing...`);
14065
+ await this.completeTask(task);
14066
+ }
15166
14067
  }
15167
- this.untrackPending(task.parentSessionID, task.id);
15168
- this.queueNotification(task);
15169
- this.notifyParentIfAllComplete(task.parentSessionID);
15170
- this.scheduleCleanup(task.id);
15171
- const duration3 = this.formatDuration(
15172
- task.startedAt,
15173
- task.completedAt
15174
- );
15175
- console.log(
15176
- `[parallel] \u2705 COMPLETED ${task.id} \u2192 ${task.agent}: ${task.description} (${duration3})`
15177
- );
15178
- log(`Completed ${task.id}`);
14068
+ } catch (error45) {
14069
+ log2(`Poll error for task ${task.id}:`, error45);
15179
14070
  }
15180
14071
  }
15181
14072
  } catch (error45) {
15182
- log("Polling error:", error45);
14073
+ log2("Polling error:", error45);
15183
14074
  }
15184
14075
  }
15185
- // ========================================================================
15186
- // Internal: Validation
15187
- // ========================================================================
14076
+ /**
14077
+ * Update task progress and stability tracking
14078
+ */
14079
+ async updateTaskProgress(task) {
14080
+ try {
14081
+ const result = await this.client.session.messages({ path: { id: task.sessionID } });
14082
+ if (result.error) return;
14083
+ const messages = result.data ?? [];
14084
+ const assistantMsgs = messages.filter((m) => m.info?.role === "assistant");
14085
+ let toolCalls = 0;
14086
+ let lastTool;
14087
+ let lastMessage;
14088
+ for (const msg of assistantMsgs) {
14089
+ for (const part of msg.parts ?? []) {
14090
+ if (part.type === "tool_use" || part.tool) {
14091
+ toolCalls++;
14092
+ lastTool = part.tool || part.name;
14093
+ }
14094
+ if (part.type === "text" && part.text) {
14095
+ lastMessage = part.text;
14096
+ }
14097
+ }
14098
+ }
14099
+ task.progress = {
14100
+ toolCalls,
14101
+ lastTool,
14102
+ lastMessage: lastMessage?.slice(0, 100),
14103
+ lastUpdate: /* @__PURE__ */ new Date()
14104
+ };
14105
+ const currentMsgCount = messages.length;
14106
+ if (task.lastMsgCount === currentMsgCount) {
14107
+ task.stablePolls = (task.stablePolls ?? 0) + 1;
14108
+ } else {
14109
+ task.stablePolls = 0;
14110
+ }
14111
+ task.lastMsgCount = currentMsgCount;
14112
+ } catch {
14113
+ }
14114
+ }
14115
+ /**
14116
+ * Complete a task and cleanup
14117
+ */
14118
+ async completeTask(task) {
14119
+ task.status = "completed";
14120
+ task.completedAt = /* @__PURE__ */ new Date();
14121
+ if (task.concurrencyKey) {
14122
+ this.concurrency.release(task.concurrencyKey);
14123
+ task.concurrencyKey = void 0;
14124
+ }
14125
+ this.store.untrackPending(task.parentSessionID, task.id);
14126
+ this.store.queueNotification(task);
14127
+ await this.notifyParentIfAllComplete(task.parentSessionID);
14128
+ this.scheduleCleanup(task.id);
14129
+ log2(`Completed ${task.id} (${formatDuration(task.startedAt, task.completedAt)})`);
14130
+ }
15188
14131
  async validateSessionHasOutput(sessionID) {
15189
14132
  try {
15190
- const response = await this.client.session.messages({
15191
- path: { id: sessionID }
15192
- });
14133
+ const response = await this.client.session.messages({ path: { id: sessionID } });
15193
14134
  const messages = response.data ?? [];
15194
- const hasContent = messages.some((m) => {
15195
- if (m.info?.role !== "assistant") return false;
15196
- const parts = m.parts ?? [];
15197
- return parts.some(
15198
- (p) => p.type === "text" && p.text?.trim() || p.type === "reasoning" && p.text?.trim() || p.type === "tool"
15199
- );
15200
- });
15201
- return hasContent;
14135
+ return messages.some((m) => m.info?.role === "assistant" && m.parts?.some((p) => p.type === "text" && p.text?.trim() || p.type === "tool"));
15202
14136
  } catch {
15203
14137
  return true;
15204
14138
  }
15205
14139
  }
15206
- // ========================================================================
15207
- // Internal: Cleanup & TTL
15208
- // ========================================================================
15209
14140
  pruneExpiredTasks() {
15210
14141
  const now = Date.now();
15211
- for (const [taskId, task] of this.tasks.entries()) {
14142
+ for (const [taskId, task] of this.store.getAll().map((t) => [t.id, t])) {
15212
14143
  const age = now - task.startedAt.getTime();
15213
- if (age > TASK_TTL_MS2) {
15214
- log(`Timeout: ${taskId} (${Math.round(age / 1e3)}s)`);
15215
- if (task.status === "running") {
15216
- task.status = "timeout";
15217
- task.error = "Task exceeded 30 minute time limit";
15218
- task.completedAt = /* @__PURE__ */ new Date();
15219
- if (task.concurrencyKey) {
15220
- this.concurrency.release(task.concurrencyKey);
15221
- }
15222
- this.untrackPending(task.parentSessionID, taskId);
15223
- console.log(
15224
- `[parallel] \u23F1\uFE0F TIMEOUT ${taskId} \u2192 ${task.agent}: ${task.description}`
15225
- );
15226
- }
15227
- this.client.session.delete({
15228
- path: { id: task.sessionID }
15229
- }).then(() => {
15230
- console.log(
15231
- `[parallel] \u{1F5D1}\uFE0F CLEANED ${taskId} (timeout session deleted)`
15232
- );
15233
- }).catch(() => {
15234
- console.log(
15235
- `[parallel] \u{1F5D1}\uFE0F CLEANED ${taskId} (timeout session already gone)`
15236
- );
15237
- });
15238
- this.tasks.delete(taskId);
15239
- }
15240
- }
15241
- for (const [sessionID, queue] of this.notifications.entries()) {
15242
- if (queue.length === 0) {
15243
- this.notifications.delete(sessionID);
15244
- }
14144
+ if (age <= CONFIG.TASK_TTL_MS) continue;
14145
+ log2(`Timeout: ${taskId}`);
14146
+ if (task.status === "running") {
14147
+ task.status = "timeout";
14148
+ task.error = "Task exceeded 30 minute time limit";
14149
+ task.completedAt = /* @__PURE__ */ new Date();
14150
+ if (task.concurrencyKey) this.concurrency.release(task.concurrencyKey);
14151
+ this.store.untrackPending(task.parentSessionID, taskId);
14152
+ }
14153
+ this.client.session.delete({ path: { id: task.sessionID } }).catch(() => {
14154
+ });
14155
+ this.store.delete(taskId);
15245
14156
  }
14157
+ this.store.cleanEmptyNotifications();
15246
14158
  }
15247
14159
  scheduleCleanup(taskId) {
15248
- const task = this.tasks.get(taskId);
14160
+ const task = this.store.get(taskId);
15249
14161
  const sessionID = task?.sessionID;
15250
14162
  setTimeout(async () => {
15251
14163
  if (sessionID) {
15252
- try {
15253
- await this.client.session.delete({
15254
- path: { id: sessionID }
15255
- });
15256
- console.log(`[parallel] \u{1F5D1}\uFE0F CLEANED ${taskId} (session deleted)`);
15257
- log(`Deleted session ${sessionID}`);
15258
- } catch {
15259
- console.log(`[parallel] \u{1F5D1}\uFE0F CLEANED ${taskId} (session already gone)`);
15260
- }
15261
- }
15262
- this.tasks.delete(taskId);
15263
- log(`Cleaned up ${taskId} from memory`);
15264
- }, CLEANUP_DELAY_MS2);
15265
- }
15266
- // ========================================================================
15267
- // Internal: Notifications
15268
- // ========================================================================
15269
- queueNotification(task) {
15270
- const queue = this.notifications.get(task.parentSessionID) ?? [];
15271
- queue.push(task);
15272
- this.notifications.set(task.parentSessionID, queue);
15273
- }
15274
- async notifyParentIfAllComplete(parentSessionID) {
15275
- const pending = this.pendingByParent.get(parentSessionID);
15276
- if (pending && pending.size > 0) {
15277
- log(`${pending.size} tasks still pending for ${parentSessionID}`);
15278
- return;
15279
- }
15280
- const completedTasks = this.notifications.get(parentSessionID) ?? [];
15281
- if (completedTasks.length === 0) return;
15282
- const summary = completedTasks.map((t) => {
15283
- const status = t.status === "completed" ? "\u2705" : "\u274C";
15284
- const duration3 = this.formatDuration(t.startedAt, t.completedAt);
15285
- return `${status} \`${t.id}\` (${duration3}): ${t.description}`;
15286
- }).join("\n");
15287
- const notification = `<system-notification>
15288
- **All Parallel Tasks Complete**
15289
-
15290
- ${summary}
15291
-
15292
- ---
15293
-
15294
- **Retrieval Options**
15295
-
15296
- Use \`get_task_result({ taskId: "task_xxx" })\` to retrieve full results.
15297
-
15298
- ---
15299
-
15300
- **Task Summary**
15301
-
15302
- Total Tasks: ${completedTasks.length}
15303
- Status: All Complete
15304
- Mode: Background (non-blocking)
15305
- </system-notification>`;
15306
- try {
15307
- await this.client.session.prompt({
15308
- path: { id: parentSessionID },
15309
- body: {
15310
- noReply: true,
15311
- parts: [{ type: "text", text: notification }]
15312
- }
15313
- });
15314
- log(`Notified parent ${parentSessionID}: ${completedTasks.length} tasks`);
15315
- } catch (error45) {
15316
- log("Notification error:", error45);
15317
- }
15318
- this.notifications.delete(parentSessionID);
15319
- }
15320
- // ========================================================================
15321
- // Internal: Formatting
15322
- // ========================================================================
15323
- formatDuration(start, end) {
15324
- const duration3 = (end ?? /* @__PURE__ */ new Date()).getTime() - start.getTime();
15325
- const seconds = Math.floor(duration3 / 1e3);
15326
- const minutes = Math.floor(seconds / 60);
15327
- if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
15328
- return `${seconds}s`;
15329
- }
15330
- };
15331
- var parallelAgentManager = {
15332
- getInstance: ParallelAgentManager.getInstance.bind(ParallelAgentManager)
15333
- };
15334
-
15335
- // src/tools/async-agent.ts
15336
- var createDelegateTaskTool = (manager, client) => tool({
15337
- description: `Delegate a task to an agent.
15338
-
15339
- <mode>
15340
- - background=true: Non-blocking. Task runs in parallel session. Use get_task_result later.
15341
- - background=false: Blocking. Waits for result. Use when you need output immediately.
15342
- </mode>
15343
-
15344
- <when_to_use_background_true>
15345
- - Multiple independent tasks to run in parallel
15346
- - Long-running tasks (build, test, analysis)
15347
- - You have other work to do while waiting
15348
- - Example: "Build module A" + "Test module B" in parallel
15349
- </when_to_use_background_true>
15350
-
15351
- <when_to_use_background_false>
15352
- - You need the result for the very next step
15353
- - Single task with nothing else to do
15354
- - Quick questions that return fast
15355
- </when_to_use_background_false>
15356
-
15357
- <safety>
15358
- - Max 3 background tasks per agent type (extras queue automatically)
15359
- - Auto-timeout: 30 minutes
15360
- - Auto-cleanup: 5 minutes after completion
15361
- </safety>`,
15362
- args: {
15363
- agent: tool.schema.string().describe(
15364
- `Agent name (e.g., '${AGENT_NAMES.BUILDER}', '${AGENT_NAMES.INSPECTOR}', '${AGENT_NAMES.ARCHITECT}')`
15365
- ),
15366
- description: tool.schema.string().describe("Short task description"),
15367
- prompt: tool.schema.string().describe("Full prompt/instructions for the agent"),
15368
- background: tool.schema.boolean().describe(
15369
- "true=async (returns task_id), false=sync (waits for result). REQUIRED."
15370
- )
15371
- },
15372
- async execute(args, context) {
15373
- const { agent, description, prompt, background } = args;
15374
- const ctx = context;
15375
- const sessionClient = client;
15376
- if (background === void 0) {
15377
- return `\u274C 'background' parameter is REQUIRED.
15378
- - background=true: Run in parallel, returns task ID
15379
- - background=false: Wait for result (blocking)`;
15380
- }
15381
- if (background === true) {
15382
- try {
15383
- const task = await manager.launch({
15384
- agent,
15385
- description,
15386
- prompt,
15387
- parentSessionID: ctx.sessionID
15388
- });
15389
- const runningCount = manager.getRunningTasks().length;
15390
- const pendingCount = manager.getPendingCount(ctx.sessionID);
15391
- console.log(
15392
- `[parallel] \u{1F680} SPAWNED ${task.id} \u2192 ${agent}: ${description}`
15393
- );
15394
- return `
15395
- ## \u{1F680} BACKGROUND TASK SPAWNED
15396
-
15397
- **Task Details**
15398
- - **ID**: \`${task.id}\`
15399
- - **Agent**: ${agent}
15400
- - **Description**: ${description}
15401
- - **Status**: \u23F3 Running in background (non-blocking)
15402
-
15403
- **Active Tasks**
15404
- - Running: ${runningCount}
15405
- - Pending: ${pendingCount}
15406
-
15407
- ---
15408
-
15409
- **Monitoring Commands**
15410
-
15411
- Check progress anytime:
15412
- - \`list_tasks()\` - View all parallel tasks
15413
- - \`get_task_result({ taskId: "${task.id}" })\` - Get latest result
15414
- - \`cancel_task({ taskId: "${task.id}" })\` - Stop this task
15415
-
15416
- ---
15417
-
15418
- \u2713 System will notify when ALL tasks complete. You can continue working!`;
15419
- } catch (error45) {
15420
- const message = error45 instanceof Error ? error45.message : String(error45);
15421
- console.log(`[parallel] \u274C FAILED: ${message}`);
15422
- return `\u274C Failed to spawn background task: ${message}`;
15423
- }
15424
- }
15425
- console.log(`[delegate] \u23F3 SYNC ${agent}: ${description}`);
15426
- try {
15427
- const session = sessionClient.session;
15428
- const directory = ".";
15429
- const createResult = await session.create({
15430
- body: {
15431
- parentID: ctx.sessionID,
15432
- title: `Task: ${description}`
15433
- },
15434
- query: { directory }
15435
- });
15436
- if (createResult.error || !createResult.data?.id) {
15437
- return `\u274C Failed to create session: ${createResult.error || "No session ID"}`;
15438
- }
15439
- const sessionID = createResult.data.id;
15440
- const startTime = Date.now();
15441
- try {
15442
- await session.prompt({
15443
- path: { id: sessionID },
15444
- body: {
15445
- agent,
15446
- parts: [{ type: "text", text: prompt }]
15447
- }
15448
- });
15449
- } catch (promptError) {
15450
- const errorMessage = promptError instanceof Error ? promptError.message : String(promptError);
15451
- return `\u274C Failed to send prompt: ${errorMessage}
15452
-
15453
- Session ID: ${sessionID}`;
15454
- }
15455
- const POLL_INTERVAL_MS2 = 500;
15456
- const MAX_POLL_TIME_MS = 10 * 60 * 1e3;
15457
- const MIN_STABILITY_MS3 = 5e3;
15458
- const STABILITY_POLLS_REQUIRED = 3;
15459
- let stablePolls = 0;
15460
- let lastMsgCount = 0;
15461
- while (Date.now() - startTime < MAX_POLL_TIME_MS) {
15462
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS2));
15463
- const statusResult = await session.status();
15464
- const allStatuses = statusResult.data ?? {};
15465
- const sessionStatus = allStatuses[sessionID];
15466
- if (sessionStatus?.type !== "idle") {
15467
- stablePolls = 0;
15468
- lastMsgCount = 0;
15469
- continue;
15470
- }
15471
- if (Date.now() - startTime < MIN_STABILITY_MS3) continue;
15472
- const messagesResult2 = await session.messages({
15473
- path: { id: sessionID }
15474
- });
15475
- const messages2 = messagesResult2.data ?? [];
15476
- const currentMsgCount = messages2.length;
15477
- if (currentMsgCount === lastMsgCount) {
15478
- stablePolls++;
15479
- if (stablePolls >= STABILITY_POLLS_REQUIRED) break;
15480
- } else {
15481
- stablePolls = 0;
15482
- lastMsgCount = currentMsgCount;
15483
- }
15484
- }
15485
- const messagesResult = await session.messages({
15486
- path: { id: sessionID }
15487
- });
15488
- const messages = messagesResult.data ?? [];
15489
- const assistantMsgs = messages.filter((m) => m.info?.role === "assistant").reverse();
15490
- const lastMsg = assistantMsgs[0];
15491
- if (!lastMsg) {
15492
- return `\u274C No assistant response found.
15493
-
15494
- Session ID: ${sessionID}`;
15495
- }
15496
- const textParts = lastMsg.parts?.filter(
15497
- (p) => p.type === "text" || p.type === "reasoning"
15498
- ) ?? [];
15499
- const textContent = textParts.map((p) => p.text ?? "").filter(Boolean).join("\n");
15500
- const duration3 = Math.floor((Date.now() - startTime) / 1e3);
15501
- console.log(
15502
- `[delegate] \u2705 COMPLETED ${agent}: ${description} (${duration3}s)`
15503
- );
15504
- return `\u2705 **Task Completed** (${duration3}s)
15505
-
15506
- Agent: ${agent}
15507
- Session ID: ${sessionID}
15508
-
15509
- ---
15510
-
15511
- ${textContent || "(No text output)"}`;
15512
- } catch (error45) {
15513
- const message = error45 instanceof Error ? error45.message : String(error45);
15514
- console.log(`[delegate] \u274C FAILED: ${message}`);
15515
- return `\u274C Task failed: ${message}`;
15516
- }
15517
- }
15518
- });
15519
- var createGetTaskResultTool = (manager) => tool({
15520
- description: `Get the result from a completed background task.
15521
-
15522
- <note>
15523
- If the task is still running, returns status info.
15524
- Wait for the "All Complete" notification before checking.
15525
- </note>`,
15526
- args: {
15527
- taskId: tool.schema.string().describe("Task ID from delegate_task (e.g., 'task_a1b2c3d4')")
15528
- },
15529
- async execute(args) {
15530
- const { taskId } = args;
15531
- const task = manager.getTask(taskId);
15532
- if (!task) {
15533
- return `\u274C Task not found: \`${taskId}\`
15534
-
15535
- Use \`list_tasks\` to see available tasks.`;
15536
- }
15537
- if (task.status === "running") {
15538
- const elapsed = Math.floor(
15539
- (Date.now() - task.startedAt.getTime()) / 1e3
15540
- );
15541
- return `\u23F3 **Task Still Running**
15542
-
15543
- | Property | Value |
15544
- |----------|-------|
15545
- | **Task ID** | \`${taskId}\` |
15546
- | **Agent** | ${task.agent} |
15547
- | **Elapsed** | ${elapsed}s |
15548
-
15549
- Wait for "All Complete" notification, then try again.`;
15550
- }
15551
- const result = await manager.getResult(taskId);
15552
- const duration3 = manager.formatDuration(task.startedAt, task.completedAt);
15553
- if (task.status === "error" || task.status === "timeout") {
15554
- return `\u274C **Task ${task.status === "timeout" ? "Timed Out" : "Failed"}**
15555
-
15556
- | Property | Value |
15557
- |----------|-------|
15558
- | **Task ID** | \`${taskId}\` |
15559
- | **Agent** | ${task.agent} |
15560
- | **Error** | ${task.error} |
15561
- | **Duration** | ${duration3} |`;
15562
- }
15563
- return `\u2705 **Task Completed**
15564
-
15565
- | Property | Value |
15566
- |----------|-------|
15567
- | **Task ID** | \`${taskId}\` |
15568
- | **Agent** | ${task.agent} |
15569
- | **Duration** | ${duration3} |
15570
-
15571
- ---
15572
-
15573
- **Result:**
15574
-
15575
- ${result || "(No output)"}`;
15576
- }
15577
- });
15578
- var createListTasksTool = (manager) => tool({
15579
- description: `List all background tasks and their status.`,
15580
- args: {
15581
- status: tool.schema.string().optional().describe("Filter: 'all', 'running', 'completed', 'error'")
15582
- },
15583
- async execute(args) {
15584
- const { status = "all" } = args;
15585
- let tasks;
15586
- switch (status) {
15587
- case "running":
15588
- tasks = manager.getRunningTasks();
15589
- break;
15590
- case "completed":
15591
- tasks = manager.getAllTasks().filter((t) => t.status === "completed");
15592
- break;
15593
- case "error":
15594
- tasks = manager.getAllTasks().filter((t) => t.status === "error" || t.status === "timeout");
15595
- break;
15596
- default:
15597
- tasks = manager.getAllTasks();
15598
- }
15599
- if (tasks.length === 0) {
15600
- return `\u{1F4CB} No background tasks found${status !== "all" ? ` (filter: ${status})` : ""}.
15601
-
15602
- Use \`delegate_task({ ..., background: true })\` to spawn background tasks.`;
15603
- }
15604
- const runningCount = manager.getRunningTasks().length;
15605
- const completedCount = manager.getAllTasks().filter((t) => t.status === "completed").length;
15606
- const errorCount = manager.getAllTasks().filter((t) => t.status === "error" || t.status === "timeout").length;
15607
- const statusIcon = (s) => {
15608
- switch (s) {
15609
- case "running":
15610
- return "\u23F3";
15611
- case "completed":
15612
- return "\u2705";
15613
- case "error":
15614
- return "\u274C";
15615
- case "timeout":
15616
- return "\u23F1\uFE0F";
15617
- default:
15618
- return "\u2753";
15619
- }
15620
- };
15621
- const rows = tasks.map((t) => {
15622
- const elapsed = Math.floor(
15623
- (Date.now() - t.startedAt.getTime()) / 1e3
15624
- );
15625
- const desc = t.description.length > 25 ? t.description.slice(0, 22) + "..." : t.description;
15626
- return `| \`${t.id}\` | ${statusIcon(t.status)} ${t.status} | ${t.agent} | ${desc} | ${elapsed}s |`;
15627
- }).join("\n");
15628
- return `\u{1F4CB} **Background Tasks** (${tasks.length} shown)
15629
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
15630
- | \u23F3 Running: ${runningCount} | \u2705 Completed: ${completedCount} | \u274C Error: ${errorCount} |
15631
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
15632
-
15633
- | Task ID | Status | Agent | Description | Elapsed |
15634
- |---------|--------|-------|-------------|---------|
15635
- ${rows}
15636
-
15637
- \u{1F4A1} Use \`get_task_result({ taskId: "task_xxx" })\` to get results.
15638
- \u{1F6D1} Use \`cancel_task({ taskId: "task_xxx" })\` to stop a running task.`;
15639
- }
15640
- });
15641
- var createCancelTaskTool = (manager) => tool({
15642
- description: `Cancel a running background task.
15643
-
15644
- <purpose>
15645
- Stop a runaway or no-longer-needed task.
15646
- Frees up concurrency slot for other tasks.
15647
- </purpose>`,
15648
- args: {
15649
- taskId: tool.schema.string().describe("Task ID to cancel (e.g., 'task_a1b2c3d4')")
15650
- },
15651
- async execute(args) {
15652
- const { taskId } = args;
15653
- const cancelled = await manager.cancelTask(taskId);
15654
- if (cancelled) {
15655
- return `\u{1F6D1} **Task Cancelled**
15656
-
15657
- Task \`${taskId}\` has been stopped. Concurrency slot released.`;
15658
- }
15659
- const task = manager.getTask(taskId);
15660
- if (task) {
15661
- return `\u26A0\uFE0F Cannot cancel: Task \`${taskId}\` is ${task.status} (not running).`;
15662
- }
15663
- return `\u274C Task \`${taskId}\` not found.
15664
-
15665
- Use \`list_tasks\` to see available tasks.`;
15666
- }
15667
- });
15668
- function createAsyncAgentTools(manager, client) {
15669
- return {
15670
- delegate_task: createDelegateTaskTool(manager, client),
15671
- get_task_result: createGetTaskResultTool(manager),
15672
- list_tasks: createListTasksTool(manager),
15673
- cancel_task: createCancelTaskTool(manager)
15674
- };
15675
- }
15676
-
15677
- // src/core/batch-processor.ts
15678
- var SmartBatchProcessor = class {
15679
- parallelAgentManager;
15680
- tasks = /* @__PURE__ */ new Map();
15681
- constructor(parallelAgentManager2) {
15682
- this.parallelAgentManager = parallelAgentManager2;
15683
- }
15684
- /**
15685
- * Process a batch of tasks with smart validation
15686
- */
15687
- async processBatch(tasks, options = {
15688
- concurrency: 3,
15689
- maxRetries: 2,
15690
- validateAfterEach: false,
15691
- continueOnError: true
15692
- }) {
15693
- const startTime = Date.now();
15694
- for (const task of tasks) {
15695
- this.tasks.set(task.id, {
15696
- ...task,
15697
- status: "pending",
15698
- attempts: 0
15699
- });
15700
- }
15701
- console.log(`
15702
- \u{1F4E6} [Smart Batch] Starting ${tasks.length} tasks with concurrency ${options.concurrency}`);
15703
- console.log(` Strategy: ${options.validateAfterEach ? "Validate each" : "Centralized validation"}`);
15704
- await this.executePhase(tasks, options, "initial");
15705
- const failedTasks = Array.from(this.tasks.values()).filter((t) => t.status === "failed" || t.status === "pending");
15706
- if (failedTasks.length === 0) {
15707
- console.log(`
15708
- \u2705 [Smart Batch] All ${tasks.length} tasks succeeded!`);
15709
- return this.buildResult(startTime, tasks);
15710
- }
15711
- console.log(`
15712
- \u{1F50D} [Smart Batch] Validation complete: ${failedTasks.length}/${tasks.length} tasks need retry`);
15713
- let retryCount = 0;
15714
- while (failedTasks.length > 0 && retryCount < options.maxRetries) {
15715
- retryCount++;
15716
- console.log(`
15717
- \u{1F504} [Smart Batch] Retry round ${retryCount}/${options.maxRetries} for ${failedTasks.length} tasks`);
15718
- await this.executePhase(failedTasks, options, `retry-${retryCount}`);
15719
- const stillFailed = Array.from(this.tasks.values()).filter((t) => t.status === "failed");
15720
- if (stillFailed.length === failedTasks.length) {
15721
- console.log(`\u26A0\uFE0F [Smart Batch] No progress in retry round ${retryCount}, stopping`);
15722
- break;
15723
- }
15724
- }
15725
- return this.buildResult(startTime, tasks);
15726
- }
15727
- /**
15728
- * Execute a phase with concurrency control
15729
- */
15730
- async executePhase(tasks, options, phase) {
15731
- for (const task of tasks) {
15732
- this.parallelAgentManager.setConcurrencyLimit(task.agent, options.concurrency);
15733
- }
15734
- const batches = [];
15735
- for (let i = 0; i < tasks.length; i += options.concurrency) {
15736
- batches.push(tasks.slice(i, i + options.concurrency));
15737
- }
15738
- for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
15739
- const batch = batches[batchIndex];
15740
- console.log(` [${phase}] Processing batch ${batchIndex + 1}/${batches.length} (${batch.length} tasks)`);
15741
- const batchPromises = batch.map((task) => this.executeTask(task, options));
15742
- await Promise.all(batchPromises);
15743
- if (!options.validateAfterEach) {
15744
- continue;
15745
- }
15746
- const failedInBatch = batch.filter((t) => t.status === "failed");
15747
- if (failedInBatch.length > 0) {
15748
- console.log(` [${phase}] ${failedInBatch.length} failed in batch, immediate retry`);
15749
- const retryPromises = failedInBatch.map((t) => this.executeTask(t, options));
15750
- await Promise.all(retryPromises);
15751
- }
15752
- }
15753
- }
15754
- /**
15755
- * Execute a single task
15756
- */
15757
- async executeTask(task, options) {
15758
- const currentTask = this.tasks.get(task.id);
15759
- if (!options.continueOnError && currentTask.status === "failed") {
15760
- return;
15761
- }
15762
- try {
15763
- currentTask.attempts++;
15764
- currentTask.status = "pending";
15765
- const launched = await this.parallelAgentManager.launch({
15766
- agent: task.agent,
15767
- description: task.description,
15768
- prompt: task.prompt,
15769
- parentSessionID: ""
15770
- // Would need actual parent ID
15771
- });
15772
- const maxWaitTime = 5 * 60 * 1e3;
15773
- const startTime = Date.now();
15774
- while (Date.now() - startTime < maxWaitTime) {
15775
- const taskData = this.parallelAgentManager.getTask(launched.id);
15776
- if (!taskData) break;
15777
- if (taskData.status === "completed") {
15778
- currentTask.status = "success";
15779
- console.log(` \u2705 [${task.id}] Success on attempt ${currentTask.attempts}`);
15780
- return;
15781
- }
15782
- if (taskData.status === "error" || taskData.status === "timeout") {
15783
- currentTask.status = "failed";
15784
- currentTask.error = taskData.error || "Unknown error";
15785
- console.log(` \u274C [${task.id}] Failed: ${currentTask.error}`);
15786
- return;
14164
+ try {
14165
+ await this.client.session.delete({ path: { id: sessionID } });
14166
+ } catch {
15787
14167
  }
15788
- await new Promise((resolve) => setTimeout(resolve, 1e3));
15789
14168
  }
15790
- currentTask.status = "failed";
15791
- currentTask.error = "Timeout";
14169
+ this.store.delete(taskId);
14170
+ log2(`Cleaned up ${taskId}`);
14171
+ }, CONFIG.CLEANUP_DELAY_MS);
14172
+ }
14173
+ async notifyParentIfAllComplete(parentSessionID) {
14174
+ if (this.store.hasPending(parentSessionID)) return;
14175
+ const notifications = this.store.getNotifications(parentSessionID);
14176
+ if (notifications.length === 0) return;
14177
+ const message = buildNotificationMessage(notifications);
14178
+ try {
14179
+ await this.client.session.prompt({
14180
+ path: { id: parentSessionID },
14181
+ body: { noReply: true, parts: [{ type: "text", text: message }] }
14182
+ });
14183
+ log2(`Notified parent ${parentSessionID}`);
15792
14184
  } catch (error45) {
15793
- currentTask.status = "failed";
15794
- currentTask.error = error45 instanceof Error ? error45.message : String(error45);
15795
- console.log(` \u274C [${task.id}] Exception: ${currentTask.error}`);
14185
+ log2("Notification error:", error45);
15796
14186
  }
15797
- }
15798
- /**
15799
- * Build result summary
15800
- */
15801
- buildResult(startTime, tasks) {
15802
- const allTasks = Array.from(this.tasks.values());
15803
- const successCount = allTasks.filter((t) => t.status === "success").length;
15804
- const failedCount = allTasks.filter((t) => t.status === "failed").length;
15805
- const retriedCount = allTasks.filter((t) => t.attempts > 1).length;
15806
- return {
15807
- total: tasks.length,
15808
- success: successCount,
15809
- failed: failedCount,
15810
- retried: retriedCount,
15811
- duration: Date.now() - startTime,
15812
- tasks: allTasks
15813
- };
15814
- }
15815
- /**
15816
- * Export failed tasks for manual review
15817
- */
15818
- exportFailedTasks() {
15819
- const failedTasks = Array.from(this.tasks.values()).filter((t) => t.status === "failed");
15820
- if (failedTasks.length === 0) {
15821
- return "\u2705 No failed tasks to export.";
15822
- }
15823
- const output = failedTasks.map((task) => {
15824
- return `
15825
- ---
15826
- Task ID: ${task.id}
15827
- Agent: ${task.agent}
15828
- Attempts: ${task.attempts}
15829
- Error: ${task.error}
15830
- Description: ${task.description}
15831
- ---
15832
- `.trim();
15833
- }).join("\n");
15834
- return `\u274C ${failedTasks.length} failed tasks:
15835
- ${output}`;
15836
- }
15837
- /**
15838
- * Clear all tasks
15839
- */
15840
- clear() {
15841
- this.tasks.clear();
14187
+ this.store.clearNotifications(parentSessionID);
15842
14188
  }
15843
14189
  };
14190
+ var parallelAgentManager = {
14191
+ getInstance: ParallelAgentManager.getInstance.bind(ParallelAgentManager)
14192
+ };
15844
14193
 
15845
- // src/tools/batch.ts
15846
- var createProcessBatchTool = (parallelAgentManager2, client) => tool({
15847
- description: `Execute a batch of tasks with intelligent validation and retry.
15848
-
15849
- <strategy>
15850
- 1. Execute all tasks in parallel (respecting concurrency limits)
15851
- 2. Centralized validation: Identify ALL failed tasks at once
15852
- 3. Retry ONLY failed tasks (not everything)
15853
- </strategy>
14194
+ // src/tools/parallel/delegate-task.ts
14195
+ var createDelegateTaskTool = (manager, client) => tool({
14196
+ description: `Delegate a task to an agent.
15854
14197
 
15855
- <benefits>
15856
- - Faster than naive retry: Failed tasks batch-identified
15857
- - More efficient: No redundant work on successful tasks
15858
- - Controlled: Concurrency limits prevent API overload
15859
- </benefits>
14198
+ <mode>
14199
+ - background=true: Non-blocking. Returns task ID immediately.
14200
+ - background=false: Blocking. Waits for result.
14201
+ </mode>
15860
14202
 
15861
- <examples>
15862
- process_batch({
15863
- concurrency: 10,
15864
- tasks: [
15865
- { id: "task1", agent: "builder", description: "Test A", prompt: "..." },
15866
- { id: "task2", agent: "inspector", description: "Test B", prompt: "..." }
15867
- ]
15868
- })
15869
- </examples>`,
14203
+ <safety>
14204
+ - Max 3 tasks per agent type
14205
+ - Auto-timeout: 30 minutes
14206
+ </safety>`,
15870
14207
  args: {
15871
- concurrency: tool.schema.string().describe("Concurrency limit (default: 3, max: 10)"),
15872
- maxRetries: tool.schema.string().optional().describe("Maximum retry rounds (default: 2)"),
15873
- validateAfterEach: tool.schema.boolean().optional().describe("Validate after each task (default: false = centralized validation)"),
15874
- tasks: tool.schema.string().describe("Array of task objects in JSON format")
14208
+ agent: tool.schema.string().describe("Agent name"),
14209
+ description: tool.schema.string().describe("Task description"),
14210
+ prompt: tool.schema.string().describe("Prompt for the agent"),
14211
+ background: tool.schema.boolean().describe("true=async, false=sync")
15875
14212
  },
15876
- async execute(args) {
15877
- const { concurrency, maxRetries = "2", validateAfterEach = false, tasks } = args;
15878
- let taskList;
15879
- try {
15880
- taskList = JSON.parse(tasks);
15881
- } catch {
15882
- return "\u274C Invalid tasks JSON. Must be valid array.";
15883
- }
15884
- if (!Array.isArray(taskList)) {
15885
- return "\u274C tasks must be an array of task objects.";
14213
+ async execute(args, context) {
14214
+ const { agent, description, prompt, background } = args;
14215
+ const ctx = context;
14216
+ const sessionClient = client;
14217
+ if (background === void 0) {
14218
+ return `\u274C 'background' parameter is REQUIRED.`;
15886
14219
  }
15887
- for (const task of taskList) {
15888
- if (!task.id || !task.agent || !task.description || !task.prompt) {
15889
- return `\u274C Task missing required fields (id, agent, description, prompt)`;
14220
+ if (background === true) {
14221
+ try {
14222
+ const task = await manager.launch({
14223
+ agent,
14224
+ description,
14225
+ prompt,
14226
+ parentSessionID: ctx.sessionID
14227
+ });
14228
+ return `\u{1F680} Task spawned: \`${task.id}\` (${agent})`;
14229
+ } catch (error45) {
14230
+ return `\u274C Failed: ${error45 instanceof Error ? error45.message : String(error45)}`;
15890
14231
  }
15891
14232
  }
15892
- const numConcurrency = parseInt(concurrency, 10);
15893
- const numRetries = parseInt(maxRetries, 10);
15894
- if (isNaN(numConcurrency) || numConcurrency < 1 || numConcurrency > 10) {
15895
- return "\u274C Invalid concurrency. Must be 1-10.";
15896
- }
15897
- if (isNaN(numRetries) || numRetries < 0 || numRetries > 5) {
15898
- return "\u274C Invalid maxRetries. Must be 0-5.";
15899
- }
15900
- const processor = new SmartBatchProcessor(parallelAgentManager2);
15901
- const result = await processor.processBatch(taskList, {
15902
- concurrency: numConcurrency,
15903
- maxRetries: numRetries,
15904
- validateAfterEach,
15905
- continueOnError: true
15906
- });
15907
- const durationSecs = Math.floor(result.duration / 1e3);
15908
- const successRate = (result.success / result.total * 100).toFixed(1);
15909
- let output = `\u2705 **Batch Processing Complete**
15910
-
15911
- | Metric | Value |
15912
- |---------|--------|
15913
- | **Total Tasks** | ${result.total} |
15914
- | **Successful** | ${result.success} (${successRate}%) |
15915
- | **Failed** | ${result.failed} |
15916
- | **Retried** | ${result.retried} |
15917
- | **Duration** | ${durationSecs}s |
15918
-
15919
- `;
15920
- if (result.failed > 0) {
15921
- output += `\u26A0\uFE0F **Failed Tasks**
15922
-
15923
- Use \`export_failed_tasks()\` to review failed tasks and manually fix issues.
15924
-
15925
- ---
14233
+ try {
14234
+ const session = sessionClient.session;
14235
+ const createResult = await session.create({
14236
+ body: { parentID: ctx.sessionID, title: `Task: ${description}` },
14237
+ query: { directory: "." }
14238
+ });
14239
+ if (createResult.error || !createResult.data?.id) {
14240
+ return `\u274C Failed to create session`;
14241
+ }
14242
+ const sessionID = createResult.data.id;
14243
+ const startTime = Date.now();
14244
+ await session.prompt({
14245
+ path: { id: sessionID },
14246
+ body: { agent, parts: [{ type: "text", text: prompt }] }
14247
+ });
14248
+ let stablePolls = 0, lastMsgCount = 0;
14249
+ while (Date.now() - startTime < 10 * 60 * 1e3) {
14250
+ await new Promise((r) => setTimeout(r, 500));
14251
+ const statusResult = await session.status();
14252
+ if (statusResult.data?.[sessionID]?.type !== "idle") {
14253
+ stablePolls = 0;
14254
+ continue;
14255
+ }
14256
+ if (Date.now() - startTime < 5e3) continue;
14257
+ const msgs2 = await session.messages({ path: { id: sessionID } });
14258
+ const count = (msgs2.data ?? []).length;
14259
+ if (count === lastMsgCount) {
14260
+ stablePolls++;
14261
+ if (stablePolls >= 3) break;
14262
+ } else {
14263
+ stablePolls = 0;
14264
+ lastMsgCount = count;
14265
+ }
14266
+ }
14267
+ const msgs = await session.messages({ path: { id: sessionID } });
14268
+ const messages = msgs.data ?? [];
14269
+ const lastMsg = messages.filter((m) => m.info?.role === "assistant").reverse()[0];
14270
+ const text = lastMsg?.parts?.filter((p) => p.type === "text" || p.type === "reasoning").map((p) => p.text ?? "").join("\n") || "";
14271
+ return `\u2705 Completed (${Math.floor((Date.now() - startTime) / 1e3)}s)
15926
14272
 
15927
- Failed Task IDs:
15928
- ${result.tasks.filter((t) => t.status === "failed").map((t) => ` - ${t.id}`).join("\n")}
15929
- ---
15930
- `;
14273
+ ${text || "(No output)"}`;
14274
+ } catch (error45) {
14275
+ return `\u274C Failed: ${error45 instanceof Error ? error45.message : String(error45)}`;
15931
14276
  }
15932
- return output;
15933
14277
  }
15934
14278
  });
15935
- var createExportFailedTasksTool = (parallelAgentManager2) => tool({
15936
- description: `Export failed tasks from the last batch for manual review.`,
15937
- args: {},
15938
- async execute() {
15939
- const processor = new SmartBatchProcessor(parallelAgentManager2);
15940
- return processor.exportFailedTasks();
15941
- }
15942
- });
15943
- var createCompareStrategiesTool = (parallelAgentManager2) => tool({
15944
- description: `Compare naive retry vs smart batch validation performance.
15945
14279
 
15946
- <comparison>
15947
- **Naive Strategy** (current):
15948
- - Concurrency: 3 fixed
15949
- - Retry: Per-task immediate retry
15950
- - Issue: Slow for large batches, redundant work
15951
-
15952
- **Smart Batch Strategy** (new):
15953
- - Concurrency: Configurable (up to 10)
15954
- - Validation: Centralized, batch-identify failures
15955
- - Retry: Only failed tasks
15956
- - Benefit: Faster, less redundant work
15957
- </comparison>`,
14280
+ // src/tools/parallel/get-task-result.ts
14281
+ var createGetTaskResultTool = (manager) => tool({
14282
+ description: `Get result from a completed background task.`,
15958
14283
  args: {
15959
- taskCount: tool.schema.string().describe("Number of simulated tasks (default: 100)"),
15960
- concurrency1: tool.schema.string().optional().describe("Strategy 1 concurrency (default: 3)"),
15961
- concurrency2: tool.schema.string().optional().describe("Strategy 2 concurrency (default: 10)")
14284
+ taskId: tool.schema.string().describe("Task ID")
15962
14285
  },
15963
14286
  async execute(args) {
15964
- const taskCount = parseInt(args.taskCount || "100", 10);
15965
- const concurrency1 = parseInt(args.concurrency1 || "3", 10);
15966
- const concurrency2 = parseInt(args.concurrency2 || "10", 10);
15967
- const naiveBatches = Math.ceil(taskCount / concurrency1);
15968
- const naiveTime = naiveBatches * 60;
15969
- const smartBatches = Math.ceil(taskCount / concurrency2);
15970
- const smartTime = smartBatches * 60 + 30;
15971
- const timeDiff = naiveTime - smartTime;
15972
- const improvement = (timeDiff / naiveTime * 100).toFixed(1);
15973
- return `\u{1F4CA} **Strategy Comparison for ${taskCount} tasks**
15974
-
15975
- **Strategy 1: Naive (Current)**
15976
- | Metric | Value |
15977
- |---------|--------|
15978
- | Concurrency | ${concurrency1} |
15979
- | Batches | ${naiveBatches} |
15980
- | Est. Time | ${naiveTime}s |
15981
-
15982
- **Strategy 2: Smart Batch (Proposed)**
15983
- | Metric | Value |
15984
- |---------|--------|
15985
- | Concurrency | ${concurrency2} |
15986
- | Batches | ${smartBatches} |
15987
- | Est. Time | ${smartTime}s |
15988
-
15989
- **Summary**
15990
- | Metric | Value |
15991
- |---------|--------|
15992
- | Time Saved | ${timeDiff}s (${improvement}%) |
15993
- | Batches Saved | ${naiveBatches - smartBatches} |
15994
-
15995
- Recommendation: Use **Smart Batch** with concurrency ${concurrency2} for ${timeDiff}s improvement.`;
15996
- }
15997
- });
15998
- function createBatchTools(parallelAgentManager2, client) {
15999
- return {
16000
- process_batch: createProcessBatchTool(parallelAgentManager2, client),
16001
- export_failed_tasks: createExportFailedTasksTool(parallelAgentManager2),
16002
- compare_strategies: createCompareStrategiesTool(parallelAgentManager2)
16003
- };
16004
- }
16005
-
16006
- // src/tools/config.ts
16007
- var createShowConfigTool = () => tool({
16008
- description: `Display current OpenCode Orchestrator configuration.
14287
+ const task = manager.getTask(args.taskId);
14288
+ if (!task) return `\u274C Task not found: \`${args.taskId}\``;
14289
+ if (task.status === "running") return `\u23F3 Still running...`;
14290
+ const result = await manager.getResult(args.taskId);
14291
+ const duration3 = manager.formatDuration(task.startedAt, task.completedAt);
14292
+ if (task.status === "error" || task.status === "timeout") {
14293
+ return `\u274C ${task.status}: ${task.error}`;
14294
+ }
14295
+ return `\u2705 Completed (${duration3})
16009
14296
 
16010
- Shows all dynamic settings including timeouts, concurrency limits, and debug flags.`,
16011
- args: {},
16012
- async execute() {
16013
- configManager.exportConfigs();
16014
- return "";
14297
+ ${result || "(No output)"}`;
16015
14298
  }
16016
14299
  });
16017
- var createSetConcurrencyTool = (client) => tool({
16018
- description: `Update concurrency limit for a specific agent type.
16019
14300
 
16020
- <examples>
16021
- set_concurrency({ agent: "builder", limit: 5 })
16022
- set_concurrency({ agent: "inspector", limit: 2 })
16023
- </examples>
16024
-
16025
- <notes>
16026
- - Changes take effect immediately
16027
- - Queued tasks will start when slots become available
16028
- - Limit cannot exceed global max (10)
16029
- </notes>`,
14301
+ // src/tools/parallel/list-tasks.ts
14302
+ var createListTasksTool = (manager) => tool({
14303
+ description: `List all background tasks.`,
16030
14304
  args: {
16031
- agent: tool.schema.string().describe('Agent type (e.g., "builder", "inspector", "architect")'),
16032
- limit: tool.schema.string().describe("New concurrency limit (number)")
14305
+ status: tool.schema.string().optional().describe("Filter: all, running, completed, error")
16033
14306
  },
16034
14307
  async execute(args) {
16035
- const { agent, limit } = args;
16036
- const numLimit = parseInt(limit, 10);
16037
- if (isNaN(numLimit) || numLimit < 1) {
16038
- return `\u274C Invalid limit: "${limit}". Must be a number >= 1.`;
16039
- }
16040
- const maxLimit = configManager.getParallelAgentConfig().maxConcurrency;
16041
- if (numLimit > maxLimit) {
16042
- return `\u274C Limit ${numLimit} exceeds global max ${maxLimit}. Using ${maxLimit}.`;
14308
+ const { status = "all" } = args;
14309
+ let tasks;
14310
+ switch (status) {
14311
+ case "running":
14312
+ tasks = manager.getRunningTasks();
14313
+ break;
14314
+ case "completed":
14315
+ tasks = manager.getAllTasks().filter((t) => t.status === "completed");
14316
+ break;
14317
+ case "error":
14318
+ tasks = manager.getAllTasks().filter((t) => t.status === "error" || t.status === "timeout");
14319
+ break;
14320
+ default:
14321
+ tasks = manager.getAllTasks();
16043
14322
  }
16044
- configManager.updateParallelAgentConfig({
16045
- defaultConcurrency: numLimit
16046
- });
16047
- return `\u2705 **Concurrency Updated**
16048
-
16049
- | Property | Value |
16050
- |----------|-------|
16051
- | **Agent** | ${agent} |
16052
- | **New Limit** | ${numLimit} parallel tasks |
14323
+ if (tasks.length === 0) return `\u{1F4CB} No tasks found.`;
14324
+ const rows = tasks.map((t) => {
14325
+ const elapsed = Math.floor((Date.now() - t.startedAt.getTime()) / 1e3);
14326
+ return `| \`${t.id}\` | ${getStatusEmoji(t.status)} ${t.status} | ${t.agent} | ${elapsed}s |`;
14327
+ }).join("\n");
14328
+ return `\u{1F4CB} **Tasks**
16053
14329
 
16054
- Changes take effect immediately. New tasks will respect to the new limit.`;
14330
+ | ID | Status | Agent | Time |
14331
+ |----|--------|-------|------|
14332
+ ${rows}`;
16055
14333
  }
16056
14334
  });
16057
- var createSetTimeoutTool = () => tool({
16058
- description: `Update task timeout duration.
16059
14335
 
16060
- <examples>
16061
- set_timeout({ taskTtlMinutes: 45 })
16062
- set_timeout({ cleanupDelayMinutes: 2 })
16063
- </examples>`,
14336
+ // src/tools/parallel/cancel-task.ts
14337
+ var createCancelTaskTool = (manager) => tool({
14338
+ description: `Cancel a running task.`,
16064
14339
  args: {
16065
- taskTtlMinutes: tool.schema.string().optional().describe("Task timeout in minutes (default: 30)"),
16066
- cleanupDelayMinutes: tool.schema.string().optional().describe("Cleanup delay after completion in minutes (default: 5)")
14340
+ taskId: tool.schema.string().describe("Task ID to cancel")
16067
14341
  },
16068
14342
  async execute(args) {
16069
- const { taskTtlMinutes, cleanupDelayMinutes } = args;
16070
- const updates = {};
16071
- if (taskTtlMinutes) {
16072
- const ttl = parseInt(taskTtlMinutes, 10);
16073
- if (isNaN(ttl) || ttl < 1) {
16074
- return `\u274C Invalid taskTtlMinutes: "${taskTtlMinutes}". Must be number >= 1.`;
16075
- }
16076
- updates.taskTtlMs = ttl * 60 * 1e3;
16077
- }
16078
- if (cleanupDelayMinutes) {
16079
- const delay = parseInt(cleanupDelayMinutes, 10);
16080
- if (isNaN(delay) || delay < 0) {
16081
- return `\u274C Invalid cleanupDelayMinutes: "${cleanupDelayMinutes}". Must be number >= 0.`;
16082
- }
16083
- updates.cleanupDelayMs = delay * 60 * 1e3;
16084
- }
16085
- if (Object.keys(updates).length === 0) {
16086
- return "\u26A0\uFE0F No changes specified. Provide at least one parameter.";
16087
- }
16088
- configManager.updateParallelAgentConfig(updates);
16089
- let output = "\u2705 **Timeout Configuration Updated**\n\n";
16090
- if (updates.taskTtlMs) {
16091
- output += `| Task TTL | ${updates.taskTtlMs / 60 / 1e3} minutes |
16092
- `;
16093
- }
16094
- if (updates.cleanupDelayMs) {
16095
- output += `| Cleanup Delay | ${updates.cleanupDelayMs / 60 / 1e3} minutes |
16096
- `;
16097
- }
16098
- output += "\nChanges take effect immediately.";
16099
- return output;
14343
+ const cancelled = await manager.cancelTask(args.taskId);
14344
+ if (cancelled) return `\u{1F6D1} Cancelled: \`${args.taskId}\``;
14345
+ const task = manager.getTask(args.taskId);
14346
+ if (task) return `\u26A0\uFE0F Cannot cancel: Task is ${task.status}`;
14347
+ return `\u274C Not found: \`${args.taskId}\``;
16100
14348
  }
16101
14349
  });
16102
- var createSetDebugTool = () => tool({
16103
- description: `Enable or disable debug logging.
16104
-
16105
- <examples>
16106
- set_debug({ component: "parallel_agent", enable: true })
16107
- set_debug({ component: "background_task", enable: false })
16108
- </examples>`,
16109
- args: {
16110
- component: tool.schema.string().describe('Component to debug: "parallel_agent", "background_task", or "all"'),
16111
- enable: tool.schema.boolean().describe("Enable (true) or disable (false) debug logs")
16112
- },
16113
- async execute(args) {
16114
- const { component, enable } = args;
16115
- const enableValue = enable ? "true" : "false";
16116
- if (component === "parallel_agent" || component === "all") {
16117
- process.env.OPENCODE_DEBUG_PARALLEL = enableValue;
16118
- configManager.updateParallelAgentConfig({
16119
- enableDebug: enable
16120
- });
16121
- }
16122
- if (component === "background_task" || component === "all") {
16123
- process.env.OPENCODE_DEBUG_BACKGROUND = enableValue;
16124
- configManager.updateBackgroundTaskConfig({
16125
- enableDebug: enable
16126
- });
16127
- }
16128
- return `\u2705 **Debug Logging ${enable ? "Enabled" : "Disabled"}**
16129
-
16130
- | Component | Debug Status |
16131
- |-----------|--------------|
16132
- | ${component === "all" ? "parallel_agent" : component} | ${enable ? "\u{1F527} Enabled" : "\u{1F507} Disabled"} |
16133
- ${component === "all" ? "| background_task | \u{1F527} Enabled |" : ""}
16134
14350
 
16135
- Changes take effect immediately.`;
16136
- }
16137
- });
16138
- function createConfigTools(client) {
14351
+ // src/tools/parallel/index.ts
14352
+ function createAsyncAgentTools(manager, client) {
16139
14353
  return {
16140
- show_config: createShowConfigTool(),
16141
- set_concurrency: createSetConcurrencyTool(client),
16142
- set_timeout: createSetTimeoutTool(),
16143
- set_debug: createSetDebugTool()
14354
+ delegate_task: createDelegateTaskTool(manager, client),
14355
+ get_task_result: createGetTaskResultTool(manager),
14356
+ list_tasks: createListTasksTool(manager),
14357
+ cancel_task: createCancelTaskTool(manager)
16144
14358
  };
16145
14359
  }
16146
14360
 
@@ -16167,16 +14381,120 @@ function formatElapsedTime(startMs, endMs = Date.now()) {
16167
14381
  return parts.join(" ");
16168
14382
  }
16169
14383
 
14384
+ // src/utils/sanity.ts
14385
+ function checkOutputSanity(text) {
14386
+ if (!text || text.length < 50) {
14387
+ return { isHealthy: true, severity: "ok" };
14388
+ }
14389
+ if (/(.)\1{15,}/.test(text)) {
14390
+ return {
14391
+ isHealthy: false,
14392
+ reason: "Single character repetition detected",
14393
+ severity: "critical"
14394
+ };
14395
+ }
14396
+ if (/(.{2,6})\1{8,}/.test(text)) {
14397
+ return {
14398
+ isHealthy: false,
14399
+ reason: "Pattern loop detected",
14400
+ severity: "critical"
14401
+ };
14402
+ }
14403
+ if (text.length > 200) {
14404
+ const cleanText = text.replace(/\s/g, "");
14405
+ if (cleanText.length > 100) {
14406
+ const uniqueChars = new Set(cleanText).size;
14407
+ const ratio = uniqueChars / cleanText.length;
14408
+ if (ratio < 0.02) {
14409
+ return {
14410
+ isHealthy: false,
14411
+ reason: "Low information density",
14412
+ severity: "critical"
14413
+ };
14414
+ }
14415
+ }
14416
+ }
14417
+ const boxChars = (text.match(/[\u2500-\u257f\u2580-\u259f\u2800-\u28ff]/g) || []).length;
14418
+ if (boxChars > 100 && boxChars / text.length > 0.3) {
14419
+ return {
14420
+ isHealthy: false,
14421
+ reason: "Visual gibberish detected",
14422
+ severity: "critical"
14423
+ };
14424
+ }
14425
+ const lines = text.split("\n").filter((l) => l.trim().length > 10);
14426
+ if (lines.length > 10) {
14427
+ const lineSet = new Set(lines);
14428
+ if (lineSet.size < lines.length * 0.2) {
14429
+ return {
14430
+ isHealthy: false,
14431
+ reason: "Excessive line repetition",
14432
+ severity: "warning"
14433
+ };
14434
+ }
14435
+ }
14436
+ const cjkChars = (text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length;
14437
+ if (cjkChars > 200) {
14438
+ const uniqueCjk = new Set(
14439
+ text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []
14440
+ ).size;
14441
+ if (uniqueCjk < 10 && cjkChars / uniqueCjk > 20) {
14442
+ return {
14443
+ isHealthy: false,
14444
+ reason: "CJK character spam detected",
14445
+ severity: "critical"
14446
+ };
14447
+ }
14448
+ }
14449
+ return { isHealthy: true, severity: "ok" };
14450
+ }
14451
+ var RECOVERY_PROMPT = `<anomaly_recovery>
14452
+ \u26A0\uFE0F SYSTEM NOTICE: Previous output was malformed (gibberish/loop detected).
14453
+
14454
+ <recovery_protocol>
14455
+ 1. DISCARD the corrupted output completely - do not reference it
14456
+ 2. RECALL the original mission objective
14457
+ 3. IDENTIFY the last confirmed successful step
14458
+ 4. RESTART with a simpler, more focused approach
14459
+ </recovery_protocol>
14460
+
14461
+ <instructions>
14462
+ - If a sub-agent produced bad output: try a different agent or simpler task
14463
+ - If stuck in a loop: break down the task into smaller pieces
14464
+ - If context seems corrupted: call recorder to restore context
14465
+ - THINK in English for maximum stability
14466
+ </instructions>
14467
+
14468
+ What was the original task? Proceed from the last known good state.
14469
+ </anomaly_recovery>`;
14470
+ var ESCALATION_PROMPT = `<critical_anomaly>
14471
+ \u{1F6A8} CRITICAL: Multiple consecutive malformed outputs detected.
14472
+
14473
+ <emergency_protocol>
14474
+ 1. STOP current execution path immediately
14475
+ 2. DO NOT continue with the same approach - it is failing
14476
+ 3. CALL architect for a completely new strategy
14477
+ 4. If architect also fails: report status to user and await guidance
14478
+ </emergency_protocol>
14479
+
14480
+ <diagnosis>
14481
+ The current approach is producing corrupted output.
14482
+ This may indicate: context overload, model instability, or task complexity.
14483
+ </diagnosis>
14484
+
14485
+ Request a fresh plan from architect with reduced scope.
14486
+ </critical_anomaly>`;
14487
+
16170
14488
  // src/index.ts
16171
14489
  var PLUGIN_VERSION = "0.2.4";
16172
14490
  var DEFAULT_MAX_STEPS = 500;
16173
14491
  var TASK_COMMAND_MAX_STEPS = 1e3;
16174
14492
  var AGENT_EMOJI2 = {
16175
- [AGENT_NAMES.ARCHITECT]: "\u{1F3D7}\uFE0F",
16176
- [AGENT_NAMES.BUILDER]: "\u{1F528}",
16177
- [AGENT_NAMES.INSPECTOR]: "\u{1F50D}",
16178
- [AGENT_NAMES.RECORDER]: "\u{1F4BE}",
16179
- [AGENT_NAMES.COMMANDER]: "\u{1F3AF}"
14493
+ "architect": "\u{1F3D7}\uFE0F",
14494
+ "builder": "\u{1F528}",
14495
+ "inspector": "\u{1F50D}",
14496
+ "recorder": "\u{1F4BE}",
14497
+ "commander": "\u{1F3AF}"
16180
14498
  };
16181
14499
  var CONTINUE_INSTRUCTION = `<auto_continue>
16182
14500
  <status>Mission not complete. Keep executing.</status>
@@ -16200,8 +14518,6 @@ var OrchestratorPlugin = async (input) => {
16200
14518
  const sessions = /* @__PURE__ */ new Map();
16201
14519
  const parallelAgentManager2 = ParallelAgentManager.getInstance(client, directory);
16202
14520
  const asyncAgentTools = createAsyncAgentTools(parallelAgentManager2, client);
16203
- const batchTools = createBatchTools(parallelAgentManager2, client);
16204
- const configTools = createConfigTools(client);
16205
14521
  return {
16206
14522
  // -----------------------------------------------------------------
16207
14523
  // Tools we expose to the LLM
@@ -16219,13 +14535,7 @@ var OrchestratorPlugin = async (input) => {
16219
14535
  list_background: listBackgroundTool,
16220
14536
  kill_background: killBackgroundTool,
16221
14537
  // Async agent tools - spawn agents in parallel sessions
16222
- ...asyncAgentTools,
16223
- // Git tools - branch info and status
16224
- git_branch: gitBranchTool(directory),
16225
- // Smart batch tools - centralized validation and retry
16226
- ...batchTools,
16227
- // Configuration tools - dynamic runtime settings
16228
- ...configTools
14538
+ ...asyncAgentTools
16229
14539
  },
16230
14540
  // -----------------------------------------------------------------
16231
14541
  // Config hook - registers our commands and agents with OpenCode
@@ -16243,7 +14553,7 @@ var OrchestratorPlugin = async (input) => {
16243
14553
  }
16244
14554
  const orchestratorAgents = {
16245
14555
  Commander: {
16246
- name: AGENT_NAMES.COMMANDER,
14556
+ name: "Commander",
16247
14557
  description: "Autonomous orchestrator - executes until mission complete",
16248
14558
  systemPrompt: AGENTS.commander.systemPrompt
16249
14559
  }
@@ -16263,7 +14573,7 @@ var OrchestratorPlugin = async (input) => {
16263
14573
  const parsed = detectSlashCommand(originalText);
16264
14574
  const sessionID = msgInput.sessionID;
16265
14575
  const agentName = (msgInput.agent || "").toLowerCase();
16266
- if (agentName === AGENT_NAMES.COMMANDER && !sessions.has(sessionID)) {
14576
+ if (agentName === "commander" && !sessions.has(sessionID)) {
16267
14577
  const now = Date.now();
16268
14578
  sessions.set(sessionID, {
16269
14579
  active: true,
@@ -16537,16 +14847,16 @@ ${stateSession.graph.getTaskSummary()}`;
16537
14847
  // Event handler - cleans up when sessions are deleted
16538
14848
  // -----------------------------------------------------------------
16539
14849
  handler: async ({ event }) => {
14850
+ try {
14851
+ const manager = ParallelAgentManager.getInstance();
14852
+ manager.handleEvent(event);
14853
+ } catch {
14854
+ }
16540
14855
  if (event.type === "session.deleted") {
16541
14856
  const props = event.properties;
16542
14857
  if (props?.info?.id) {
16543
- const sessionID = props.info.id;
16544
- sessions.delete(sessionID);
16545
- state.sessions.delete(sessionID);
16546
- const cleanedCount = backgroundTaskManager.cleanupBySession(sessionID);
16547
- if (cleanedCount > 0) {
16548
- console.log(`[background] Cleaned up ${cleanedCount} tasks for deleted session ${sessionID}`);
16549
- }
14858
+ sessions.delete(props.info.id);
14859
+ state.sessions.delete(props.info.id);
16550
14860
  }
16551
14861
  }
16552
14862
  }