pi-subagents 0.11.1 → 0.11.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 +10 -0
- package/README.md +24 -4
- package/execution.ts +7 -0
- package/index.ts +16 -7
- package/package.json +1 -1
- package/parallel-utils.ts +9 -3
- package/skills.ts +15 -0
- package/subagent-runner.ts +4 -0
- package/types.ts +1 -0
- package/utils.ts +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.11.2] - 2026-03-11
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- `--no-skills` was missing from the async runner (`subagent-runner.ts`). PR #41 added skill scoping to the sync path but the async runner spawns pi through its own code path, so background subagents with explicit skills still got the full `<available_skills>` catalog injected.
|
|
9
|
+
- `defaultSessionDir` and `sessionDir` with `~` paths (e.g. `"~/.pi/agent/sessions/subagent/"`) were not expanded — `path.resolve("~/...")` treats `~` as a literal directory name. Added tilde expansion matching the existing pattern in `skills.ts`.
|
|
10
|
+
- Multiple subagent calls within a session would collide when `defaultSessionDir` was configured, since it wasn't appending a unique `runId`. Both `defaultSessionDir` and parent-session-derived paths now get `runId` appended.
|
|
11
|
+
|
|
12
|
+
### Removed
|
|
13
|
+
- Removed exported `resolveSessionRoot()` function and `SessionRootInput` interface. These were introduced by PR #46 but never called in production — the inline resolution logic diverged (always-on sessions, `runId` appended) making the function's contract misleading. Associated tests and dead code from PR #47 scaffolding also removed from `path-handling.test.ts`.
|
|
14
|
+
|
|
5
15
|
## [0.11.1] - 2026-03-08
|
|
6
16
|
|
|
7
17
|
### Changed
|
package/README.md
CHANGED
|
@@ -538,7 +538,7 @@ Notes:
|
|
|
538
538
|
| `artifacts` | boolean | true | Write debug artifacts |
|
|
539
539
|
| `includeProgress` | boolean | false | Include full progress in result |
|
|
540
540
|
| `share` | boolean | false | Upload session to GitHub Gist (see [Session Sharing](#session-sharing)) |
|
|
541
|
-
| `sessionDir` | string |
|
|
541
|
+
| `sessionDir` | string | - | Override session log directory (takes precedence over `defaultSessionDir` and parent-session-derived path) |
|
|
542
542
|
|
|
543
543
|
**ChainItem** can be either a sequential step or a parallel step:
|
|
544
544
|
|
|
@@ -604,11 +604,31 @@ Templates support three variables:
|
|
|
604
604
|
|
|
605
605
|
This aggregated output becomes `{previous}` for the next step.
|
|
606
606
|
|
|
607
|
-
##
|
|
607
|
+
## Extension Configuration
|
|
608
|
+
|
|
609
|
+
`pi-subagents` reads optional JSON config from `~/.pi/agent/extensions/subagent/config.json`.
|
|
610
|
+
|
|
611
|
+
### `defaultSessionDir`
|
|
608
612
|
|
|
613
|
+
`defaultSessionDir` sets the fallback directory used for session logs. Eg:
|
|
614
|
+
|
|
615
|
+
```json
|
|
616
|
+
{
|
|
617
|
+
"defaultSessionDir": "~/.pi/agent/sessions/subagent/"
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
Session root resolution follows this precedence:
|
|
622
|
+
1. `params.sessionDir` from the `subagent` tool call
|
|
623
|
+
2. `config.defaultSessionDir`
|
|
624
|
+
3. Derived from parent session (stored alongside parent session file)
|
|
625
|
+
|
|
626
|
+
Sessions are always enabled — every subagent run gets a session directory for tracking.
|
|
627
|
+
|
|
628
|
+
## Chain Directory
|
|
609
629
|
Each chain run creates `<tmpdir>/pi-chain-runs/{runId}/` containing:
|
|
610
630
|
- `context.md` - Scout/context-builder output
|
|
611
|
-
- `plan.md` - Planner output
|
|
631
|
+
- `plan.md` - Planner output
|
|
612
632
|
- `progress.md` - Worker/reviewer shared progress
|
|
613
633
|
- `parallel-{stepIndex}/` - Subdirectories for parallel step outputs
|
|
614
634
|
- `0-{agent}/output.md` - First parallel task output
|
|
@@ -629,7 +649,7 @@ Files per task:
|
|
|
629
649
|
|
|
630
650
|
## Session Logs
|
|
631
651
|
|
|
632
|
-
Session files (JSONL) are stored under a per-run session
|
|
652
|
+
Session files (JSONL) are stored under a per-run session directory. Directory selection follows the same precedence as session root resolution: explicit `sessionDir` > `config.defaultSessionDir` > parent-session-derived path. The session file path is shown in output.
|
|
633
653
|
|
|
634
654
|
## Session Sharing
|
|
635
655
|
|
package/execution.ts
CHANGED
|
@@ -113,6 +113,13 @@ export async function runSync(
|
|
|
113
113
|
const skillNames = options.skills ?? agent.skills ?? [];
|
|
114
114
|
const { resolved: resolvedSkills, missing: missingSkills } = resolveSkills(skillNames, runtimeCwd);
|
|
115
115
|
|
|
116
|
+
// When explicit skills are specified (via options or agent config), disable
|
|
117
|
+
// pi's own skill discovery so the spawned process doesn't inject the full
|
|
118
|
+
// <available_skills> catalog. This mirrors how extensions are scoped above.
|
|
119
|
+
if (skillNames.length > 0) {
|
|
120
|
+
args.push("--no-skills");
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
let systemPrompt = agent.systemPrompt?.trim() || "";
|
|
117
124
|
if (resolvedSkills.length > 0) {
|
|
118
125
|
const skillInjection = buildSkillInjection(resolvedSkills);
|
package/index.ts
CHANGED
|
@@ -83,6 +83,10 @@ function loadConfig(): ExtensionConfig {
|
|
|
83
83
|
return {};
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function expandTilde(p: string): string {
|
|
87
|
+
return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
|
|
88
|
+
}
|
|
89
|
+
|
|
86
90
|
/**
|
|
87
91
|
* Create a directory and verify it is actually accessible.
|
|
88
92
|
* On Windows with Azure AD/Entra ID, directories created shortly after
|
|
@@ -298,12 +302,17 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
|
|
|
298
302
|
const agents = discoverAgents(ctx.cwd, scope).agents;
|
|
299
303
|
const runId = randomUUID().slice(0, 8);
|
|
300
304
|
const shareEnabled = params.share === true;
|
|
301
|
-
// Session root: explicit param >
|
|
302
|
-
// Sessions are always enabled
|
|
305
|
+
// Session root precedence: explicit param > config default > parent session derived
|
|
306
|
+
// Sessions are always enabled - stored alongside parent session for tracking
|
|
303
307
|
// Include runId to ensure uniqueness across multiple subagent calls
|
|
304
308
|
const sessionRoot = params.sessionDir
|
|
305
|
-
? path.resolve(params.sessionDir)
|
|
306
|
-
: path.join(
|
|
309
|
+
? path.resolve(expandTilde(params.sessionDir))
|
|
310
|
+
: path.join(
|
|
311
|
+
config.defaultSessionDir
|
|
312
|
+
? path.resolve(expandTilde(config.defaultSessionDir))
|
|
313
|
+
: getSubagentSessionRoot(parentSessionFile),
|
|
314
|
+
runId,
|
|
315
|
+
);
|
|
307
316
|
try {
|
|
308
317
|
fs.mkdirSync(sessionRoot, { recursive: true });
|
|
309
318
|
} catch {}
|
|
@@ -320,7 +329,7 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
|
|
|
320
329
|
// - Chains default to TUI (clarify: true), so async requires explicit clarify: false
|
|
321
330
|
// - Single defaults to no TUI, so async is allowed unless clarify: true is passed
|
|
322
331
|
const effectiveAsync = requestedAsync && !hasTasks && (
|
|
323
|
-
hasChain
|
|
332
|
+
hasChain
|
|
324
333
|
? params.clarify === false // chains: only async if TUI explicitly disabled
|
|
325
334
|
: params.clarify !== true // single: async unless TUI explicitly enabled
|
|
326
335
|
);
|
|
@@ -539,7 +548,7 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
|
|
|
539
548
|
let tasks = params.tasks.map(t => t.task);
|
|
540
549
|
const modelOverrides: (string | undefined)[] = params.tasks.map(t => (t as { model?: string }).model);
|
|
541
550
|
// Initialize skill overrides from task-level skill params (may be overridden by TUI)
|
|
542
|
-
const skillOverrides: (string[] | false | undefined)[] = params.tasks.map(t =>
|
|
551
|
+
const skillOverrides: (string[] | false | undefined)[] = params.tasks.map(t =>
|
|
543
552
|
normalizeSkillInput((t as { skill?: string | string[] | boolean }).skill)
|
|
544
553
|
);
|
|
545
554
|
|
|
@@ -553,7 +562,7 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
|
|
|
553
562
|
}));
|
|
554
563
|
|
|
555
564
|
// Resolve behaviors with task-level skill overrides for TUI display
|
|
556
|
-
const behaviors = agentConfigs.map((c, i) =>
|
|
565
|
+
const behaviors = agentConfigs.map((c, i) =>
|
|
557
566
|
resolveStepBehavior(c, { skills: skillOverrides[i] })
|
|
558
567
|
);
|
|
559
568
|
const availableSkills = discoverAvailableSkills(ctx.cwd);
|
package/package.json
CHANGED
package/parallel-utils.ts
CHANGED
|
@@ -44,18 +44,24 @@ export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] {
|
|
|
44
44
|
return flat;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** Run async tasks with bounded concurrency, preserving result order
|
|
47
|
+
/** Run async tasks with bounded concurrency, preserving result order.
|
|
48
|
+
* `staggerMs` adds a small delay between each worker's start to avoid
|
|
49
|
+
* file-lock contention when multiple subagents read shared config. */
|
|
48
50
|
export async function mapConcurrent<T, R>(
|
|
49
51
|
items: T[],
|
|
50
52
|
limit: number,
|
|
51
53
|
fn: (item: T, i: number) => Promise<R>,
|
|
54
|
+
staggerMs = 150,
|
|
52
55
|
): Promise<R[]> {
|
|
53
56
|
// Clamp to at least 1; NaN/undefined/0/negative all become 1
|
|
54
57
|
const safeLimit = Math.max(1, Math.floor(limit) || 1);
|
|
55
58
|
const results: R[] = new Array(items.length);
|
|
56
59
|
let next = 0;
|
|
57
60
|
|
|
58
|
-
async function worker(): Promise<void> {
|
|
61
|
+
async function worker(workerIndex: number): Promise<void> {
|
|
62
|
+
if (staggerMs > 0 && workerIndex > 0) {
|
|
63
|
+
await new Promise((r) => setTimeout(r, workerIndex * staggerMs));
|
|
64
|
+
}
|
|
59
65
|
while (next < items.length) {
|
|
60
66
|
const i = next++;
|
|
61
67
|
results[i] = await fn(items[i], i);
|
|
@@ -63,7 +69,7 @@ export async function mapConcurrent<T, R>(
|
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
await Promise.all(
|
|
66
|
-
Array.from({ length: Math.min(safeLimit, items.length) }, () => worker()),
|
|
72
|
+
Array.from({ length: Math.min(safeLimit, items.length) }, (_, wi) => worker(wi)),
|
|
67
73
|
);
|
|
68
74
|
return results;
|
|
69
75
|
}
|
package/skills.ts
CHANGED
|
@@ -347,6 +347,21 @@ export function normalizeSkillInput(
|
|
|
347
347
|
if (Array.isArray(input)) {
|
|
348
348
|
return [...new Set(input.map((s) => s.trim()).filter((s) => s.length > 0))];
|
|
349
349
|
}
|
|
350
|
+
// Guard against JSON-encoded arrays arriving as strings (e.g. '["a","b"]').
|
|
351
|
+
// Models sometimes serialise the skill parameter as a JSON string instead of
|
|
352
|
+
// a native array, and naively splitting on "," would embed brackets/quotes
|
|
353
|
+
// into the skill names, causing resolution to silently fail.
|
|
354
|
+
const trimmed = input.trim();
|
|
355
|
+
if (trimmed.startsWith("[")) {
|
|
356
|
+
try {
|
|
357
|
+
const parsed = JSON.parse(trimmed);
|
|
358
|
+
if (Array.isArray(parsed)) {
|
|
359
|
+
return normalizeSkillInput(parsed);
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
// Not valid JSON – fall through to comma-split
|
|
363
|
+
}
|
|
364
|
+
}
|
|
350
365
|
return [...new Set(input.split(",").map((s) => s.trim()).filter((s) => s.length > 0))];
|
|
351
366
|
}
|
|
352
367
|
|
package/subagent-runner.ts
CHANGED
|
@@ -304,6 +304,10 @@ async function runSingleStep(
|
|
|
304
304
|
for (const extPath of toolExtensionPaths) args.push("--extension", extPath);
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
if (step.skills?.length) {
|
|
308
|
+
args.push("--no-skills");
|
|
309
|
+
}
|
|
310
|
+
|
|
307
311
|
let tmpDir: string | null = null;
|
|
308
312
|
if (step.systemPrompt) {
|
|
309
313
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
package/types.ts
CHANGED
package/utils.ts
CHANGED
|
@@ -332,20 +332,24 @@ export async function mapConcurrent<T, R>(
|
|
|
332
332
|
items: T[],
|
|
333
333
|
limit: number,
|
|
334
334
|
fn: (item: T, i: number) => Promise<R>,
|
|
335
|
+
staggerMs = 150,
|
|
335
336
|
): Promise<R[]> {
|
|
336
337
|
// Clamp to at least 1; NaN/undefined/0/negative all become 1
|
|
337
338
|
const safeLimit = Math.max(1, Math.floor(limit) || 1);
|
|
338
339
|
const results: R[] = new Array(items.length);
|
|
339
340
|
let next = 0;
|
|
340
341
|
|
|
341
|
-
async function worker(): Promise<void> {
|
|
342
|
+
async function worker(workerIndex: number): Promise<void> {
|
|
343
|
+
if (staggerMs > 0 && workerIndex > 0) {
|
|
344
|
+
await new Promise((r) => setTimeout(r, workerIndex * staggerMs));
|
|
345
|
+
}
|
|
342
346
|
while (next < items.length) {
|
|
343
347
|
const i = next++;
|
|
344
348
|
results[i] = await fn(items[i], i);
|
|
345
349
|
}
|
|
346
350
|
}
|
|
347
351
|
|
|
348
|
-
const workers = Array.from({ length: Math.min(safeLimit, items.length) }, () => worker());
|
|
352
|
+
const workers = Array.from({ length: Math.min(safeLimit, items.length) }, (_, wi) => worker(wi));
|
|
349
353
|
await Promise.all(workers);
|
|
350
354
|
return results;
|
|
351
355
|
}
|