pi-subagents 0.3.0 → 0.3.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2] - 2026-01-25
4
+
5
+ ### Performance
6
+ - **4x faster polling** - Reduced poll interval from 1000ms to 250ms (efficient with mtime caching)
7
+ - **Mtime-based caching** - status.json and output tail reads cached to avoid redundant I/O
8
+ - **Unified throttled updates** - All onUpdate calls consolidated under 50ms throttle
9
+ - **Widget change detection** - Hash-based change detection skips no-op re-renders
10
+ - **Array optimizations** - Use concat instead of spread for chain progress updates
11
+
12
+ ### Fixed
13
+ - **Timer leaks** - Track and clear pendingTimer and cleanupTimers properly
14
+ - **Updates after close** - processClosed flag prevents updates after process terminates
15
+ - **Session cleanup** - Clear cleanup timers on session_start/switch/branch/shutdown
16
+
17
+ ## [0.3.1] - 2026-01-24
18
+
19
+ ### Changed
20
+ - **Major code refactor** - Split monolithic index.ts into focused modules:
21
+ - `execution.ts` - Core runSync function for single agent execution
22
+ - `chain-execution.ts` - Chain orchestration (sequential + parallel steps)
23
+ - `async-execution.ts` - Async/background execution support
24
+ - `render.ts` - TUI rendering (widget, tool result display)
25
+ - `schemas.ts` - TypeBox parameter schemas
26
+ - `formatters.ts` - Output formatting utilities
27
+ - `utils.ts` - Shared utility functions
28
+ - `types.ts` - Shared type definitions and constants
29
+
30
+ ### Fixed
31
+ - **Expanded view visibility** - Running chains now properly show:
32
+ - Task preview (truncated to 80 chars) for each step
33
+ - Recent tools fallback when between tool calls
34
+ - Increased recent output from 2 to 3 lines
35
+ - **Progress matching** - Added agent name fallback when index doesn't match
36
+ - **Type safety** - Added defensive `?? []` for `recentOutput` access on union types
37
+
3
38
  ## [0.3.0] - 2026-01-24
4
39
 
5
40
  ### Added
package/agents.ts CHANGED
@@ -160,13 +160,3 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
160
160
 
161
161
  return { agents: Array.from(agentMap.values()), projectAgentsDir };
162
162
  }
