@johnnywu/pi-subagents 2.0.0 → 2.1.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/CHANGELOG.md +14 -0
- package/README.md +20 -3
- package/extensions/subagent-executor.ts +26 -5
- package/extensions/subagent-render.ts +6 -1
- package/extensions/subagent-tool.ts +93 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [2.1.1](https://github.com/jwu/pi-subagents/compare/v2.1.0...v2.1.1) (2026-07-21)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* migrate model registry to Pi 0.80 runtime ([00026d6](https://github.com/jwu/pi-subagents/commit/00026d6e6aaea112896f358c0f86d20f48e73318))
|
|
7
|
+
|
|
8
|
+
# [2.1.0](https://github.com/jwu/pi-subagents/compare/v2.0.0...v2.1.0) (2026-06-17)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* support forked subagent sessions ([50f27f5](https://github.com/jwu/pi-subagents/commit/50f27f5ea45f559f0125944f4f42c8c955cddd49))
|
|
14
|
+
|
|
1
15
|
# [2.0.0](https://github.com/jwu/pi-subagents/compare/v1.5.0...v2.0.0) (2026-06-16)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Sub-agents extension for [pi](https://github.com/badlogic/pi-mono) coding agent.
|
|
4
4
|
|
|
5
|
-
Delegates tasks to isolated pi child processes — each running with its own model, system prompt, and tool set. Sub-agents
|
|
5
|
+
Delegates tasks to isolated pi child processes — each running with its own model, system prompt, and tool set. Sub-agents start with a fresh session by default; pass `session: "fork"` to branch from the current parent pi session.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -48,6 +48,23 @@ Or instruct pi to delegate:
|
|
|
48
48
|
Run the code-reviewer agent on the last three commits
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
By default, sub-agents do not inherit parent conversation history. For tasks that need the current conversation, request a forked session:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
subagent({ agent: "code-reviewer", task: "Review the approach we just discussed", session: "fork" })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`session` is optional and accepts:
|
|
58
|
+
|
|
59
|
+
| Value | Behavior |
|
|
60
|
+
|-------|----------|
|
|
61
|
+
| `none` | Default. Start a new sub-agent session in the subagents session directory. |
|
|
62
|
+
| `fork` | Fork the current parent session at its active leaf and run the sub-agent with that branched session. |
|
|
63
|
+
|
|
64
|
+
If `fork` is requested but unavailable, pi-subagents falls back to `none` and shows a warning in the tool details/rendering. Fallback happens when the parent session is not persisted, has no current leaf, the forked session file is not materialized, or the call uses a `cwd` different from the parent session cwd.
|
|
65
|
+
|
|
66
|
+
The tool prompt also guides the model to choose `session: "fork"` when a delegated task depends on the current conversation, prior discussion, or parent session history, and to keep `session: "none"` for self-contained tasks.
|
|
67
|
+
|
|
51
68
|
### Available subagents in the prompt
|
|
52
69
|
|
|
53
70
|
The extension exposes discovered sub-agents to the model when the active tool set includes `subagent`:
|
|
@@ -159,7 +176,7 @@ Sub-agents can spawn their own sub-agents (if the `subagent` tool is in their wh
|
|
|
159
176
|
|
|
160
177
|
The available-subagents prompt entries respect the same filtering: parent sessions use the currently visible agents, and child sessions only list agents allowed by their parent.
|
|
161
178
|
|
|
162
|
-
These are passed via environment variables (`PI_SUBAGENT_DEPTH`, `PI_SUBAGENT_MAX_DEPTH`, `PI_SUBAGENT_ALLOWED`). Child processes also receive `PI_SUBAGENT_NAME` and `
|
|
179
|
+
These are passed via environment variables (`PI_SUBAGENT_DEPTH`, `PI_SUBAGENT_MAX_DEPTH`, `PI_SUBAGENT_ALLOWED`). Child processes also receive `PI_SUBAGENT_NAME`, `PI_SUBAGENT_SYSTEM_PROMPT_MODE`, and `PI_SUBAGENT_SESSION` so runtime hooks can distinguish prompt mode and effective session mode.
|
|
163
180
|
|
|
164
181
|
## Session storage
|
|
165
182
|
|
|
@@ -171,7 +188,7 @@ Sub-agent sessions are saved as `.jsonl` files for post-hoc debugging:
|
|
|
171
188
|
└── ...
|
|
172
189
|
```
|
|
173
190
|
|
|
174
|
-
Each file contains one JSON object per line — session headers, messages, tool calls, and usage data. Parent pi sessions live in the same project directory (no `subagents/` subdirectory).
|
|
191
|
+
Each file contains one JSON object per line — session headers, messages, tool calls, and usage data. Parent pi sessions live in the same project directory (no `subagents/` subdirectory). When `session: "fork"` succeeds, the branched child session is still written under `subagents/` and points back to the parent session in its header.
|
|
175
192
|
|
|
176
193
|
## Development
|
|
177
194
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ModelRegistry,
|
|
3
|
+
ModelRuntime,
|
|
4
|
+
withFileMutationQueue,
|
|
5
|
+
} from '@earendil-works/pi-coding-agent';
|
|
2
6
|
import { spawn } from 'node:child_process';
|
|
3
7
|
import * as fs from 'node:fs/promises';
|
|
4
8
|
import * as os from 'node:os';
|
|
@@ -25,6 +29,15 @@ export interface AgentToolLog {
|
|
|
25
29
|
nested?: AgentProgress;
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
export type SubagentSessionMode = 'none' | 'fork';
|
|
33
|
+
|
|
34
|
+
export interface SubagentSessionInfo {
|
|
35
|
+
requested: SubagentSessionMode;
|
|
36
|
+
effective: SubagentSessionMode;
|
|
37
|
+
warning?: string;
|
|
38
|
+
file?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
export interface AgentProgress {
|
|
29
42
|
agent: string;
|
|
30
43
|
status: 'running' | 'done' | 'error';
|
|
@@ -34,6 +47,7 @@ export interface AgentProgress {
|
|
|
34
47
|
startedAt: number;
|
|
35
48
|
elapsedMs: number;
|
|
36
49
|
model?: string;
|
|
50
|
+
session?: SubagentSessionInfo;
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
export interface AgentResult extends AgentProgress {
|
|
@@ -82,6 +96,7 @@ export interface RunSubagentOptions {
|
|
|
82
96
|
tempRoot?: string;
|
|
83
97
|
outputArchiveDir?: string;
|
|
84
98
|
agentDir?: string;
|
|
99
|
+
session?: SubagentSessionInfo;
|
|
85
100
|
resolvePi?: () => Promise<PiResolution> | PiResolution;
|
|
86
101
|
runner?: ProcessRunner;
|
|
87
102
|
fs?: ExecutorFs;
|
|
@@ -424,6 +439,7 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
424
439
|
startedAt,
|
|
425
440
|
elapsedMs: now() - startedAt,
|
|
426
441
|
model,
|
|
442
|
+
session: options.session ?? { requested: 'none', effective: 'none' },
|
|
427
443
|
});
|
|
428
444
|
|
|
429
445
|
const emit = (status: AgentProgress['status'] = 'running') =>
|
|
@@ -455,10 +471,11 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
455
471
|
}
|
|
456
472
|
|
|
457
473
|
const pi = await resolvePi();
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
options.agentDir ? path.join(options.agentDir, 'models.json') : undefined,
|
|
461
|
-
);
|
|
474
|
+
const runtime = await ModelRuntime.create({
|
|
475
|
+
authPath: options.agentDir ? path.join(options.agentDir, 'auth.json') : undefined,
|
|
476
|
+
modelsPath: options.agentDir ? path.join(options.agentDir, 'models.json') : undefined,
|
|
477
|
+
});
|
|
478
|
+
const modelRegistry = new ModelRegistry(runtime);
|
|
462
479
|
const args = [pi.entryPoint, '--mode', 'json', '-p', '--no-skills', '--no-prompt-templates'];
|
|
463
480
|
|
|
464
481
|
if (options.agent.systemPromptMode === 'replace-all') args.push('--no-context-files');
|
|
@@ -470,6 +487,9 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
470
487
|
promptFilePath,
|
|
471
488
|
);
|
|
472
489
|
args.push('--session-dir', subagentSessionDir(options.cwd, options.agentDir));
|
|
490
|
+
if (options.session?.effective === 'fork' && options.session.file) {
|
|
491
|
+
args.push('--session', options.session.file);
|
|
492
|
+
}
|
|
473
493
|
args.push(buildTaskArgument(options.task, taskFilePath));
|
|
474
494
|
|
|
475
495
|
const env: NodeJS.ProcessEnv = {
|
|
@@ -478,6 +498,7 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
478
498
|
PI_SUBAGENT_MAX_DEPTH: String(options.agent.maxDepth),
|
|
479
499
|
PI_SUBAGENT_NAME: options.agent.name,
|
|
480
500
|
PI_SUBAGENT_SYSTEM_PROMPT_MODE: options.agent.systemPromptMode,
|
|
501
|
+
PI_SUBAGENT_SESSION: options.session?.effective ?? 'none',
|
|
481
502
|
};
|
|
482
503
|
const visibleAgents = availableSubagentsForAgent(options.agent, options.availableAgents);
|
|
483
504
|
if (visibleAgents.length > 0) {
|
|
@@ -4,6 +4,7 @@ import { numberArg, preview, shortenPath, stringArg } from './tool-args.ts';
|
|
|
4
4
|
export interface SubagentCallArgs {
|
|
5
5
|
agent?: string;
|
|
6
6
|
task?: string;
|
|
7
|
+
session?: 'none' | 'fork';
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export interface RenderTextOptions {
|
|
@@ -195,7 +196,8 @@ export function formatSubagentResultLines(
|
|
|
195
196
|
options: RenderTextOptions,
|
|
196
197
|
): SubagentResultLine[] {
|
|
197
198
|
const icon = progress.status === 'error' ? '✗' : progress.status === 'done' ? '✓' : '▸';
|
|
198
|
-
const
|
|
199
|
+
const sessionBadge = progress.session?.effective === 'fork' ? ' [fork]' : '';
|
|
200
|
+
const statusLine = `${icon} ${progress.agent}${sessionBadge}${progress.model ? ` (${progress.model})` : ''} — ${
|
|
199
201
|
progress.tools.length
|
|
200
202
|
} tools · ${elapsedSeconds(progress.elapsedMs)}s`;
|
|
201
203
|
const toolLines = formatToolLineItems(progress, options);
|
|
@@ -203,6 +205,9 @@ export function formatSubagentResultLines(
|
|
|
203
205
|
const lines: SubagentResultLine[] = [
|
|
204
206
|
{ text: '', kind: 'blank', singleLine: false },
|
|
205
207
|
{ text: statusLine, kind: 'status', singleLine: false },
|
|
208
|
+
...(progress.session?.warning
|
|
209
|
+
? [{ text: `session: ${progress.session.warning}`, kind: 'hint' as const, singleLine: true }]
|
|
210
|
+
: []),
|
|
206
211
|
...toolLines,
|
|
207
212
|
];
|
|
208
213
|
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getMarkdownTheme,
|
|
3
3
|
keyHint,
|
|
4
|
+
SessionManager,
|
|
4
5
|
type AgentToolUpdateCallback,
|
|
5
6
|
type ExtensionAPI,
|
|
6
7
|
type ExtensionContext,
|
|
7
8
|
type ThemeColor,
|
|
8
9
|
} from '@earendil-works/pi-coding-agent';
|
|
9
10
|
import { Container, Markdown, Spacer, Text } from '@earendil-works/pi-tui';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
10
13
|
import type { AgentConfig } from './agent-loader.ts';
|
|
11
14
|
import {
|
|
12
15
|
availableSubagentsForAgent,
|
|
13
16
|
type AgentProgress,
|
|
14
17
|
type AgentResult,
|
|
15
18
|
runSubagent,
|
|
19
|
+
subagentSessionDir,
|
|
20
|
+
type SubagentSessionInfo,
|
|
21
|
+
type SubagentSessionMode,
|
|
16
22
|
} from './subagent-executor.ts';
|
|
17
23
|
import {
|
|
18
24
|
contextUsageSeverity,
|
|
@@ -30,6 +36,12 @@ const SubagentParams = {
|
|
|
30
36
|
agent: { type: 'string', description: 'Name of the agent to invoke' },
|
|
31
37
|
task: { type: 'string', description: 'Task to delegate to the agent' },
|
|
32
38
|
cwd: { type: 'string', description: 'Working directory for the agent process' },
|
|
39
|
+
session: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
enum: ['none', 'fork'],
|
|
42
|
+
description:
|
|
43
|
+
'Parent session handling: none starts a fresh subagent session, fork branches from the current session',
|
|
44
|
+
},
|
|
33
45
|
},
|
|
34
46
|
required: ['agent', 'task'],
|
|
35
47
|
additionalProperties: false,
|
|
@@ -39,6 +51,7 @@ type SubagentParamsType = {
|
|
|
39
51
|
agent: string;
|
|
40
52
|
task: string;
|
|
41
53
|
cwd?: string;
|
|
54
|
+
session?: SubagentSessionMode;
|
|
42
55
|
};
|
|
43
56
|
|
|
44
57
|
type RegisterablePi = Pick<ExtensionAPI, 'registerTool'>;
|
|
@@ -46,6 +59,7 @@ export interface RegisterSubagentToolOptions {
|
|
|
46
59
|
agents: AgentConfig[];
|
|
47
60
|
run?: typeof runSubagent;
|
|
48
61
|
env?: RecursionEnv;
|
|
62
|
+
agentDir?: string;
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
function availableAgentsText(agents: AgentConfig[]): string {
|
|
@@ -72,6 +86,76 @@ function toProgressResult(progress: AgentProgress) {
|
|
|
72
86
|
};
|
|
73
87
|
}
|
|
74
88
|
|
|
89
|
+
function freshSessionInfo(requested: SubagentSessionMode, warning?: string): SubagentSessionInfo {
|
|
90
|
+
return {
|
|
91
|
+
requested,
|
|
92
|
+
effective: 'none',
|
|
93
|
+
...(warning ? { warning } : {}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveSubagentSession(
|
|
98
|
+
requested: SubagentSessionMode | undefined,
|
|
99
|
+
childCwd: string,
|
|
100
|
+
ctx: ExtensionContext,
|
|
101
|
+
agentDir?: string,
|
|
102
|
+
): SubagentSessionInfo {
|
|
103
|
+
const mode = requested ?? 'none';
|
|
104
|
+
if (mode === 'none') return freshSessionInfo(mode);
|
|
105
|
+
|
|
106
|
+
if (path.resolve(childCwd) !== path.resolve(ctx.cwd)) {
|
|
107
|
+
return freshSessionInfo(
|
|
108
|
+
mode,
|
|
109
|
+
`Requested fork session but cwd differs from the parent session; running with a fresh subagent session instead.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const parentSessionFile = ctx.sessionManager?.getSessionFile();
|
|
114
|
+
if (!parentSessionFile) {
|
|
115
|
+
return freshSessionInfo(
|
|
116
|
+
mode,
|
|
117
|
+
'Requested fork session but the parent session is not persisted; running with a fresh subagent session instead.',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (!existsSync(parentSessionFile)) {
|
|
121
|
+
return freshSessionInfo(
|
|
122
|
+
mode,
|
|
123
|
+
'Requested fork session but the parent session file was not materialized; running with a fresh subagent session instead.',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const parentLeafId = ctx.sessionManager?.getLeafId();
|
|
128
|
+
if (!parentLeafId) {
|
|
129
|
+
return freshSessionInfo(
|
|
130
|
+
mode,
|
|
131
|
+
'Requested fork session but the parent session has no current leaf; running with a fresh subagent session instead.',
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const forkSource = SessionManager.open(
|
|
137
|
+
parentSessionFile,
|
|
138
|
+
subagentSessionDir(childCwd, agentDir),
|
|
139
|
+
childCwd,
|
|
140
|
+
);
|
|
141
|
+
const sessionFile = forkSource.createBranchedSession(parentLeafId);
|
|
142
|
+
if (!sessionFile || !existsSync(sessionFile)) {
|
|
143
|
+
return freshSessionInfo(
|
|
144
|
+
mode,
|
|
145
|
+
'Requested fork session but the forked session file was not materialized; running with a fresh subagent session instead.',
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { requested: mode, effective: 'fork', file: sessionFile };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
152
|
+
return freshSessionInfo(
|
|
153
|
+
mode,
|
|
154
|
+
`Requested fork session but creating the fork failed (${message}); running with a fresh subagent session instead.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
75
159
|
type CollapsedTheme = {
|
|
76
160
|
fg: (name: ThemeColor, text: string) => string;
|
|
77
161
|
bold: (text: string) => string;
|
|
@@ -224,14 +308,16 @@ export function registerSubagentTool(
|
|
|
224
308
|
|
|
225
309
|
const availableSubagents = agents.map((agent) => agent.name);
|
|
226
310
|
const agentNames = [...availableSubagents].sort().join(', ');
|
|
311
|
+
const sessionGuideline =
|
|
312
|
+
'Use session: "fork" when the delegated task depends on the current conversation, prior discussion, or parent session history. Use the default session: "none" for self-contained tasks.';
|
|
227
313
|
const promptGuidelines =
|
|
228
|
-
agentNames.length > 0 ? [`Available subagents: ${agentNames}
|
|
314
|
+
agentNames.length > 0 ? [`Available subagents: ${agentNames}`, sessionGuideline] : undefined;
|
|
229
315
|
|
|
230
316
|
pi.registerTool({
|
|
231
317
|
name: 'subagent',
|
|
232
318
|
label: 'Subagent',
|
|
233
319
|
description: 'Delegate a task to a named sub-agent running in an isolated pi process.',
|
|
234
|
-
promptSnippet: 'Delegate isolated tasks with subagent({ agent, task, cwd? }).',
|
|
320
|
+
promptSnippet: 'Delegate isolated tasks with subagent({ agent, task, cwd?, session? }).',
|
|
235
321
|
promptGuidelines,
|
|
236
322
|
parameters: SubagentParams,
|
|
237
323
|
|
|
@@ -249,13 +335,17 @@ export function registerSubagentTool(
|
|
|
249
335
|
);
|
|
250
336
|
}
|
|
251
337
|
|
|
338
|
+
const childCwd = params.cwd ?? ctx.cwd;
|
|
339
|
+
const session = resolveSubagentSession(params.session, childCwd, ctx, options.agentDir);
|
|
252
340
|
const result = await runner({
|
|
253
341
|
agent,
|
|
254
342
|
task: params.task,
|
|
255
|
-
cwd:
|
|
343
|
+
cwd: childCwd,
|
|
256
344
|
signal,
|
|
257
345
|
depth: Number(env.PI_SUBAGENT_DEPTH ?? '0') + 1,
|
|
258
346
|
availableAgents: availableSubagentsForAgent(agent, availableSubagents),
|
|
347
|
+
agentDir: options.agentDir,
|
|
348
|
+
session,
|
|
259
349
|
onProgress: (progress) => onUpdate?.(toProgressResult(progress)),
|
|
260
350
|
});
|
|
261
351
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnnywu/pi-subagents",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Sub-agents extension for pi coding agent.",
|
|
5
5
|
"homepage": "https://github.com/jwu/pi-subagents#readme",
|
|
6
6
|
"repository": {
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
},
|
|
83
83
|
"peerDependencies": {
|
|
84
84
|
"@earendil-works/pi-ai": "*",
|
|
85
|
-
"@earendil-works/pi-coding-agent": "
|
|
85
|
+
"@earendil-works/pi-coding-agent": ">=0.80.10",
|
|
86
86
|
"@earendil-works/pi-tui": "*",
|
|
87
87
|
"typebox": "*"
|
|
88
88
|
},
|