@ferris1225/pi-subagents 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -351,7 +351,7 @@ agent's default — its frontmatter `thinking`, else the global default). The gl
351
351
  | `agentScope` | `user`, `project`, or `both`; controls which user/project agent directories are discovered. |
352
352
  | `maxConcurrency` | Max sub-agent processes running at once (1–16, default 4), and the max tasks one parallel `subagent` call accepts. Extra work waits in the queue. |
353
353
  | `maxFixRounds` | Auto-fix rounds when a reviewer returns `REVIEW_FAIL`: the extension dispatches a `worker` (briefed with the review's concrete findings) then a `reviewer` re-review, repeating up to this many times before waking the main agent with the full chain. `0` disables it (the main agent handles fixes itself). Default 2. The reviewer stays read-only and in its own context; the loop is orchestrated by the extension, not by the reviewer. |
354
- | `idleTimeoutSec` | Idle timeout in seconds: a sub-agent whose stdout (JSON event stream) goes silent for this long is terminated and retried with the fallback model (if one is available). `0` disables the idle watchdog. Default 90. Unlike the total timeout, this only fires when the child produces no output at all — a long but active run is never interrupted. |
354
+ | `idleTimeoutSec` | Idle timeout in seconds: a sub-agent whose stdout (JSON event stream) goes silent for this long is terminated and retried with the fallback model (if one is available). `0` disables the idle watchdog. Default 90. This only fires when the child produces no output at all — a long but active run is never interrupted. |
355
355
 
356
356
  ### Configuration migration
357
357
 
@@ -382,10 +382,10 @@ At runtime, if an agent's model fails at the provider level before producing any
382
382
  model id, auth, thinking level, quota, ...), the run is retried **once** with the main window's
383
383
  current model. This per-run degradation is never persisted — a transient provider hiccup must
384
384
  not silently downgrade the configured model — and it does not apply to task-level failures
385
- (the model worked, the task failed), aborts, or total timeouts. Idle timeouts (the child's
386
- stdout goes silent for `idleTimeoutSec` seconds) are treated as model-level failures and do
387
- trigger the fallback, since a stalled SSE stream is usually a provider-side issue. Results
388
- carry a `model fell back from …` note when it happened.
385
+ (the model worked, the task failed) or aborts. Idle timeouts (the child's stdout goes silent
386
+ for `idleTimeoutSec` seconds) are treated as model-level failures and do trigger the fallback,
387
+ since a stalled SSE stream is usually a provider-side issue. Results carry a `model fell back
388
+ from …` note when it happened.
389
389
 
390
390
  Thinking strength uses this precedence: `agentThinkingLevels` entry → agent frontmatter `thinking` → `thinkingLevel` default.
391
391
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ferris1225/pi-subagents",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Focused sub-agent delegation for pi: explore / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -12,7 +12,6 @@
12
12
  * runaway recursion and keeps child context windows clean.
13
13
  */
14
14
 
15
- import type { AgentToolResult } from "@earendil-works/pi-agent-core";
16
15
  import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
16
  import { Text, truncateToWidth } from "@earendil-works/pi-tui";
18
17
  import { Type } from "typebox";
@@ -31,7 +30,6 @@ import { buildDelegationDirective } from "./prompt.ts";
31
30
  import { runSetup } from "./setup.ts";
