micode 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL_CLAUDE.md +0 -1
- package/README.md +0 -11
- package/package.json +1 -1
- package/src/agents/brainstormer.ts +10 -17
- package/src/agents/commander.ts +3 -7
- package/src/agents/executor.ts +43 -67
- package/src/agents/project-initializer.ts +38 -72
- package/src/index.ts +0 -11
- package/src/tools/background-task/index.ts +0 -3
- package/src/tools/background-task/manager.ts +0 -433
- package/src/tools/background-task/tools.ts +0 -161
- package/src/tools/background-task/types.ts +0 -68
package/INSTALL_CLAUDE.md
CHANGED
|
@@ -159,7 +159,6 @@ Never run this automatically without consent.
|
|
|
159
159
|
| `ast_grep_search` | AST-aware code search |
|
|
160
160
|
| `ast_grep_replace` | AST-aware code replace |
|
|
161
161
|
| `look_at` | Screenshot analysis |
|
|
162
|
-
| `background_task` | Run tasks in background |
|
|
163
162
|
|
|
164
163
|
### Model Override
|
|
165
164
|
|
package/README.md
CHANGED
|
@@ -71,23 +71,12 @@ Maintain context across sessions with structured compaction. Run `/ledger` to cr
|
|
|
71
71
|
| `look_at` | Extract file structure |
|
|
72
72
|
| `artifact_search` | Search past plans/ledgers |
|
|
73
73
|
| `btca_ask` | Query library source code |
|
|
74
|
-
| `background_task` | Fire subagent in background |
|
|
75
|
-
| `background_list` | List tasks and status |
|
|
76
|
-
| `background_output` | Get task results |
|
|
77
|
-
| `background_cancel` | Cancel task(s) |
|
|
78
74
|
| `pty_spawn` | Start background terminal session |
|
|
79
75
|
| `pty_write` | Send input to PTY |
|
|
80
76
|
| `pty_read` | Read PTY output |
|
|
81
77
|
| `pty_list` | List PTY sessions |
|
|
82
78
|
| `pty_kill` | Terminate PTY |
|
|
83
79
|
|
|
84
|
-
### Background Tasks vs PTY
|
|
85
|
-
|
|
86
|
-
| System | Purpose | Use Case |
|
|
87
|
-
|--------|---------|----------|
|
|
88
|
-
| `background_task` | Async AI subagents | Research, implementation, reviews |
|
|
89
|
-
| `pty_spawn` | Async bash processes | Dev servers, watch modes, REPLs |
|
|
90
|
-
|
|
91
80
|
## Hooks
|
|
92
81
|
|
|
93
82
|
- **Think Mode** - Keywords like "think hard" enable 32k token thinking budget
|
package/package.json
CHANGED
|
@@ -14,15 +14,9 @@ This is DESIGN ONLY. The planner agent handles detailed implementation plans.
|
|
|
14
14
|
<rule priority="HIGHEST">ONE QUESTION AT A TIME: Ask exactly ONE question, then STOP and wait for the user's response. NEVER ask multiple questions in a single message. This is the most important rule.</rule>
|
|
15
15
|
<rule>NO CODE: Never write code. Never provide code examples. Design only.</rule>
|
|
16
16
|
<rule>TOOLS (grep, read, etc.): Do NOT use directly - use subagents instead.</rule>
|
|
17
|
-
<rule>
|
|
17
|
+
<rule>Use Task tool to spawn subagents synchronously. They complete before you continue.</rule>
|
|
18
18
|
</critical-rules>
|
|
19
19
|
|
|
20
|
-
<subagent-tools>
|
|
21
|
-
<tool name="background_task">Spawns subagent asynchronously. Returns task_id immediately.</tool>
|
|
22
|
-
<tool name="background_list">Check status of all background tasks.</tool>
|
|
23
|
-
<tool name="background_output">Get results from a completed task.</tool>
|
|
24
|
-
</subagent-tools>
|
|
25
|
-
|
|
26
20
|
<available-subagents>
|
|
27
21
|
<subagent name="codebase-locator">Find files, modules, patterns.</subagent>
|
|
28
22
|
<subagent name="codebase-analyzer">Deep analysis of specific modules.</subagent>
|
|
@@ -34,14 +28,13 @@ This is DESIGN ONLY. The planner agent handles detailed implementation plans.
|
|
|
34
28
|
<phase name="understanding" trigger="FIRST thing on any new topic">
|
|
35
29
|
<action>IMMEDIATELY spawn subagents to gather codebase context</action>
|
|
36
30
|
<example>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
Task(subagent_type="codebase-locator", prompt="Find files related to [topic]", description="Find [topic] files")
|
|
32
|
+
Task(subagent_type="codebase-analyzer", prompt="Analyze [related feature]", description="Analyze [feature]")
|
|
33
|
+
Task(subagent_type="pattern-finder", prompt="Find patterns for [functionality]", description="Find patterns")
|
|
40
34
|
</example>
|
|
41
35
|
<workflow>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
3. Use background_output(task_id="...") to collect results
|
|
36
|
+
Call multiple Task tools in ONE message for parallel execution.
|
|
37
|
+
Results are available immediately - no polling needed.
|
|
45
38
|
</workflow>
|
|
46
39
|
<rule>Do NOT proceed to questions until you have codebase context</rule>
|
|
47
40
|
<focus>purpose, constraints, success criteria</focus>
|
|
@@ -77,8 +70,8 @@ This is DESIGN ONLY. The planner agent handles detailed implementation plans.
|
|
|
77
70
|
<phase name="handoff" trigger="user approves design">
|
|
78
71
|
<action>When user says yes/approved/ready, IMMEDIATELY spawn the planner:</action>
|
|
79
72
|
<spawn>
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
Task(
|
|
74
|
+
subagent_type="planner",
|
|
82
75
|
prompt="Create a detailed implementation plan based on the design at thoughts/shared/designs/YYYY-MM-DD-{topic}-design.md",
|
|
83
76
|
description="Create implementation plan"
|
|
84
77
|
)
|
|
@@ -89,8 +82,8 @@ This is DESIGN ONLY. The planner agent handles detailed implementation plans.
|
|
|
89
82
|
|
|
90
83
|
<principles>
|
|
91
84
|
<principle name="design-only">NO CODE. Describe components, not implementations. Planner writes code.</principle>
|
|
92
|
-
<principle name="
|
|
93
|
-
<principle name="parallel-research">Multiple
|
|
85
|
+
<principle name="sync-subagents">Use Task tool for subagents. They complete before you continue.</principle>
|
|
86
|
+
<principle name="parallel-research">Multiple Task calls in one message run in parallel</principle>
|
|
94
87
|
<principle name="one-question">Ask exactly ONE question per message. STOP after asking. Wait for user's answer before continuing. NEVER bundle multiple questions together.</principle>
|
|
95
88
|
<principle name="yagni">Remove unnecessary features from ALL designs</principle>
|
|
96
89
|
<principle name="explore-alternatives">ALWAYS propose 2-3 approaches before settling</principle>
|
package/src/agents/commander.ts
CHANGED
|
@@ -85,14 +85,10 @@ Just do it - including obvious follow-up actions.
|
|
|
85
85
|
<agent name="executor" mode="subagent" purpose="Execute plan (runs implementer then reviewer automatically)"/>
|
|
86
86
|
<agent name="ledger-creator" mode="subagent" purpose="Create/update continuity ledgers"/>
|
|
87
87
|
<spawning>
|
|
88
|
-
<rule>
|
|
89
|
-
<tool name="background_task">Spawns subagent async. Returns task_id immediately.</tool>
|
|
90
|
-
<tool name="background_list">Check status of all background tasks.</tool>
|
|
91
|
-
<tool name="background_output">Get results from completed task.</tool>
|
|
88
|
+
<rule>Use Task tool to spawn subagents synchronously. They complete before you continue.</rule>
|
|
92
89
|
<example>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
background_output(task_id="bg_xxx") // get results
|
|
90
|
+
Task(subagent_type="planner", prompt="Create plan for...", description="Create plan")
|
|
91
|
+
// Result available immediately - no polling needed
|
|
96
92
|
</example>
|
|
97
93
|
</spawning>
|
|
98
94
|
<parallelization>
|
package/src/agents/executor.ts
CHANGED
|
@@ -11,15 +11,13 @@ Each task gets its own implementer → reviewer cycle.
|
|
|
11
11
|
Detect and parallelize independent tasks.
|
|
12
12
|
</purpose>
|
|
13
13
|
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
- background_list: List all background tasks and their status
|
|
19
|
-
</background-tools>
|
|
14
|
+
<subagent-tools>
|
|
15
|
+
Use Task tool to spawn subagents synchronously. They complete before you continue.
|
|
16
|
+
Call multiple Task tools in ONE message for parallel execution.
|
|
17
|
+
</subagent-tools>
|
|
20
18
|
|
|
21
19
|
<pty-tools description="For background bash processes">
|
|
22
|
-
PTY tools manage background terminal sessions
|
|
20
|
+
PTY tools manage background terminal sessions:
|
|
23
21
|
- pty_spawn: Start a background process (dev server, watch mode, REPL)
|
|
24
22
|
- pty_write: Send input to a PTY (commands, Ctrl+C, etc.)
|
|
25
23
|
- pty_read: Read output from a PTY buffer
|
|
@@ -33,15 +31,14 @@ Use PTY when:
|
|
|
33
31
|
|
|
34
32
|
Do NOT use PTY for:
|
|
35
33
|
- Quick commands (use bash)
|
|
36
|
-
- Subagent tasks (use background_task)
|
|
37
34
|
</pty-tools>
|
|
38
35
|
|
|
39
|
-
<workflow
|
|
36
|
+
<workflow>
|
|
40
37
|
<step>Parse plan to extract individual tasks</step>
|
|
41
38
|
<step>Analyze task dependencies to build execution graph</step>
|
|
42
39
|
<step>Group tasks into parallel batches (independent tasks run together)</step>
|
|
43
|
-
<step>Fire ALL implementers in batch
|
|
44
|
-
<step>
|
|
40
|
+
<step>Fire ALL implementers in batch using Task tool (parallel in one message)</step>
|
|
41
|
+
<step>When implementers complete, fire reviewers</step>
|
|
45
42
|
<step>Wait for batch to complete before starting dependent batch</step>
|
|
46
43
|
<step>Aggregate results and report</step>
|
|
47
44
|
</workflow>
|
|
@@ -61,20 +58,17 @@ Tasks are DEPENDENT (must be sequential) when:
|
|
|
61
58
|
When uncertain, assume DEPENDENT (safer).
|
|
62
59
|
</dependency-analysis>
|
|
63
60
|
|
|
64
|
-
<execution-pattern
|
|
65
|
-
|
|
66
|
-
1.
|
|
67
|
-
2.
|
|
68
|
-
3.
|
|
69
|
-
4.
|
|
61
|
+
<execution-pattern>
|
|
62
|
+
Maximize parallelism by calling multiple Task tools in one message:
|
|
63
|
+
1. Fire all implementers as Task calls in ONE message (parallel execution)
|
|
64
|
+
2. Results available immediately when all complete
|
|
65
|
+
3. Fire all reviewers as Task calls in ONE message
|
|
66
|
+
4. Handle any review feedback
|
|
70
67
|
|
|
71
68
|
Example: 3 independent tasks
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
- Task 2
|
|
75
|
-
- Task 1 finishes → immediately start reviewer 1
|
|
76
|
-
- Task 3 finishes → immediately start reviewer 3
|
|
77
|
-
- Reviewers run in parallel as they're spawned
|
|
69
|
+
- Call Task for implementer 1, 2, 3 in ONE message (all run in parallel)
|
|
70
|
+
- All results available when message completes
|
|
71
|
+
- Call Task for reviewer 1, 2, 3 in ONE message (all run in parallel)
|
|
78
72
|
</execution-pattern>
|
|
79
73
|
|
|
80
74
|
<available-subagents>
|
|
@@ -83,7 +77,7 @@ Example: 3 independent tasks
|
|
|
83
77
|
Input: Single task with context (which files, what to do).
|
|
84
78
|
Output: Changes made and verification results for that task.
|
|
85
79
|
<invocation>
|
|
86
|
-
|
|
80
|
+
Task(subagent_type="implementer", prompt="...", description="Implement task 1")
|
|
87
81
|
</invocation>
|
|
88
82
|
</subagent>
|
|
89
83
|
<subagent name="reviewer">
|
|
@@ -91,70 +85,54 @@ Example: 3 independent tasks
|
|
|
91
85
|
Input: Single task's changes against its requirements.
|
|
92
86
|
Output: APPROVED or CHANGES REQUESTED for that task.
|
|
93
87
|
<invocation>
|
|
94
|
-
|
|
88
|
+
Task(subagent_type="reviewer", prompt="...", description="Review task 1")
|
|
95
89
|
</invocation>
|
|
96
90
|
</subagent>
|
|
97
91
|
</available-subagents>
|
|
98
92
|
|
|
99
93
|
<per-task-cycle>
|
|
100
94
|
For each task:
|
|
101
|
-
1. Fire implementer
|
|
102
|
-
2.
|
|
103
|
-
3.
|
|
104
|
-
4.
|
|
105
|
-
5.
|
|
106
|
-
6. Report task status: DONE / BLOCKED
|
|
95
|
+
1. Fire implementer using Task tool
|
|
96
|
+
2. When complete, fire reviewer using Task tool
|
|
97
|
+
3. If reviewer requests changes: fire new implementer for fixes
|
|
98
|
+
4. Max 3 cycles per task before marking as blocked
|
|
99
|
+
5. Report task status: DONE / BLOCKED
|
|
107
100
|
</per-task-cycle>
|
|
108
101
|
|
|
109
|
-
<
|
|
102
|
+
<batch-execution>
|
|
110
103
|
Within a batch:
|
|
111
|
-
1. Fire ALL implementers as
|
|
112
|
-
2.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
- If implementer completed: start its reviewer as background_task
|
|
117
|
-
- If reviewer completed: check APPROVED or CHANGES REQUESTED
|
|
118
|
-
c. If changes needed and cycles < 3: fire new implementer
|
|
119
|
-
d. Sleep briefly, then repeat until all tasks done or blocked
|
|
120
|
-
3. Move to next batch
|
|
121
|
-
|
|
122
|
-
IMPORTANT: Always poll with background_list first to check status,
|
|
123
|
-
then fetch results with background_output only for completed tasks.
|
|
124
|
-
</fire-and-check-loop>
|
|
104
|
+
1. Fire ALL implementers as Task calls in ONE message (parallel)
|
|
105
|
+
2. When all complete, fire ALL reviewers as Task calls in ONE message (parallel)
|
|
106
|
+
3. If any reviewer requests changes and cycles < 3: fire new implementers
|
|
107
|
+
4. Move to next batch when current batch is done
|
|
108
|
+
</batch-execution>
|
|
125
109
|
|
|
126
110
|
<rules>
|
|
127
111
|
<rule>Parse ALL tasks from plan before starting execution</rule>
|
|
128
112
|
<rule>ALWAYS analyze dependencies before parallelizing</rule>
|
|
129
|
-
<rule>Fire parallel tasks as
|
|
130
|
-
<rule>Start reviewer immediately when its implementer finishes - don't wait for others</rule>
|
|
113
|
+
<rule>Fire parallel tasks as multiple Task calls in ONE message</rule>
|
|
131
114
|
<rule>Wait for entire batch before starting next batch</rule>
|
|
132
115
|
<rule>Each task gets its own implement → review cycle</rule>
|
|
133
116
|
<rule>Max 3 review cycles per task</rule>
|
|
134
117
|
<rule>Continue with other tasks if one is blocked</rule>
|
|
135
118
|
</rules>
|
|
136
119
|
|
|
137
|
-
<execution-example
|
|
120
|
+
<execution-example>
|
|
138
121
|
# Batch with tasks 1, 2, 3 (independent)
|
|
139
122
|
|
|
140
|
-
## Step 1: Fire all implementers
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
123
|
+
## Step 1: Fire all implementers in ONE message
|
|
124
|
+
Task(subagent_type="implementer", prompt="Execute task 1: [details]", description="Task 1")
|
|
125
|
+
Task(subagent_type="implementer", prompt="Execute task 2: [details]", description="Task 2")
|
|
126
|
+
Task(subagent_type="implementer", prompt="Execute task 3: [details]", description="Task 3")
|
|
127
|
+
// All three run in parallel, results available when message completes
|
|
144
128
|
|
|
145
|
-
## Step 2:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
129
|
+
## Step 2: Fire all reviewers in ONE message
|
|
130
|
+
Task(subagent_type="reviewer", prompt="Review task 1 implementation", description="Review 1")
|
|
131
|
+
Task(subagent_type="reviewer", prompt="Review task 2 implementation", description="Review 2")
|
|
132
|
+
Task(subagent_type="reviewer", prompt="Review task 3 implementation", description="Review 3")
|
|
133
|
+
// All three run in parallel, results available when message completes
|
|
149
134
|
|
|
150
|
-
|
|
151
|
-
background_output(task_id="task_id_1") → get result
|
|
152
|
-
background_output(task_id="task_id_3") → get result
|
|
153
|
-
background_task(description="Review 1", prompt="Review task 1 implementation", agent="reviewer") → review_id_1
|
|
154
|
-
background_task(description="Review 3", prompt="Review task 3 implementation", agent="reviewer") → review_id_3
|
|
155
|
-
|
|
156
|
-
## Step 3: Continue polling until all reviews complete
|
|
157
|
-
...
|
|
135
|
+
## Step 3: Handle any review feedback, then move to next batch
|
|
158
136
|
</execution-example>
|
|
159
137
|
|
|
160
138
|
<output-format>
|
|
@@ -191,12 +169,10 @@ background_task(description="Review 3", prompt="Review task 3 implementation", a
|
|
|
191
169
|
</output-format>
|
|
192
170
|
|
|
193
171
|
<never-do>
|
|
194
|
-
<forbidden>NEVER call background_output on running tasks - always poll with background_list first</forbidden>
|
|
195
172
|
<forbidden>Never skip dependency analysis</forbidden>
|
|
196
173
|
<forbidden>Never spawn dependent tasks in parallel</forbidden>
|
|
197
174
|
<forbidden>Never skip reviewer for any task</forbidden>
|
|
198
175
|
<forbidden>Never continue past 3 cycles for a single task</forbidden>
|
|
199
176
|
<forbidden>Never report success if any task is blocked</forbidden>
|
|
200
|
-
<forbidden>Never wait for all implementers before starting any reviewer</forbidden>
|
|
201
177
|
</never-do>`,
|
|
202
178
|
};
|
|
@@ -10,7 +10,7 @@ const PROMPT = `
|
|
|
10
10
|
|
|
11
11
|
<critical-rule>
|
|
12
12
|
MAXIMIZE PARALLELISM. Speed is critical.
|
|
13
|
-
-
|
|
13
|
+
- Call multiple Task tools in ONE message for parallel execution
|
|
14
14
|
- Run multiple tool calls in single message
|
|
15
15
|
- Never wait for one thing when you can do many
|
|
16
16
|
</critical-rule>
|
|
@@ -23,56 +23,38 @@ const PROMPT = `
|
|
|
23
23
|
</outputs>
|
|
24
24
|
</task>
|
|
25
25
|
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<tool name="background_output">
|
|
37
|
-
Get results from a completed task. Only call after background_list shows task is done.
|
|
38
|
-
Parameters: task_id
|
|
39
|
-
Example: background_output(task_id="abc123")
|
|
40
|
-
</tool>
|
|
41
|
-
</background-tools>
|
|
42
|
-
|
|
43
|
-
<parallel-execution-strategy pattern="fire-and-collect">
|
|
44
|
-
<phase name="1-fire" description="Fire ALL tasks simultaneously">
|
|
45
|
-
<description>Launch ALL discovery agents + run tools in a SINGLE message</description>
|
|
46
|
-
<fire-agents>
|
|
26
|
+
<subagent-tools>
|
|
27
|
+
Use Task tool to spawn subagents synchronously. They complete before you continue.
|
|
28
|
+
Call multiple Task tools in ONE message for parallel execution.
|
|
29
|
+
Example: Task(subagent_type="codebase-locator", prompt="Find all entry points", description="Find entry points")
|
|
30
|
+
</subagent-tools>
|
|
31
|
+
|
|
32
|
+
<parallel-execution-strategy>
|
|
33
|
+
<phase name="1-discovery" description="Launch ALL discovery in ONE message">
|
|
34
|
+
<description>Call multiple Task tools + other tools in a SINGLE message</description>
|
|
35
|
+
<subagents>
|
|
47
36
|
<agent name="codebase-locator">Find entry points, configs, main modules</agent>
|
|
48
37
|
<agent name="codebase-locator">Find test files and test patterns</agent>
|
|
49
38
|
<agent name="codebase-locator">Find linter, formatter, CI configs</agent>
|
|
50
39
|
<agent name="codebase-analyzer">Analyze directory structure</agent>
|
|
51
40
|
<agent name="pattern-finder">Find naming conventions across files</agent>
|
|
52
|
-
</
|
|
41
|
+
</subagents>
|
|
53
42
|
<parallel-tools>
|
|
54
43
|
<tool>Glob for package.json, pyproject.toml, go.mod, Cargo.toml, etc.</tool>
|
|
55
44
|
<tool>Glob for *.config.*, .eslintrc*, .prettierrc*, ruff.toml, etc.</tool>
|
|
56
45
|
<tool>Glob for README*, CONTRIBUTING*, docs/*</tool>
|
|
57
46
|
<tool>Read root directory listing</tool>
|
|
58
47
|
</parallel-tools>
|
|
48
|
+
<note>All Task calls and tools run in parallel, results available when message completes</note>
|
|
59
49
|
</phase>
|
|
60
50
|
|
|
61
|
-
<phase name="2-
|
|
62
|
-
<description>
|
|
63
|
-
<
|
|
64
|
-
<action>If still running: wait, poll again (max 5 times)</action>
|
|
65
|
-
<action>Call background_output for each completed task (skip errored)</action>
|
|
66
|
-
<action>Process tool results from phase 1</action>
|
|
67
|
-
</phase>
|
|
68
|
-
|
|
69
|
-
<phase name="3-deep-analysis" description="Fire deep analysis tasks">
|
|
70
|
-
<description>Based on discovery, fire more background tasks</description>
|
|
71
|
-
<fire-agents>
|
|
51
|
+
<phase name="2-deep-analysis" description="Fire deep analysis tasks">
|
|
52
|
+
<description>Based on discovery, call more Task tools in ONE message</description>
|
|
53
|
+
<subagents>
|
|
72
54
|
<agent name="codebase-analyzer">Analyze core/domain logic</agent>
|
|
73
55
|
<agent name="codebase-analyzer">Analyze API/entry points</agent>
|
|
74
56
|
<agent name="codebase-analyzer">Analyze data layer</agent>
|
|
75
|
-
</
|
|
57
|
+
</subagents>
|
|
76
58
|
<parallel-tools>
|
|
77
59
|
<tool>Read 5 core source files simultaneously</tool>
|
|
78
60
|
<tool>Read 3 test files simultaneously</tool>
|
|
@@ -80,9 +62,7 @@ const PROMPT = `
|
|
|
80
62
|
</parallel-tools>
|
|
81
63
|
</phase>
|
|
82
64
|
|
|
83
|
-
<phase name="
|
|
84
|
-
<description>Collect deep analysis results, then write both files</description>
|
|
85
|
-
<action>Collect all deep analysis results</action>
|
|
65
|
+
<phase name="3-write" description="Write output files">
|
|
86
66
|
<action>Write ARCHITECTURE.md</action>
|
|
87
67
|
<action>Write CODE_STYLE.md</action>
|
|
88
68
|
</phase>
|
|
@@ -92,26 +72,24 @@ const PROMPT = `
|
|
|
92
72
|
<subagent name="codebase-locator">
|
|
93
73
|
Fast file/pattern finder. Spawn multiple with different queries.
|
|
94
74
|
Examples: "Find all entry points", "Find all config files", "Find test directories"
|
|
95
|
-
|
|
75
|
+
Task(subagent_type="codebase-locator", prompt="Find all entry points and main files", description="Find entry points")
|
|
96
76
|
</subagent>
|
|
97
77
|
<subagent name="codebase-analyzer">
|
|
98
78
|
Deep module analyzer. Spawn multiple for different areas.
|
|
99
79
|
Examples: "Analyze src/core", "Analyze api layer", "Analyze database module"
|
|
100
|
-
|
|
80
|
+
Task(subagent_type="codebase-analyzer", prompt="Analyze the core module", description="Analyze core")
|
|
101
81
|
</subagent>
|
|
102
82
|
<subagent name="pattern-finder">
|
|
103
83
|
Pattern extractor. Spawn for different pattern types.
|
|
104
84
|
Examples: "Find naming patterns", "Find error handling patterns", "Find async patterns"
|
|
105
|
-
|
|
85
|
+
Task(subagent_type="pattern-finder", prompt="Find naming conventions", description="Find patterns")
|
|
106
86
|
</subagent>
|
|
107
|
-
<rule>
|
|
87
|
+
<rule>Use Task tool to spawn subagents synchronously.</rule>
|
|
108
88
|
</available-subagents>
|
|
109
89
|
|
|
110
90
|
<critical-instruction>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
Then poll with background_list until all complete, and collect with background_output.
|
|
114
|
-
This is the fire-and-collect pattern - fire everything, poll, then collect everything.
|
|
91
|
+
Call multiple Task tools in ONE message for TRUE parallelism.
|
|
92
|
+
All results available immediately when message completes - no polling needed.
|
|
115
93
|
</critical-instruction>
|
|
116
94
|
|
|
117
95
|
<language-detection>
|
|
@@ -177,10 +155,9 @@ const PROMPT = `
|
|
|
177
155
|
|
|
178
156
|
<rules>
|
|
179
157
|
<category name="Speed">
|
|
180
|
-
<rule>ALWAYS
|
|
158
|
+
<rule>ALWAYS call multiple Task tools in a SINGLE message for parallelism</rule>
|
|
181
159
|
<rule>ALWAYS run multiple tool calls in a SINGLE message</rule>
|
|
182
160
|
<rule>NEVER wait for one task when you can start others</rule>
|
|
183
|
-
<rule>Use fire-and-collect: fire all, then collect all</rule>
|
|
184
161
|
</category>
|
|
185
162
|
|
|
186
163
|
<category name="Analysis">
|
|
@@ -205,38 +182,27 @@ const PROMPT = `
|
|
|
205
182
|
</category>
|
|
206
183
|
</rules>
|
|
207
184
|
|
|
208
|
-
<execution-example
|
|
209
|
-
<step description="
|
|
210
|
-
In a SINGLE message,
|
|
211
|
-
-
|
|
212
|
-
-
|
|
213
|
-
-
|
|
214
|
-
-
|
|
215
|
-
-
|
|
185
|
+
<execution-example>
|
|
186
|
+
<step description="Discovery: Launch all tasks in ONE message">
|
|
187
|
+
In a SINGLE message, call ALL Task tools AND run other tools:
|
|
188
|
+
- Task(subagent_type="codebase-locator", prompt="Find all entry points and main files", description="Find entry points")
|
|
189
|
+
- Task(subagent_type="codebase-locator", prompt="Find all config files (linters, formatters, build)", description="Find configs")
|
|
190
|
+
- Task(subagent_type="codebase-locator", prompt="Find test directories and test files", description="Find tests")
|
|
191
|
+
- Task(subagent_type="codebase-analyzer", prompt="Analyze the directory structure and organization", description="Analyze structure")
|
|
192
|
+
- Task(subagent_type="pattern-finder", prompt="Find naming conventions used across the codebase", description="Find patterns")
|
|
216
193
|
- Glob: package.json, pyproject.toml, go.mod, Cargo.toml, etc.
|
|
217
194
|
- Glob: README*, ARCHITECTURE*, docs/*
|
|
195
|
+
// All results available when message completes - no polling needed
|
|
218
196
|
</step>
|
|
219
197
|
|
|
220
|
-
<step description="
|
|
221
|
-
|
|
222
|
-
-
|
|
223
|
-
Then collect results (skip errored tasks):
|
|
224
|
-
- background_output(task_id=task_id_1)
|
|
225
|
-
- background_output(task_id=task_id_2)
|
|
226
|
-
- background_output(task_id=task_id_3)
|
|
227
|
-
- background_output(task_id=task_id_4)
|
|
228
|
-
- background_output(task_id=task_id_5)
|
|
229
|
-
</step>
|
|
230
|
-
|
|
231
|
-
<step description="FIRE: Deep analysis based on discovery">
|
|
232
|
-
Based on discovery, in a SINGLE message fire more tasks:
|
|
233
|
-
- background_task for each major module: agent="codebase-analyzer"
|
|
198
|
+
<step description="Deep analysis: Fire more tasks in ONE message">
|
|
199
|
+
Based on discovery, in a SINGLE message call more Task tools:
|
|
200
|
+
- Task for each major module: subagent_type="codebase-analyzer"
|
|
234
201
|
- Read multiple source files simultaneously
|
|
235
202
|
- Read multiple test files simultaneously
|
|
236
203
|
</step>
|
|
237
204
|
|
|
238
|
-
<step description="
|
|
239
|
-
Collect deep analysis results, then write:
|
|
205
|
+
<step description="Write output files">
|
|
240
206
|
- Write ARCHITECTURE.md
|
|
241
207
|
- Write CODE_STYLE.md
|
|
242
208
|
</step>
|
package/src/index.ts
CHANGED
|
@@ -22,9 +22,6 @@ import { createLedgerLoaderHook } from "./hooks/ledger-loader";
|
|
|
22
22
|
import { createArtifactAutoIndexHook } from "./hooks/artifact-auto-index";
|
|
23
23
|
import { createFileOpsTrackerHook } from "./hooks/file-ops-tracker";
|
|
24
24
|
|
|
25
|
-
// Background Task System
|
|
26
|
-
import { BackgroundTaskManager, createBackgroundTaskTools } from "./tools/background-task";
|
|
27
|
-
|
|
28
25
|
// PTY System
|
|
29
26
|
import { PTYManager, createPtyTools } from "./tools/pty";
|
|
30
27
|
|
|
@@ -96,10 +93,6 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
|
|
|
96
93
|
const artifactAutoIndexHook = createArtifactAutoIndexHook(ctx);
|
|
97
94
|
const fileOpsTrackerHook = createFileOpsTrackerHook(ctx);
|
|
98
95
|
|
|
99
|
-
// Background Task System
|
|
100
|
-
const backgroundTaskManager = new BackgroundTaskManager(ctx);
|
|
101
|
-
const backgroundTaskTools = createBackgroundTaskTools(backgroundTaskManager);
|
|
102
|
-
|
|
103
96
|
// PTY System
|
|
104
97
|
const ptyManager = new PTYManager();
|
|
105
98
|
const ptyTools = createPtyTools(ptyManager);
|
|
@@ -112,7 +105,6 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
|
|
|
112
105
|
btca_ask,
|
|
113
106
|
look_at,
|
|
114
107
|
artifact_search,
|
|
115
|
-
...backgroundTaskTools,
|
|
116
108
|
...ptyTools,
|
|
117
109
|
},
|
|
118
110
|
|
|
@@ -243,9 +235,6 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
|
|
|
243
235
|
await tokenAwareTruncationHook.event({ event });
|
|
244
236
|
await contextWindowMonitorHook.event({ event });
|
|
245
237
|
|
|
246
|
-
// Background task manager event handling
|
|
247
|
-
backgroundTaskManager.handleEvent(event);
|
|
248
|
-
|
|
249
238
|
// File ops tracker cleanup
|
|
250
239
|
await fileOpsTrackerHook.event({ event });
|
|
251
240
|
},
|
|
@@ -1,433 +0,0 @@
|
|
|
1
|
-
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
-
import type {
|
|
3
|
-
BackgroundTask,
|
|
4
|
-
BackgroundTaskInput,
|
|
5
|
-
SessionCreateResponse,
|
|
6
|
-
SessionStatusResponse,
|
|
7
|
-
SessionMessagesResponse,
|
|
8
|
-
} from "./types";
|
|
9
|
-
|
|
10
|
-
const POLL_INTERVAL_MS = 2000;
|
|
11
|
-
const TASK_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
12
|
-
|
|
13
|
-
function generateTaskId(): string {
|
|
14
|
-
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
15
|
-
let result = "bg_";
|
|
16
|
-
for (let i = 0; i < 8; i++) {
|
|
17
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
18
|
-
}
|
|
19
|
-
return result;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function formatDuration(start: Date, end?: Date): string {
|
|
23
|
-
const ms = (end || new Date()).getTime() - start.getTime();
|
|
24
|
-
const seconds = Math.floor(ms / 1000);
|
|
25
|
-
const minutes = Math.floor(seconds / 60);
|
|
26
|
-
|
|
27
|
-
if (minutes > 0) {
|
|
28
|
-
return `${minutes}m ${seconds % 60}s`;
|
|
29
|
-
}
|
|
30
|
-
return `${seconds}s`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export class BackgroundTaskManager {
|
|
34
|
-
private tasks: Map<string, BackgroundTask> = new Map();
|
|
35
|
-
private notifications: Map<string, BackgroundTask[]> = new Map();
|
|
36
|
-
private pollingInterval?: ReturnType<typeof setInterval>;
|
|
37
|
-
private ctx: PluginInput;
|
|
38
|
-
|
|
39
|
-
constructor(ctx: PluginInput) {
|
|
40
|
-
this.ctx = ctx;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async launch(input: BackgroundTaskInput): Promise<BackgroundTask> {
|
|
44
|
-
const taskId = generateTaskId();
|
|
45
|
-
|
|
46
|
-
// Create new session for background task
|
|
47
|
-
const sessionResp = await this.ctx.client.session.create({
|
|
48
|
-
body: {},
|
|
49
|
-
query: { directory: this.ctx.directory },
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const sessionData = sessionResp as SessionCreateResponse;
|
|
53
|
-
const sessionID = sessionData.data?.id;
|
|
54
|
-
|
|
55
|
-
if (!sessionID) {
|
|
56
|
-
throw new Error("Failed to create background session");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const task: BackgroundTask = {
|
|
60
|
-
id: taskId,
|
|
61
|
-
sessionID,
|
|
62
|
-
parentSessionID: input.parentSessionID,
|
|
63
|
-
parentMessageID: input.parentMessageID,
|
|
64
|
-
description: input.description,
|
|
65
|
-
prompt: input.prompt,
|
|
66
|
-
agent: input.agent,
|
|
67
|
-
status: "running",
|
|
68
|
-
startedAt: new Date(),
|
|
69
|
-
progress: {
|
|
70
|
-
toolCalls: 0,
|
|
71
|
-
lastUpdate: new Date(),
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
this.tasks.set(taskId, task);
|
|
76
|
-
|
|
77
|
-
// Fire-and-forget prompt
|
|
78
|
-
this.ctx.client.session
|
|
79
|
-
.prompt({
|
|
80
|
-
path: { id: sessionID },
|
|
81
|
-
body: {
|
|
82
|
-
parts: [{ type: "text", text: input.prompt }],
|
|
83
|
-
agent: input.agent,
|
|
84
|
-
},
|
|
85
|
-
query: { directory: this.ctx.directory },
|
|
86
|
-
})
|
|
87
|
-
.catch((error) => {
|
|
88
|
-
console.error(`[background-task] Failed to prompt session ${sessionID}:`, error);
|
|
89
|
-
task.status = "error";
|
|
90
|
-
task.error = error instanceof Error ? error.message : String(error);
|
|
91
|
-
task.completedAt = new Date();
|
|
92
|
-
this.markForNotification(task);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// Start polling if not already
|
|
96
|
-
this.startPolling();
|
|
97
|
-
|
|
98
|
-
return task;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async cancel(taskId: string): Promise<boolean> {
|
|
102
|
-
const task = this.tasks.get(taskId);
|
|
103
|
-
if (!task || task.status !== "running") {
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
// Fire-and-forget abort
|
|
109
|
-
this.ctx.client.session
|
|
110
|
-
.abort({
|
|
111
|
-
path: { id: task.sessionID },
|
|
112
|
-
query: { directory: this.ctx.directory },
|
|
113
|
-
})
|
|
114
|
-
.catch((error) => {
|
|
115
|
-
console.error(`[background-task] Failed to abort session ${task.sessionID}:`, error);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
task.status = "cancelled";
|
|
119
|
-
task.completedAt = new Date();
|
|
120
|
-
this.markForNotification(task);
|
|
121
|
-
return true;
|
|
122
|
-
} catch {
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async cancelAll(): Promise<number> {
|
|
128
|
-
let cancelled = 0;
|
|
129
|
-
for (const task of this.tasks.values()) {
|
|
130
|
-
if (task.status === "running") {
|
|
131
|
-
if (await this.cancel(task.id)) {
|
|
132
|
-
cancelled++;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return cancelled;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
getTask(taskId: string): BackgroundTask | undefined {
|
|
140
|
-
return this.tasks.get(taskId);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
findBySession(sessionID: string): BackgroundTask | undefined {
|
|
144
|
-
for (const task of this.tasks.values()) {
|
|
145
|
-
if (task.sessionID === sessionID) {
|
|
146
|
-
return task;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
getAllTasks(): BackgroundTask[] {
|
|
153
|
-
return Array.from(this.tasks.values());
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Poll all running tasks and update their status.
|
|
158
|
-
* Called by background_list to ensure fresh status.
|
|
159
|
-
*/
|
|
160
|
-
async refreshTaskStatus(): Promise<void> {
|
|
161
|
-
const runningTasks = this.getRunningTasks();
|
|
162
|
-
if (runningTasks.length === 0) return;
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
// Get all session statuses in one call
|
|
166
|
-
const resp = await this.ctx.client.session.status({
|
|
167
|
-
query: { directory: this.ctx.directory },
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const statusResp = resp as SessionStatusResponse;
|
|
171
|
-
const statusMap = statusResp.data || {};
|
|
172
|
-
|
|
173
|
-
for (const task of runningTasks) {
|
|
174
|
-
const sessionStatus = statusMap[task.sessionID];
|
|
175
|
-
const statusType = sessionStatus?.type;
|
|
176
|
-
|
|
177
|
-
// Store last known session status for debugging
|
|
178
|
-
(task as BackgroundTask & { _sessionStatus?: string })._sessionStatus = statusType;
|
|
179
|
-
|
|
180
|
-
if (statusType === "idle" || statusType === undefined) {
|
|
181
|
-
// Session is idle OR not in status map (likely finished and cleaned up)
|
|
182
|
-
// Try to get result - if successful, mark as completed
|
|
183
|
-
const result = await this.fetchTaskResult(task);
|
|
184
|
-
if (result !== undefined || statusType === "idle") {
|
|
185
|
-
task.status = "completed";
|
|
186
|
-
task.completedAt = new Date();
|
|
187
|
-
task.result = result;
|
|
188
|
-
}
|
|
189
|
-
// If result is undefined and statusType is undefined, keep waiting
|
|
190
|
-
// (might be a timing issue with status propagation)
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
} catch (error) {
|
|
194
|
-
console.error("[background-task] Failed to refresh task status:", error);
|
|
195
|
-
// Don't mark all tasks as error - they may still be running
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
getRunningTasks(): BackgroundTask[] {
|
|
200
|
-
return this.getAllTasks().filter((t) => t.status === "running");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async getTaskResult(taskId: string): Promise<string | undefined> {
|
|
204
|
-
const task = this.tasks.get(taskId);
|
|
205
|
-
if (!task || task.status === "running") {
|
|
206
|
-
return undefined;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (task.result) {
|
|
210
|
-
return task.result;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const result = await this.fetchTaskResult(task);
|
|
214
|
-
if (result !== undefined) {
|
|
215
|
-
task.result = result;
|
|
216
|
-
}
|
|
217
|
-
return result;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Fetch result from session messages without checking task status.
|
|
222
|
-
* Used during polling to check if a session has completed.
|
|
223
|
-
*/
|
|
224
|
-
private async fetchTaskResult(task: BackgroundTask): Promise<string | undefined> {
|
|
225
|
-
try {
|
|
226
|
-
const resp = await this.ctx.client.session.messages({
|
|
227
|
-
path: { id: task.sessionID },
|
|
228
|
-
query: { directory: this.ctx.directory },
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
const messagesResp = resp as SessionMessagesResponse;
|
|
232
|
-
const messages = messagesResp.data || [];
|
|
233
|
-
const lastAssistant = [...messages].reverse().find((m) => m.info?.role === "assistant");
|
|
234
|
-
|
|
235
|
-
if (lastAssistant) {
|
|
236
|
-
const textParts = lastAssistant.parts?.filter((p) => p.type === "text") || [];
|
|
237
|
-
return textParts.map((p) => p.text || "").join("\n");
|
|
238
|
-
}
|
|
239
|
-
} catch (error) {
|
|
240
|
-
console.error(`[background-task] Failed to fetch result for task ${task.id}:`, error);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return undefined;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
formatTaskStatus(task: BackgroundTask): string {
|
|
247
|
-
const duration = formatDuration(task.startedAt, task.completedAt);
|
|
248
|
-
const status = task.status.toUpperCase();
|
|
249
|
-
|
|
250
|
-
let output = `## Task: ${task.description}\n\n`;
|
|
251
|
-
output += `| Field | Value |\n|-------|-------|\n`;
|
|
252
|
-
output += `| ID | ${task.id} |\n`;
|
|
253
|
-
output += `| Status | ${status} |\n`;
|
|
254
|
-
output += `| Agent | ${task.agent} |\n`;
|
|
255
|
-
output += `| Duration | ${duration} |\n`;
|
|
256
|
-
|
|
257
|
-
if (task.progress) {
|
|
258
|
-
output += `| Tool Calls | ${task.progress.toolCalls} |\n`;
|
|
259
|
-
if (task.progress.lastTool) {
|
|
260
|
-
output += `| Last Tool | ${task.progress.lastTool} |\n`;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (task.error) {
|
|
265
|
-
output += `\n### Error\n${task.error}\n`;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return output;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
private startPolling(): void {
|
|
272
|
-
if (this.pollingInterval) return;
|
|
273
|
-
|
|
274
|
-
this.pollingInterval = setInterval(() => {
|
|
275
|
-
this.pollRunningTasks();
|
|
276
|
-
}, POLL_INTERVAL_MS);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
private stopPolling(): void {
|
|
280
|
-
if (this.pollingInterval) {
|
|
281
|
-
clearInterval(this.pollingInterval);
|
|
282
|
-
this.pollingInterval = undefined;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
private cleanupOldTasks(): void {
|
|
287
|
-
const now = Date.now();
|
|
288
|
-
for (const [taskId, task] of this.tasks) {
|
|
289
|
-
// Only cleanup completed/cancelled/error tasks
|
|
290
|
-
if (task.status === "running") continue;
|
|
291
|
-
|
|
292
|
-
const completedAt = task.completedAt?.getTime() || 0;
|
|
293
|
-
if (now - completedAt > TASK_TTL_MS) {
|
|
294
|
-
this.tasks.delete(taskId);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
private async pollRunningTasks(): Promise<void> {
|
|
300
|
-
// Cleanup old completed tasks to prevent memory leak
|
|
301
|
-
this.cleanupOldTasks();
|
|
302
|
-
|
|
303
|
-
const runningTasks = this.getRunningTasks();
|
|
304
|
-
|
|
305
|
-
if (runningTasks.length === 0) {
|
|
306
|
-
this.stopPolling();
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
try {
|
|
311
|
-
// Get all session statuses in one call
|
|
312
|
-
const resp = await this.ctx.client.session.status({
|
|
313
|
-
query: { directory: this.ctx.directory },
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
const statusResp = resp as SessionStatusResponse;
|
|
317
|
-
const statusMap = statusResp.data || {};
|
|
318
|
-
|
|
319
|
-
for (const task of runningTasks) {
|
|
320
|
-
const sessionStatus = statusMap[task.sessionID];
|
|
321
|
-
const statusType = sessionStatus?.type;
|
|
322
|
-
|
|
323
|
-
if (statusType === "idle" || statusType === undefined) {
|
|
324
|
-
// Session is idle OR not in status map (likely finished and cleaned up)
|
|
325
|
-
// Try to get result - if successful, mark as completed
|
|
326
|
-
const result = await this.fetchTaskResult(task);
|
|
327
|
-
if (result !== undefined || statusType === "idle") {
|
|
328
|
-
task.status = "completed";
|
|
329
|
-
task.completedAt = new Date();
|
|
330
|
-
task.result = result;
|
|
331
|
-
this.markForNotification(task);
|
|
332
|
-
|
|
333
|
-
await this.ctx.client.tui
|
|
334
|
-
.showToast({
|
|
335
|
-
body: {
|
|
336
|
-
title: "Background Task Complete",
|
|
337
|
-
message: task.description,
|
|
338
|
-
variant: "success",
|
|
339
|
-
duration: 5000,
|
|
340
|
-
},
|
|
341
|
-
})
|
|
342
|
-
.catch((error) => {
|
|
343
|
-
console.error(`[background-task] Failed to show toast for task ${task.id}:`, error);
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
// If result is undefined and statusType is undefined, keep waiting
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
} catch (error) {
|
|
350
|
-
console.error("[background-task] Failed to poll tasks:", error);
|
|
351
|
-
// Don't mark tasks as error - they may still be running, just can't check
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
private markForNotification(task: BackgroundTask): void {
|
|
356
|
-
const existing = this.notifications.get(task.parentSessionID) || [];
|
|
357
|
-
existing.push(task);
|
|
358
|
-
this.notifications.set(task.parentSessionID, existing);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
getPendingNotifications(parentSessionID: string): BackgroundTask[] {
|
|
362
|
-
const notifications = this.notifications.get(parentSessionID) || [];
|
|
363
|
-
this.notifications.delete(parentSessionID);
|
|
364
|
-
return notifications;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
handleEvent(event: { type: string; properties?: unknown }): void {
|
|
368
|
-
const props = event.properties as Record<string, unknown> | undefined;
|
|
369
|
-
|
|
370
|
-
// Primary completion detection: session.idle event
|
|
371
|
-
if (event.type === "session.idle") {
|
|
372
|
-
const sessionID = props?.sessionID as string | undefined;
|
|
373
|
-
if (!sessionID) return;
|
|
374
|
-
|
|
375
|
-
const task = this.findBySession(sessionID);
|
|
376
|
-
if (!task || task.status !== "running") return;
|
|
377
|
-
|
|
378
|
-
task.status = "completed";
|
|
379
|
-
task.completedAt = new Date();
|
|
380
|
-
this.fetchTaskResult(task).then((result) => {
|
|
381
|
-
task.result = result;
|
|
382
|
-
});
|
|
383
|
-
this.markForNotification(task);
|
|
384
|
-
|
|
385
|
-
this.ctx.client.tui
|
|
386
|
-
.showToast({
|
|
387
|
-
body: {
|
|
388
|
-
title: "Background Task Complete",
|
|
389
|
-
message: task.description,
|
|
390
|
-
variant: "success",
|
|
391
|
-
duration: 5000,
|
|
392
|
-
},
|
|
393
|
-
})
|
|
394
|
-
.catch((error) => {
|
|
395
|
-
console.error(`[background-task] Failed to show toast for task ${task.id}:`, error);
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Track tool usage for progress
|
|
400
|
-
if (event.type === "message.part.updated") {
|
|
401
|
-
const info = props?.info as Record<string, unknown> | undefined;
|
|
402
|
-
const sessionID = info?.sessionID as string | undefined;
|
|
403
|
-
const partType = info?.type as string | undefined;
|
|
404
|
-
|
|
405
|
-
if (sessionID && partType === "tool_use") {
|
|
406
|
-
for (const task of this.tasks.values()) {
|
|
407
|
-
if (task.sessionID === sessionID && task.status === "running") {
|
|
408
|
-
if (!task.progress) {
|
|
409
|
-
task.progress = { toolCalls: 0, lastUpdate: new Date() };
|
|
410
|
-
}
|
|
411
|
-
task.progress.toolCalls++;
|
|
412
|
-
task.progress.lastTool = (info?.name as string) || undefined;
|
|
413
|
-
task.progress.lastUpdate = new Date();
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Cleanup on session delete
|
|
421
|
-
if (event.type === "session.deleted") {
|
|
422
|
-
const sessionInfo = props?.info as { id?: string } | undefined;
|
|
423
|
-
if (sessionInfo?.id) {
|
|
424
|
-
for (const task of this.tasks.values()) {
|
|
425
|
-
if (task.sessionID === sessionInfo.id && task.status === "running") {
|
|
426
|
-
task.status = "cancelled";
|
|
427
|
-
task.completedAt = new Date();
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
|
-
import type { BackgroundTaskManager } from "./manager";
|
|
3
|
-
|
|
4
|
-
// Extended tool context with metadata for UI navigation
|
|
5
|
-
type ToolContextWithMetadata = {
|
|
6
|
-
sessionID: string;
|
|
7
|
-
messageID?: string;
|
|
8
|
-
agent: string;
|
|
9
|
-
abort: AbortSignal;
|
|
10
|
-
metadata?: (input: { title?: string; metadata?: Record<string, unknown> }) => void;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function createBackgroundTaskTools(manager: BackgroundTaskManager) {
|
|
14
|
-
const background_task = tool({
|
|
15
|
-
description: `Launch a task to run in the background using a subagent.
|
|
16
|
-
The task runs independently while you continue working.
|
|
17
|
-
Use background_output to check progress or get results when complete.
|
|
18
|
-
Useful for: parallel research, concurrent implementation, async reviews.`,
|
|
19
|
-
args: {
|
|
20
|
-
description: tool.schema.string().describe("Short description of the task (shown in status)"),
|
|
21
|
-
prompt: tool.schema.string().describe("Full prompt/instructions for the background agent"),
|
|
22
|
-
agent: tool.schema.string().describe("Agent to use (e.g., 'codebase-analyzer', 'implementer')"),
|
|
23
|
-
},
|
|
24
|
-
execute: async (args, toolContext) => {
|
|
25
|
-
const ctx = toolContext as ToolContextWithMetadata;
|
|
26
|
-
try {
|
|
27
|
-
const task = await manager.launch({
|
|
28
|
-
description: args.description,
|
|
29
|
-
prompt: args.prompt,
|
|
30
|
-
agent: args.agent,
|
|
31
|
-
parentSessionID: ctx.sessionID,
|
|
32
|
-
parentMessageID: ctx.messageID || "",
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// Set metadata for OpenCode UI session navigation (ctrl+x + arrow keys)
|
|
36
|
-
ctx.metadata?.({
|
|
37
|
-
title: args.description,
|
|
38
|
-
metadata: { sessionId: task.sessionID },
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
return `## Background Task Launched
|
|
42
|
-
|
|
43
|
-
| Field | Value |
|
|
44
|
-
|-------|-------|
|
|
45
|
-
| Task ID | ${task.id} |
|
|
46
|
-
| Agent | ${args.agent} |
|
|
47
|
-
| Status | RUNNING |
|
|
48
|
-
|
|
49
|
-
Use \`background_output\` with task_id="${task.id}" to check progress or get results.`;
|
|
50
|
-
} catch (error) {
|
|
51
|
-
return `Failed to launch background task: ${error instanceof Error ? error.message : String(error)}`;
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const background_output = tool({
|
|
57
|
-
description: `Get status or results from a background task.
|
|
58
|
-
Returns immediately with current status. Use background_list to poll for completion.`,
|
|
59
|
-
args: {
|
|
60
|
-
task_id: tool.schema.string().describe("ID of the task to check (e.g., 'bg_abc12345')"),
|
|
61
|
-
},
|
|
62
|
-
execute: async (args) => {
|
|
63
|
-
const { task_id } = args;
|
|
64
|
-
|
|
65
|
-
const task = manager.getTask(task_id);
|
|
66
|
-
if (!task) {
|
|
67
|
-
return `Task not found: ${task_id}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Format status
|
|
71
|
-
let output = manager.formatTaskStatus(task);
|
|
72
|
-
|
|
73
|
-
// Include result if completed
|
|
74
|
-
if (task.status === "completed") {
|
|
75
|
-
const result = await manager.getTaskResult(task_id);
|
|
76
|
-
if (result) {
|
|
77
|
-
output += `\n### Result\n${result}\n`;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return output;
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const background_cancel = tool({
|
|
86
|
-
description: `Cancel a running background task or all tasks.`,
|
|
87
|
-
args: {
|
|
88
|
-
task_id: tool.schema.string().optional().describe("ID of the task to cancel (omit to cancel all)"),
|
|
89
|
-
all: tool.schema.boolean().optional().describe("Cancel all running tasks (default: false)"),
|
|
90
|
-
},
|
|
91
|
-
execute: async (args) => {
|
|
92
|
-
const { task_id, all = false } = args;
|
|
93
|
-
|
|
94
|
-
if (all) {
|
|
95
|
-
const cancelled = await manager.cancelAll();
|
|
96
|
-
return `Cancelled ${cancelled} running task(s).`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!task_id) {
|
|
100
|
-
return "Provide task_id or set all=true to cancel tasks.";
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const success = await manager.cancel(task_id);
|
|
104
|
-
if (success) {
|
|
105
|
-
return `Task ${task_id} cancelled.`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return `Could not cancel task ${task_id}. It may already be completed or not exist.`;
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const background_list = tool({
|
|
113
|
-
description: `List all background tasks and their status.`,
|
|
114
|
-
args: {},
|
|
115
|
-
execute: async () => {
|
|
116
|
-
// Refresh status of running tasks before returning
|
|
117
|
-
await manager.refreshTaskStatus();
|
|
118
|
-
const tasks = manager.getAllTasks();
|
|
119
|
-
|
|
120
|
-
if (tasks.length === 0) {
|
|
121
|
-
return "No background tasks.";
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
125
|
-
const errored = tasks.filter((t) => t.status === "error").length;
|
|
126
|
-
const running = tasks.filter((t) => t.status === "running").length;
|
|
127
|
-
const total = tasks.length;
|
|
128
|
-
const allDone = running === 0;
|
|
129
|
-
|
|
130
|
-
let output = "## Background Tasks\n\n";
|
|
131
|
-
output += `**Status: ${completed + errored}/${total} done${allDone ? " ✓ ALL COMPLETE" : ` (${running} still running)`}**\n\n`;
|
|
132
|
-
output += "| ID | Description | Agent | Status | Duration | Session |\n";
|
|
133
|
-
output += "|----|-------------|-------|--------|----------|---------|";
|
|
134
|
-
|
|
135
|
-
for (const task of tasks) {
|
|
136
|
-
const duration = task.completedAt
|
|
137
|
-
? `${Math.round((task.completedAt.getTime() - task.startedAt.getTime()) / 1000)}s`
|
|
138
|
-
: `${Math.round((Date.now() - task.startedAt.getTime()) / 1000)}s`;
|
|
139
|
-
|
|
140
|
-
// Show session status for debugging
|
|
141
|
-
const sessionStatus = (task as { _sessionStatus?: string })._sessionStatus || "?";
|
|
142
|
-
const statusDisplay = task.status === "running" ? `${task.status} (${sessionStatus})` : task.status;
|
|
143
|
-
|
|
144
|
-
output += `| ${task.id} | ${task.description} | ${task.agent} | ${statusDisplay} | ${duration} | ${task.sessionID} |\n`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (allDone) {
|
|
148
|
-
output += "\n**→ All tasks complete. Proceed to collect results with background_output.**";
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return output;
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
background_task,
|
|
157
|
-
background_output,
|
|
158
|
-
background_cancel,
|
|
159
|
-
background_list,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
export interface BackgroundTask {
|
|
2
|
-
id: string;
|
|
3
|
-
sessionID: string;
|
|
4
|
-
parentSessionID: string;
|
|
5
|
-
parentMessageID: string;
|
|
6
|
-
description: string;
|
|
7
|
-
prompt: string;
|
|
8
|
-
agent: string;
|
|
9
|
-
status: "running" | "completed" | "error" | "cancelled";
|
|
10
|
-
startedAt: Date;
|
|
11
|
-
completedAt?: Date;
|
|
12
|
-
result?: string;
|
|
13
|
-
error?: string;
|
|
14
|
-
progress?: {
|
|
15
|
-
toolCalls: number;
|
|
16
|
-
lastTool?: string;
|
|
17
|
-
lastUpdate: Date;
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface BackgroundTaskInput {
|
|
22
|
-
description: string;
|
|
23
|
-
prompt: string;
|
|
24
|
-
agent: string;
|
|
25
|
-
parentSessionID: string;
|
|
26
|
-
parentMessageID: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// API Response Types - SDK wraps responses in { data: T } format
|
|
30
|
-
export interface SessionCreateResponse {
|
|
31
|
-
data?: {
|
|
32
|
-
id?: string;
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// SessionStatus from OpenCode SDK - status is a discriminated union with 'type' field
|
|
37
|
-
export type SessionStatus =
|
|
38
|
-
| { type: "idle" }
|
|
39
|
-
| { type: "retry"; attempt: number; message: string; next: number }
|
|
40
|
-
| { type: "busy" };
|
|
41
|
-
|
|
42
|
-
// session.status() returns { data: map of sessionID -> SessionStatus }
|
|
43
|
-
export interface SessionStatusResponse {
|
|
44
|
-
data?: {
|
|
45
|
-
[sessionID: string]: SessionStatus;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface MessagePart {
|
|
50
|
-
type: string;
|
|
51
|
-
text?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface MessageInfo {
|
|
55
|
-
role?: "user" | "assistant";
|
|
56
|
-
sessionID?: string;
|
|
57
|
-
type?: string;
|
|
58
|
-
name?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface SessionMessage {
|
|
62
|
-
info?: MessageInfo;
|
|
63
|
-
parts?: MessagePart[];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface SessionMessagesResponse {
|
|
67
|
-
data?: SessionMessage[];
|
|
68
|
-
}
|