163
-
164
- export function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {
165
- if (agents.length === 0) return { text: "none", remaining: 0 };
166
- const listed = agents.slice(0, maxItems);
167
- const remaining = agents.length - listed.length;
168
- return {
169
- text: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join("; "),
170
- remaining,
171
- };
172
- }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Async execution logic for subagent tool
3
+ */
4
+
5
+ import { spawn } from "node:child_process";
6
+ import * as fs from "node:fs";
7
+ import * as os from "node:os";
8
+ import * as path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { createRequire } from "node:module";
11
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
12
+ import type { AgentConfig } from "./agents.js";
13
+ import { isParallelStep, type ChainStep, type SequentialStep } from "./settings.js";
14
+ import {
15
+ type ArtifactConfig,
16
+ type Details,
17
+ type MaxOutputConfig,
18
+ ASYNC_DIR,
19
+ RESULTS_DIR,
20
+ } from "./types.js";
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const jitiCliPath: string | undefined = (() => {
24
+ try {
25
+ return path.join(path.dirname(require.resolve("jiti/package.json")), "lib/jiti-cli.mjs");
26
+ } catch {
27
+ return undefined;
28
+ }
29
+ })();
30
+
31
+ export interface AsyncExecutionContext {
32
+ pi: ExtensionAPI;
33
+ cwd: string;
34
+ currentSessionId: string;
35
+ }
36
+
37
+ export interface AsyncChainParams {
38
+ chain: ChainStep[];
39
+ agents: AgentConfig[];
40
+ ctx: AsyncExecutionContext;
41
+ cwd?: string;
42
+ maxOutput?: MaxOutputConfig;
43
+ artifactsDir?: string;
44
+ artifactConfig: ArtifactConfig;
45
+ shareEnabled: boolean;
46
+ sessionRoot?: string;
47
+ }
48
+
49
+ export interface AsyncSingleParams {
50
+ agent: string;
51
+ task: string;
52
+ agentConfig: AgentConfig;
53
+ ctx: AsyncExecutionContext;
54
+ cwd?: string;
55
+ maxOutput?: MaxOutputConfig;
56
+ artifactsDir?: string;
57
+ artifactConfig: ArtifactConfig;
58
+ shareEnabled: boolean;
59
+ sessionRoot?: string;
60
+ }
61
+
62
+ export interface AsyncExecutionResult {
63
+ content: Array<{ type: "text"; text: string }>;
64
+ details: Details;
65
+ isError?: boolean;
66
+ }
67
+
68
+ /**
69
+ * Check if jiti is available for async execution
70
+ */
71
+ export function isAsyncAvailable(): boolean {
72
+ return jitiCliPath !== undefined;
73
+ }
74
+
75
+ /**
76
+ * Spawn the async runner process
77
+ */
78
+ function spawnRunner(cfg: object, suffix: string, cwd: string): number | undefined {
79
+ if (!jitiCliPath) return undefined;
80
+
81
+ const cfgPath = path.join(os.tmpdir(), `pi-async-cfg-${suffix}.json`);
82
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg));
83
+ const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
84
+
85
+ const proc = spawn("node", [jitiCliPath, runner, cfgPath], {
86
+ cwd,
87
+ detached: true,
88
+ stdio: "ignore",
89
+ });
90
+ proc.unref();
91
+ return proc.pid;
92
+ }
93
+
94
+ /**
95
+ * Execute a chain asynchronously
96
+ */
97
+ export function executeAsyncChain(
98
+ id: string,
99
+ params: AsyncChainParams,
100
+ ): AsyncExecutionResult {
101
+ const { chain, agents, ctx, cwd, maxOutput, artifactsDir, artifactConfig, shareEnabled, sessionRoot } = params;
102
+
103
+ // Async mode doesn't support parallel steps (v1 limitation)
104
+ const hasParallelInChain = chain.some(isParallelStep);
105
+ if (hasParallelInChain) {
106
+ return {
107
+ content: [{ type: "text", text: "Async mode doesn't support chains with parallel steps. Use clarify: true (sync mode) for parallel-in-chain." }],
108
+ isError: true,
109
+ details: { mode: "chain" as const, results: [] },
110
+ };
111
+ }
112
+
113
+ // At this point, all steps are sequential
114
+ const seqSteps = chain as SequentialStep[];
115
+
116
+ // Validate all agents exist before building steps
117
+ for (const s of seqSteps) {
118
+ if (!agents.find((x) => x.name === s.agent)) {
119
+ return {
120
+ content: [{ type: "text", text: `Unknown agent: ${s.agent}` }],
121
+ isError: true,
122
+ details: { mode: "chain" as const, results: [] },
123
+ };
124
+ }
125
+ }
126
+
127
+ const asyncDir = path.join(ASYNC_DIR, id);
128
+ try {
129
+ fs.mkdirSync(asyncDir, { recursive: true });
130
+ } catch {}
131
+
132
+ const steps = seqSteps.map((s, i) => {
133
+ const a = agents.find((x) => x.name === s.agent)!;
134
+ return {
135
+ agent: s.agent,
136
+ // First step validated to have task; others default to {previous} (replaced by runner)
137
+ task: s.task ?? "{previous}",
138
+ cwd: s.cwd,
139
+ model: a.model,
140
+ tools: a.tools,
141
+ systemPrompt: a.systemPrompt?.trim() || null,
142
+ };
143
+ });
144
+
145
+ const runnerCwd = cwd ?? ctx.cwd;
146
+ const pid = spawnRunner(
147
+ {
148
+ id,
149
+ steps,
150
+ resultPath: path.join(RESULTS_DIR, `${id}.json`),
151
+ cwd: runnerCwd,
152
+ placeholder: "{previous}",
153
+ maxOutput,
154
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
155
+ artifactConfig,
156
+ share: shareEnabled,
157
+ sessionDir: sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined,
158
+ asyncDir,
159
+ sessionId: ctx.currentSessionId,
160
+ },
161
+ id,
162
+ runnerCwd,
163
+ );
164
+
165
+ if (pid) {
166
+ const firstAgent = chain[0] as SequentialStep;
167
+ ctx.pi.events.emit("subagent_enhanced:started", {
168
+ id,
169
+ pid,
170
+ agent: firstAgent.agent,
171
+ task: firstAgent.task?.slice(0, 50),
172
+ chain: chain.map((s) => (s as SequentialStep).agent),
173
+ cwd: runnerCwd,
174
+ asyncDir,
175
+ });
176
+ ctx.pi.events.emit("subagent:started", {
177
+ id,
178
+ pid,
179
+ agent: firstAgent.agent,
180
+ task: firstAgent.task?.slice(0, 50),
181
+ chain: chain.map((s) => (s as SequentialStep).agent),
182
+ cwd: runnerCwd,
183
+ asyncDir,
184
+ });
185
+ }
186
+
187
+ return {
188
+ content: [
189
+ { type: "text", text: `Async chain: ${chain.map((s) => (s as SequentialStep).agent).join(" -> ")} [${id}]` },
190
+ ],
191
+ details: { mode: "chain", results: [], asyncId: id, asyncDir },
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Execute a single agent asynchronously
197
+ */
198
+ export function executeAsyncSingle(
199
+ id: string,
200
+ params: AsyncSingleParams,
201
+ ): AsyncExecutionResult {
202
+ const { agent, task, agentConfig, ctx, cwd, maxOutput, artifactsDir, artifactConfig, shareEnabled, sessionRoot } = params;
203
+
204
+ const asyncDir = path.join(ASYNC_DIR, id);
205
+ try {
206
+ fs.mkdirSync(asyncDir, { recursive: true });
207
+ } catch {}
208
+
209
+ const runnerCwd = cwd ?? ctx.cwd;
210
+ const pid = spawnRunner(
211
+ {
212
+ id,
213
+ steps: [
214
+ {
215
+ agent,
216
+ task,
217
+ cwd,
218
+ model: agentConfig.model,
219
+ tools: agentConfig.tools,
220
+ systemPrompt: agentConfig.systemPrompt?.trim() || null,
221
+ },
222
+ ],
223
+ resultPath: path.join(RESULTS_DIR, `${id}.json`),
224
+ cwd: runnerCwd,
225
+ placeholder: "{previous}",
226
+ maxOutput,
227
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
228
+ artifactConfig,
229
+ share: shareEnabled,
230
+ sessionDir: sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined,
231
+ asyncDir,
232
+ sessionId: ctx.currentSessionId,
233
+ },
234
+ id,
235
+ runnerCwd,
236
+ );
237
+
238
+ if (pid) {
239
+ ctx.pi.events.emit("subagent_enhanced:started", {
240
+ id,
241
+ pid,
242
+ agent,
243
+ task: task?.slice(0, 50),
244
+ cwd: runnerCwd,
245
+ asyncDir,
246
+ });
247
+ ctx.pi.events.emit("subagent:started", {
248
+ id,
249
+ pid,
250
+ agent,
251
+ task: task?.slice(0, 50),
252
+ cwd: runnerCwd,
253
+ asyncDir,
254
+ });
255
+ }
256
+
257
+ return {
258
+ content: [{ type: "text", text: `Async: ${agent} [${id}]` }],
259
+ details: { mode: "single", results: [], asyncId: id, asyncDir },
260
+ };
261
+ }