32
31
  import {
33
32
  currentSubagentDepth,
34
- getFinalOutput,
35
33
  getResultOutput,
36
34
  isFailedResult,
37
35
  reviewVerdict,
@@ -217,7 +215,7 @@ export default function (pi: ExtensionAPI): void {
217
215
  ],
218
216
  parameters: SubagentParams,
219
217
 
220
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
218
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
221
219
  monitor.beginTurn();
222
220
  let config = await loadConfig(configPath);
223
221
  // Pick up concurrency changes from /subagents-setup without a restart.
package/src/spawn.ts CHANGED
@@ -4,8 +4,7 @@
4
4
  * written to a temp file and passed via `--append-system-prompt` (which accepts a
5
5
  * file path). The task itself is sent through the child's stdin pipe, not another
6
6
  * temp file or command-line argument. Child stdout is a JSON-lines event stream;
7
- * we accumulate assistant messages from `message_end` events and stream partial
8
- * output back via onUpdate.
7
+ * we accumulate assistant messages from `message_end` events.
9
8
  *
10
9
  * Adapted from the official pi example `examples/extensions/subagent`.
11
10
  */
@@ -16,7 +15,6 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
16
15
  import { tmpdir } from "node:os";
17
16
  import { basename, join } from "node:path";
18
17
  import { StringDecoder } from "node:string_decoder";
19
- import type { AgentToolResult } from "@earendil-works/pi-agent-core";
20
18
  import type { Message } from "@earendil-works/pi-ai";
21
19
  import type { AgentConfig, AgentSource } from "./agents.ts";
22
20
  import { DEFAULT_THINKING_LEVEL, type ThinkingLevel } from "./config.ts";
@@ -28,8 +26,6 @@ import { DEFAULT_THINKING_LEVEL, type ThinkingLevel } from "./config.ts";
28
26
  /** Default thinking level for sub-agents. pi clamps it to the resolved model's support. */
29
27
  export const SUBAGENT_THINKING_LEVEL: ThinkingLevel = DEFAULT_THINKING_LEVEL;
30
28
  export const DEPTH_ENV_VAR = "PI_SUBAGENT_DEPTH";
31
- /** No default deadline: sub-agents may run until completion or explicit cancellation. */
32
- export const SUBAGENT_TIMEOUT_MS = 0;
33
29
  export const SUBAGENT_KILL_GRACE_MS = 5_000;
34
30
  /** Default idle watchdog: terminate a child whose stdout goes silent for this
35
31
  * many milliseconds. 0 disables it. The actual value comes from config
@@ -70,8 +66,6 @@ export interface SubagentDetails {
70
66
  background?: boolean;
71
67
  }
72
68
 
73
- export type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
74
-
75
69
  export type SubagentLiveEvent =
76
70
  | { kind: "status"; status: "queued" | "running" | "done" | "failed" }
77
71
  | { kind: "usage"; usage: UsageStats; model?: string }
@@ -165,7 +159,6 @@ export function isModelLevelFailure(result: SingleResult): boolean {
165
159
  if (result.errorMessage?.includes("idle timeout")) return true;
166
160
  // The model produced text: the failure belongs to the task, not the model.
167
161
  if (getFinalOutput(result.messages)) return false;
168
- if (result.errorMessage?.includes("timed out")) return false;
169
162
  // Require evidence the failure came from the model/provider (an error
170
163
  // message or stderr), not from the child process failing to start.
171
164
  return result.messages.length > 0 || result.stderr.trim().length > 0;
@@ -178,26 +171,6 @@ export function getResultOutput(result: SingleResult): string {
178
171
  return getFinalOutput(result.messages) || "(no output)";
179
172
  }
180
173
 
181
- export async function mapWithConcurrencyLimit<TIn, TOut>(
182
- items: TIn[],
183
- concurrency: number,
184
- fn: (item: TIn, index: number) => Promise<TOut>,
185
- ): Promise<TOut[]> {
186
- if (items.length === 0) return [];
187
- const limit = Math.max(1, Math.min(concurrency, items.length));
188
- const results: TOut[] = new Array(items.length);
189
- let nextIndex = 0;
190
- const workers = new Array(limit).fill(null).map(async () => {
191
- while (true) {
192
- const current = nextIndex++;
193
- if (current >= items.length) return;
194
- results[current] = await fn(items[current], current);
195
- }
196
- });
197
- await Promise.all(workers);
198
- return results;
199
- }
200
-
201
174
  async function writePromptToTempFile(agentName: string, prompt: string): Promise<{ dir: string; filePath: string }> {
202
175
  const dir = await mkdtemp(join(tmpdir(), "pi-subagents-"));
203
176
  const safeName = agentName.replace(/[^\w.-]+/g, "_");
@@ -261,13 +234,10 @@ export interface RunSingleOptions {
261
234
  cwd?: string;
262
235
  /** Thinking level passed to the child pi process. */
263
236
  thinkingLevel?: ThinkingLevel;
264
- /** Optional total timeout; zero (the default) disables it. Intended for tests and controlled callers. */
265
- timeoutMs?: number;
266
237
  /** Idle timeout in ms: terminate the child if its stdout produces no activity
267
238
  * for this duration. 0 (the default) disables the idle watchdog. */
268
239
  idleTimeoutMs?: number;
269
240
  signal?: AbortSignal;
270
- onUpdate?: OnUpdateCallback;
271
241
  onLive?: (e: SubagentLiveEvent) => void;
272
242
  makeDetails: (results: SingleResult[]) => SubagentDetails;
273
243
  env?: NodeJS.ProcessEnv;
@@ -281,10 +251,8 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
281
251
  task,
282
252
  cwd,
283
253
  thinkingLevel = SUBAGENT_THINKING_LEVEL,
284
- timeoutMs = SUBAGENT_TIMEOUT_MS,
285
254
  idleTimeoutMs = SUBAGENT_DEFAULT_IDLE_TIMEOUT_MS,
286
255
  signal,
287
- onUpdate,
288
256
  onLive,
289
257
  makeDetails,
290
258
  } = options;
