pi-thread-engine 0.4.6 → 0.4.7

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.
@@ -1,36 +1,36 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import type { ThreadRegistry } from "./registry.js";
3
- import type { Thread, ThreadTask } from "./types.js";
4
-
5
- /**
6
- * Executor — runs thread tasks.
7
- *
8
- * Two backends:
9
- * - "subagent": delegates to pi-subagents via sendUserMessage (inherits agent specialization, TUI, artifacts)
10
- * - "native": spawns pi -p directly (for fusion/zero where we need raw control)
11
- */
12
- export class ThreadExecutor {
13
- constructor(
14
- private pi: ExtensionAPI,
15
- private registry: ThreadRegistry
16
- ) {}
17
-
18
- // ── Native execution (pi -p) ────────────────────────────────
19
-
20
- private async runTaskNative(thread: Thread, task: ThreadTask): Promise<void> {
21
- const cwd = thread.config.cwd ?? process.cwd();
22
- this.registry.startTask(thread.id, task.id);
23
-
24
- try {
25
- const args = ["-p", task.prompt];
26
- if (task.model) args.unshift("-m", task.model);
27
-
28
- const result = await this.pi.exec("pi", args, {
29
- cwd,
30
- timeout: 10 * 60 * 1000,
31
- });
32
-
33
- if (result.code === 0) {
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { ThreadRegistry } from "./registry.js";
3
+ import type { Thread, ThreadTask } from "./types.js";
4
+
5
+ /**
6
+ * Executor — runs thread tasks.
7
+ *
8
+ * Two backends:
9
+ * - "subagent": delegates to pi-subagents via sendUserMessage (inherits agent specialization, TUI, artifacts)
10
+ * - "native": spawns pi -p directly (for fusion/zero where we need raw control)
11
+ */
12
+ export class ThreadExecutor {
13
+ constructor(
14
+ private pi: ExtensionAPI,
15
+ private registry: ThreadRegistry
16
+ ) {}
17
+
18
+ // ── Native execution (pi -p) ────────────────────────────────
19
+
20
+ private async runTaskNative(thread: Thread, task: ThreadTask): Promise<void> {
21
+ const cwd = thread.config.cwd ?? process.cwd();
22
+ this.registry.startTask(thread.id, task.id);
23
+
24
+ try {
25
+ const args = ["-p", task.prompt];
26
+ if (task.model) args.unshift("-m", task.model);
27
+
28
+ const result = await this.pi.exec("pi", args, {
29
+ cwd,
30
+ timeout: 10 * 60 * 1000,
31
+ });
32
+
33
+ if (result.code === 0) {
34
34
  this.registry.completeTask(thread.id, task.id, result.stdout);
35
35
  // Parse --mode json output for token/cost data
36
36
  try {
@@ -49,190 +49,248 @@ export class ThreadExecutor {
49
49
  };
50
50
  }
51
51
  }
52
- } catch {}
53
- } else {
54
- this.registry.failTask(thread.id, task.id, result.stderr || `Exit code: ${result.code}`);
55
- }
56
- } catch (err: any) {
57
- this.registry.failTask(thread.id, task.id, err.message ?? String(err));
58
- }
59
- }
60
-
61
- // ── Subagent execution (via sendUserMessage) ────────────────
62
-
63
- private launchSubagentParallel(thread: Thread): void {
64
- const agent = thread.config.agent ?? "worker";
65
- const tasks = thread.tasks.map((t) => ({
66
- agent,
67
- task: t.prompt,
68
- ...(t.model ? { model: t.model } : {}),
69
- }));
70
-
71
- // Send as a user message that pi-subagents will pick up
72
- this.pi.sendUserMessage(
73
- `Run these tasks in parallel using subagent:\n\`\`\`json\n${JSON.stringify({ tasks }, null, 2)}\n\`\`\``,
74
- { deliverAs: "followUp" }
75
- );
76
-
77
- // Mark all tasks as running (subagent handles the rest)
78
- this.registry.startThread(thread.id);
79
- for (const task of thread.tasks) {
80
- this.registry.startTask(thread.id, task.id);
81
- }
82
- }
83
-
84
- private launchSubagentChain(thread: Thread): void {
85
- const agent = thread.config.agent ?? "worker";
86
- const chain = thread.tasks.map((t, i) => ({
87
- agent,
88
- task: i === 0 ? t.prompt : `Continue: ${t.prompt}. Previous context: {previous}`,
89
- ...(t.model ? { model: t.model } : {}),
90
- }));
91
-
92
- this.pi.sendUserMessage(
93
- `Run this chain using subagent:\n\`\`\`json\n${JSON.stringify({ chain }, null, 2)}\n\`\`\``,
94
- { deliverAs: "followUp" }
95
- );
96
-
97
- this.registry.startThread(thread.id);
98
- this.registry.startTask(thread.id, thread.tasks[0].id);
99
- }
100
-
101
- private launchSubagentMeta(thread: Thread): void {
102
- const chain = [
103
- { agent: "scout", task: thread.tasks[0]?.prompt ?? "Scout the codebase" },
104
- { agent: "planner", task: "{previous}" },
105
- { agent: "worker", task: "{previous}" },
106
- { agent: "reviewer", task: "{previous}" },
107
- ];
108
-
109
- this.pi.sendUserMessage(
110
- `Run this meta pipeline using subagent:\n\`\`\`json\n${JSON.stringify({ chain }, null, 2)}\n\`\`\``,
111
- { deliverAs: "followUp" }
112
- );
113
-
114
- this.registry.startThread(thread.id);
115
- this.registry.startTask(thread.id, thread.tasks[0].id);
116
- }
117
-
118
- // ── Fusion (native, multi-model) ────────────────────────────
119
-
120
- async execFusion(thread: Thread): Promise<void> {
121
- this.registry.startThread(thread.id);
122
- // All tasks run in parallel with potentially different models
123
- await Promise.allSettled(thread.tasks.map((task) => this.runTaskNative(thread, task)));
124
- }
125
-
126
- // ── Zero-touch (native + verification) ──────────────────────
127
-
128
- async execZero(thread: Thread): Promise<void> {
129
- this.registry.startThread(thread.id);
130
- const task = thread.tasks[0];
131
- await this.runTaskNative(thread, task);
132
-
133
- // If task succeeded and we have a verify command, run it
134
- if (task.state === "completed" && thread.config.verifyCommand) {
135
- const cwd = thread.config.cwd ?? process.cwd();
136
- try {
137
- const verify = await this.pi.exec("bash", ["-c", thread.config.verifyCommand], {
138
- cwd,
139
- timeout: 5 * 60 * 1000,
140
- });
141
- if (verify.code !== 0) {
142
- // Override the completed state to failed
143
- task.state = "failed";
144
- task.error = `Verification failed (${thread.config.verifyCommand}): ${verify.stderr || verify.stdout}`;
145
- thread.state = "failed";
146
- thread.completedAt = Date.now();
147
- thread.duration = thread.completedAt - (thread.startedAt ?? thread.createdAt);
148
- }
149
- } catch (err: any) {
150
- task.state = "failed";
151
- task.error = `Verification error: ${err.message}`;
152
- thread.state = "failed";
153
- thread.completedAt = Date.now();
154
- }
155
- }
156
- }
157
-
158
- // ── Dispatch ─────────────────────────────────────────────────
159
-
160
- async dispatch(
161
- thread: Thread,
162
- opts?: { onCheckpoint?: (phase: number, task: ThreadTask) => Promise<boolean> }
163
- ): Promise<void> {
164
- const backend = thread.config.backend;
165
-
166
- if (backend === "subagent") {
167
- switch (thread.type) {
168
- case "parallel":
169
- return this.launchSubagentParallel(thread);
170
- case "chained":
171
- return this.launchSubagentChain(thread);
172
- case "meta":
173
- return this.launchSubagentMeta(thread);
174
- default:
175
- // Fall through to native for unsupported subagent types
176
- break;
177
- }
178
- }
179
-
180
- // Native execution
181
- switch (thread.type) {
182
- case "base":
183
- case "long":
184
- this.registry.startThread(thread.id);
185
- return this.runTaskNative(thread, thread.tasks[0]);
186
- case "parallel":
187
- this.registry.startThread(thread.id);
188
- await Promise.allSettled(thread.tasks.map((t) => this.runTaskNative(thread, t)));
189
- return;
190
- case "fusion":
191
- return this.execFusion(thread);
192
- case "zero":
193
- return this.execZero(thread);
194
- case "chained": {
195
- this.registry.startThread(thread.id);
196
- for (let i = 0; i < thread.tasks.length; i++) {
197
- if (i > 0 && opts?.onCheckpoint) {
198
- const proceed = await opts.onCheckpoint(i, thread.tasks[i]);
199
- if (!proceed) {
200
- this.registry.kill(thread.id);
201
- return;
202
- }
203
- }
204
- await this.runTaskNative(thread, thread.tasks[i]);
205
- if (thread.tasks[i].state === "failed") return;
206
- }
207
- return;
208
- }
209
- case "meta":
210
- this.registry.startThread(thread.id);
211
- for (const task of thread.tasks) {
212
- await this.runTaskNative(thread, task);
213
- if (task.state === "failed") return;
214
- }
215
- return;
216
- }
217
- }
218
-
219
- /** Inject a reply into a running thread — sends message to its session */
220
- injectReply(threadId: string, message: string): void {
221
- const thread = this.registry.get(threadId);
222
- if (!thread) return;
223
-
224
- // Mark the blocked task as no longer needing input
225
- for (const task of thread.tasks) {
226
- if (task.state === "needs_input") {
227
- task.state = "running";
228
- }
229
- }
230
-
231
- // Forward the reply to the running subagent session via sendUserMessage
232
- // The subagent will pick up the new context and continue
233
- this.pi.sendUserMessage(
234
- `[Thread ${threadId}] Reply to blocked thread: ${message}`,
235
- { deliverAs: "followUp" }
236
- );
237
- }
238
- }
52
+ } catch {}
53
+ } else {
54
+ this.registry.failTask(thread.id, task.id, result.stderr || `Exit code: ${result.code}`);
55
+ }
56
+ } catch (err: any) {
57
+ this.registry.failTask(thread.id, task.id, err.message ?? String(err));
58
+ }
59
+ }
60
+
61
+ // ── Subagent execution (via sendUserMessage) ────────────────
62
+
63
+ private launchSubagentParallel(thread: Thread): void {
64
+ const agent = thread.config.agent ?? "worker";
65
+ const tasks = thread.tasks.map((t) => ({
66
+ agent,
67
+ task: t.prompt,
68
+ ...(t.model ? { model: t.model } : {}),
69
+ }));
70
+
71
+ // Send as a user message that pi-subagents will pick up
72
+ this.pi.sendUserMessage(
73
+ `Run these tasks in parallel using subagent:\n\`\`\`json\n${JSON.stringify({ tasks }, null, 2)}\n\`\`\``,
74
+ { deliverAs: "followUp" }
75
+ );
76
+
77
+ // Mark all tasks as running (subagent handles the rest)
78
+ this.registry.startThread(thread.id);
79
+ for (const task of thread.tasks) {
80
+ this.registry.startTask(thread.id, task.id);
81
+ }
82
+ }
83
+
84
+ private launchSubagentChain(thread: Thread): void {
85
+ const agent = thread.config.agent ?? "worker";
86
+ const chain = thread.tasks.map((t, i) => ({
87
+ agent,
88
+ task: i === 0 ? t.prompt : `Continue: ${t.prompt}. Previous context: {previous}`,
89
+ ...(t.model ? { model: t.model } : {}),
90
+ }));
91
+
92
+ this.pi.sendUserMessage(
93
+ `Run this chain using subagent:\n\`\`\`json\n${JSON.stringify({ chain }, null, 2)}\n\`\`\``,
94
+ { deliverAs: "followUp" }
95
+ );
96
+
97
+ this.registry.startThread(thread.id);
98
+ this.registry.startTask(thread.id, thread.tasks[0].id);
99
+ }
100
+
101
+ private launchSubagentMeta(thread: Thread): void {
102
+ const chain = [
103
+ { agent: "scout", task: thread.tasks[0]?.prompt ?? "Scout the codebase" },
104
+ { agent: "planner", task: "{previous}" },
105
+ { agent: "worker", task: "{previous}" },
106
+ { agent: "reviewer", task: "{previous}" },
107
+ ];
108
+
109
+ this.pi.sendUserMessage(
110
+ `Run this meta pipeline using subagent:\n\`\`\`json\n${JSON.stringify({ chain }, null, 2)}\n\`\`\``,
111
+ { deliverAs: "followUp" }
112
+ );
113
+
114
+ this.registry.startThread(thread.id);
115
+ this.registry.startTask(thread.id, thread.tasks[0].id);
116
+ }
117
+
118
+ // ── Fusion (native, multi-model) ────────────────────────────
119
+
120
+ async execFusion(thread: Thread): Promise<void> {
121
+ this.registry.startThread(thread.id);
122
+ // All tasks run in parallel with potentially different models
123
+ await Promise.allSettled(thread.tasks.map((task) => this.runTaskNative(thread, task)));
124
+ }
125
+
126
+ // ── Zero-touch (native + verification) ──────────────────────
127
+
128
+ async execZero(thread: Thread): Promise<void> {
129
+ this.registry.startThread(thread.id);
130
+ const task = thread.tasks[0];
131
+ await this.runTaskNative(thread, task);
132
+
133
+ // If task succeeded and we have a verify command, run it
134
+ if (task.state === "completed" && thread.config.verifyCommand) {
135
+ const cwd = thread.config.cwd ?? process.cwd();
136
+ try {
137
+ const verify = await this.pi.exec("bash", ["-c", thread.config.verifyCommand], {
138
+ cwd,
139
+ timeout: 5 * 60 * 1000,
140
+ });
141
+ if (verify.code !== 0) {
142
+ // Override the completed state to failed
143
+ task.state = "failed";
144
+ task.error = `Verification failed (${thread.config.verifyCommand}): ${verify.stderr || verify.stdout}`;
145
+ thread.state = "failed";
146
+ thread.completedAt = Date.now();
147
+ thread.duration = thread.completedAt - (thread.startedAt ?? thread.createdAt);
148
+ }
149
+ } catch (err: any) {
150
+ task.state = "failed";
151
+ task.error = `Verification error: ${err.message}`;
152
+ thread.state = "failed";
153
+ thread.completedAt = Date.now();
154
+ }
155
+ }
156
+ }
157
+
158
+ // ── Dispatch ─────────────────────────────────────────────────
159
+
160
+ async dispatch(
161
+ thread: Thread,
162
+ opts?: { onCheckpoint?: (phase: number, task: ThreadTask) => Promise<boolean> }
163
+ ): Promise<void> {
164
+ const backend = thread.config.backend;
165
+
166
+ if (backend === "subagent") {
167
+ switch (thread.type) {
168
+ case "parallel":
169
+ return this.launchSubagentParallel(thread);
170
+ case "chained":
171
+ return this.launchSubagentChain(thread);
172
+ case "meta":
173
+ return this.launchSubagentMeta(thread);
174
+ default:
175
+ // Fall through to native for unsupported subagent types
176
+ break;
177
+ }
178
+ }
179
+
180
+ // Native execution
181
+ switch (thread.type) {
182
+ case "base":
183
+ case "long":
184
+ case "plan":
185
+ case "scheduled":
186
+ this.registry.startThread(thread.id);
187
+ return this.runTaskNative(thread, thread.tasks[0]);
188
+ case "parallel":
189
+ this.registry.startThread(thread.id);
190
+ await Promise.allSettled(thread.tasks.map((t) => this.runTaskNative(thread, t)));
191
+ return;
192
+ case "fusion":
193
+ return this.execFusion(thread);
194
+ case "zero":
195
+ return this.execZero(thread);
196
+ case "worktree":
197
+ return this.execWorktree(thread);
198
+ case "chained": {
199
+ this.registry.startThread(thread.id);
200
+ for (let i = 0; i < thread.tasks.length; i++) {
201
+ if (i > 0 && opts?.onCheckpoint) {
202
+ const proceed = await opts.onCheckpoint(i, thread.tasks[i]);
203
+ if (!proceed) {
204
+ this.registry.kill(thread.id);
205
+ return;
206
+ }
207
+ }
208
+ await this.runTaskNative(thread, thread.tasks[i]);
209
+ if (thread.tasks[i].state === "failed") return;
210
+ }
211
+ return;
212
+ }
213
+ case "meta":
214
+ this.registry.startThread(thread.id);
215
+ for (const task of thread.tasks) {
216
+ await this.runTaskNative(thread, task);
217
+ if (task.state === "failed") return;
218
+ }
219
+ return;
220
+ }
221
+ }
222
+
223
+ /** Execute a thread in an isolated git worktree */
224
+ private async execWorktree(thread: Thread): Promise<void> {
225
+ const cwd = thread.config.cwd ?? process.cwd();
226
+ this.registry.startThread(thread.id);
227
+
228
+ try {
229
+ const { createWorktree, removeWorktree, findRepoRoot, pushWorktreeChanges } = await import("./worktree.js");
230
+ const repoPath = findRepoRoot(cwd);
231
+
232
+ if (!repoPath) {
233
+ this.registry.failTask(thread.id, thread.tasks[0].id, "Not in a git repository");
234
+ return;
235
+ }
236
+
237
+ // Create worktree
238
+ const wt = createWorktree(cwd, thread.id);
239
+ const task = thread.tasks[0];
240
+
241
+ // Write task prompt to worktree
242
+ const { writeFileSync } = await import("fs");
243
+ const { join } = await import("path");
244
+ writeFileSync(join(wt.path, ".pi-thread-task.md"), task.prompt, "utf8");
245
+
246
+ // Run the task inside the worktree using pi -p
247
+ try {
248
+ const result = await this.pi.exec("pi", ["-p", task.prompt], {
249
+ cwd: wt.path,
250
+ timeout: 30 * 60 * 1000, // 30 min max
251
+ });
252
+
253
+ if (result.code === 0) {
254
+ // Write result marker
255
+ writeFileSync(join(wt.path, ".pi-thread-result.json"), JSON.stringify({
256
+ threadId: thread.id,
257
+ status: "completed",
258
+ output: result.stdout,
259
+ timestamp: Date.now(),
260
+ }, null, 2), "utf8");
261
+
262
+ this.registry.completeTask(thread.id, task.id, result.stdout);
263
+ } else {
264
+ this.registry.failTask(thread.id, task.id, result.stderr || `Exit code: ${result.code}`);
265
+ }
266
+ } catch (err: any) {
267
+ this.registry.failTask(thread.id, task.id, err.message ?? String(err));
268
+ }
269
+
270
+ // Always clean up worktree after execution
271
+ removeWorktree(cwd, thread.id);
272
+ } catch (err: any) {
273
+ this.registry.failTask(thread.id, thread.tasks[0].id, err.message ?? String(err));
274
+ }
275
+ }
276
+
277
+ /** Inject a reply into a running thread — sends message to its session */
278
+ injectReply(threadId: string, message: string): void {
279
+ const thread = this.registry.get(threadId);
280
+ if (!thread) return;
281
+
282
+ // Mark the blocked task as no longer needing input
283
+ for (const task of thread.tasks) {
284
+ if (task.state === "needs_input") {
285
+ task.state = "running";
286
+ }
287
+ }
288
+
289
+ // Forward the reply to the running subagent session via sendUserMessage
290
+ // The subagent will pick up the new context and continue
291
+ this.pi.sendUserMessage(
292
+ `[Thread ${threadId}] Reply to blocked thread: ${message}`,
293
+ { deliverAs: "followUp" }
294
+ );
295
+ }
296
+ }
package/src/core/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Thread types from the thread engineering framework */
2
- export type ThreadType = "base" | "parallel" | "chained" | "fusion" | "meta" | "long" | "zero";
2
+ export type ThreadType = "base" | "parallel" | "chained" | "fusion" | "meta" | "long" | "zero" | "worktree" | "plan" | "scheduled" | "monitor";
3
3
 
4
4
  /** Thread lifecycle states */
5
5
  export type ThreadState = "pending" | "running" | "paused" | "completed" | "failed" | "killed" | "needs_input";