@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 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 inherit zero conversation context; all necessary context must be provided in the task description.
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 `PI_SUBAGENT_SYSTEM_PROMPT_MODE` so runtime hooks can distinguish append vs. replace behavior.
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 { AuthStorage, ModelRegistry, withFileMutationQueue } from '@earendil-works/pi-coding-agent';
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 modelRegistry = ModelRegistry.create(
459
- AuthStorage.create(options.agentDir ? path.join(options.agentDir, 'auth.json') : undefined),
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 statusLine = `${icon} ${progress.agent}${progress.model ? ` (${progress.model})` : ''} — ${
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}`] : undefined;
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: params.cwd ?? ctx.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.0.0",
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
  },