@@ -324,13 +292,6 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
324
292
  thinking: thinkingLevel,
325
293
  };
326
294
 
327
- const emitUpdate = (): void => {
328
- onUpdate?.({
329
- content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(running...)" }],
330
- details: makeDetails([currentResult]),
331
- });
332
- };
333
-
334
295
  try {
335
296
  if (agent.systemPrompt.trim()) {
336
297
  const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt);
@@ -340,7 +301,6 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
340
301
  }
341
302
 
342
303
  let wasAborted = false;
343
- let timedOut = false;
344
304
 
345
305
  // Increment depth so nested sub-agents can be guarded against runaway recursion.
346
306
  const childDepth = currentSubagentDepth(options.env) + 1;
@@ -361,7 +321,6 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
361
321
  let closed = false;
362
322
  let termSent = false;
363
323
  let forceKillTimer: ReturnType<typeof setTimeout> | undefined;
364
- let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
365
324
  let abortHandler: (() => void) | undefined;
366
325
  let lastActivityAt = Date.now();
367
326
  let idleTimer: ReturnType<typeof setInterval> | undefined;
@@ -370,7 +329,6 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
370
329
  if (closed) return;
371
330
  closed = true;
372
331
  if (forceKillTimer) clearTimeout(forceKillTimer);
373
- if (timeoutTimer) clearTimeout(timeoutTimer);
374
332
  if (idleTimer) clearInterval(idleTimer);
375
333
  if (signal && abortHandler) signal.removeEventListener("abort", abortHandler);
376
334
  resolve(code ?? 1);
@@ -461,12 +419,10 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
461
419
  onLive({ kind: "usage", usage: { ...currentResult.usage }, model: currentResult.model });
462
420
  } catch { /* never throw from event handling */ }
463
421
  }
464
- emitUpdate();
465
422
  }
466
423
 
467
424
  if (event.type === "tool_result_end" && event.message) {
468
425
  currentResult.messages.push(event.message as Message);
469
- emitUpdate();
470
426
  }
471
427
  };
472
428
  // Send the task through the child stdin pipe instead of the process
@@ -502,7 +458,6 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
502
458
  const failed =
503
459
  code !== 0 ||
504
460
  wasAborted ||
505
- timedOut ||
506
461
  (signal?.aborted ?? false) ||
507
462
  currentResult.stopReason === "error" ||
508
463
  currentResult.stopReason === "aborted";
@@ -526,22 +481,12 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
526
481
  finish(1);
527
482
  });
528
483
 
529
- if (timeoutMs > 0) {
530
- timeoutTimer = setTimeout(() => {
531
- timedOut = true;
532
- currentResult.stopReason = "error";
533
- currentResult.errorMessage = `Subagent timed out after ${Math.ceil(timeoutMs / 1000)} seconds.`;
534
- terminate();
535
- }, timeoutMs);
536
- }
537
-
538
484
  if (idleTimeoutMs > 0) {
539
485
  const checkInterval = Math.min(10_000, Math.floor(idleTimeoutMs / 3));
540
486
  idleTimer = setInterval(() => {
541
487
  if (closed) return;
542
488
  if (Date.now() - lastActivityAt >= idleTimeoutMs) {
543
489
  if (idleTimer) clearInterval(idleTimer);
544
- timedOut = true;
545
490
  currentResult.stopReason = "error";
546
491
  currentResult.errorMessage = `Subagent idle timeout: no activity for ${Math.ceil(idleTimeoutMs / 1000)} seconds.`;
547
492
  terminate();