@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 +5 -5
- package/package.json +1 -1
- package/src/index.ts +1 -3
- package/src/spawn.ts +1 -56
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.
|
|
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)
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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.
|
|
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,
|
|
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
|
|
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();
|