@johnnywu/pi-subagents 1.5.0 → 2.1.0

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,21 @@
1
+ # [2.1.0](https://github.com/jwu/pi-subagents/compare/v2.0.0...v2.1.0) (2026-06-17)
2
+
3
+
4
+ ### Features
5
+
6
+ * support forked subagent sessions ([50f27f5](https://github.com/jwu/pi-subagents/commit/50f27f5ea45f559f0125944f4f42c8c955cddd49))
7
+
8
+ # [2.0.0](https://github.com/jwu/pi-subagents/compare/v1.5.0...v2.0.0) (2026-06-16)
9
+
10
+
11
+ * feat!: clarify systemPrompt replace semantics ([5cdb603](https://github.com/jwu/pi-subagents/commit/5cdb603e814b966f43fa0537dae5ac90799233b7))
12
+
13
+
14
+ ### BREAKING CHANGES
15
+
16
+ * systemPrompt: replace now preserves pi context files.
17
+ Use systemPrompt: replace-all for the previous isolated behavior.
18
+
1
19
  # [1.5.0](https://github.com/jwu/pi-subagents/compare/v1.4.0...v1.5.0) (2026-06-03)
2
20
 
3
21
 
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`:
@@ -63,10 +80,24 @@ Available subagents:
63
80
  - test-writer
64
81
  ```
65
82
 
66
- The prompt file passed to the child process contains only the agent prompt plus skills. Runtime prompt assembly then depends on `systemPrompt` mode:
83
+ The prompt file passed to the child process contains only the agent prompt plus skills. Runtime prompt assembly then depends on `systemPrompt` mode.
84
+
85
+ ### What goes into the sub-agent system prompt
86
+
87
+ | Component | `append` | `replace` | `replace-all` |
88
+ |------|----------|-----------|---------------|
89
+ | pi default system prompt | ✅ kept | ❌ replaced | ❌ replaced |
90
+ | Project context files (AGENTS.md/CLAUDE.md, etc.) | ✅ included | ✅ included | ❌ skipped |
91
+ | Agent body (.md file body) | ✅ appended | ✅ becomes the prompt | ✅ becomes the prompt |
92
+ | Skills XML block | ✅ appended | ✅ appended | ✅ appended |
93
+ | Available tools / Guidelines block | from default prompt | re-injected by `before_agent_start` hook | re-injected by `before_agent_start` hook |
94
+ | Available subagents block | injected at agent start | injected at agent start | injected at agent start |
95
+
96
+ `append` keeps pi's full default prompt (with project context) and adds the agent body at the end.
97
+ `replace` swaps out pi's default prompt for the agent body while keeping pi context files.
98
+ `replace-all` is the fully isolated mode: it swaps out pi's default prompt and skips pi context files, then the runtime hook re-injects tool and guideline blocks to preserve tool visibility.
67
99
 
68
- - `append`: the child process uses pi's default prompt and project context files, then appends the agent prompt.
69
- - `replace`: the child process replaces pi's default prompt with the agent prompt and skips project context files; pi-subagents reinjects the active `Available tools` and `Guidelines` blocks at agent-start time so custom replace prompts still expose the selected tool affordances.
100
+ Breaking change: the old `replace` behavior is now `replace-all`. Existing agents that need to keep skipping AGENTS.md/CLAUDE.md should change `systemPrompt: replace` to `systemPrompt: replace-all`.
70
101
 
71
102
  The `Available subagents` block is injected by the child process at agent-start time, after `PI_SUBAGENT_ALLOWED` and recursion depth filtering are applied.
72
103
 
@@ -81,7 +112,7 @@ debug: true
81
112
  ---
82
113
  ```
83
114
 
84
- The child process writes `debug-system-prompt.md` in the project cwd. The file contains the prompt visible during `before_agent_start`, including `systemPrompt` append/replace behavior, tools/guidelines, skills, project context files in append mode, and pi-subagents' runtime `Available subagents` block when applicable.
115
+ The child process writes `debug-system-prompt.md` in the project cwd. The file contains the prompt visible during `before_agent_start`, including `systemPrompt` append/replace/replace-all behavior, tools/guidelines, skills, project context files in append and replace modes, and pi-subagents' runtime `Available subagents` block when applicable.
85
116
 
86
117
  ## Agent configuration
87
118
 
@@ -94,7 +125,7 @@ Agents are Markdown files with YAML frontmatter.
94
125
  | `tools` | no | _none_ | Comma-separated tool whitelist (`read, write, bash, grep`, etc.) |
95
126
  | `model` | no | parent's model | Provider/model-id (`anthropic/claude-sonnet-4-6`) |
96
127
  | `thinking` | no | `off` | Reasoning level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` |
97
- | `systemPrompt` | no | `append` | How the body is applied: `append` (append to pi default system prompt and project context) or `replace` (replace default prompt and skip project context) |
128
+ | `systemPrompt` | no | `append` | How the body is applied: `append` (append to pi default system prompt and project context), `replace` (replace pi default prompt while keeping project context), or `replace-all` (replace pi default prompt and skip project context) |
98
129
  | `skills` | no | _none_ | Comma-separated skill names or simple wildcard patterns (`*`, `obsidian-*`) to load (resolved from project `.agents/skills/`, `.pi/skills/`, global `~/.pi/agent/skills/`, or npm packages) |
99
130
  | `allowedAgents` | no | _all_ | Comma-separated list of sub-agents this agent may spawn |
100
131
  | `maxDepth` | no | `10` | Maximum recursion depth (`0` = no sub-agents, `1` = one level, etc.) |
@@ -145,7 +176,7 @@ Sub-agents can spawn their own sub-agents (if the `subagent` tool is in their wh
145
176
 
146
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.
147
178
 
148
- 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.
149
180
 
150
181
  ## Session storage
151
182
 
@@ -157,7 +188,7 @@ Sub-agent sessions are saved as `.jsonl` files for post-hoc debugging:
157
188
  └── ...
158
189
  ```
159
190
 
160
- 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.
161
192
 
162
193
  ## Development
163
194
 
@@ -3,7 +3,7 @@ import * as os from 'node:os';
3
3
  import * as path from 'node:path';
4
4
 
5
5
  export type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
6
- export type SystemPromptMode = 'replace' | 'append';
6
+ export type SystemPromptMode = 'replace' | 'replace-all' | 'append';
7
7
  export type AgentSource = 'global' | 'project';
8
8
 
9
9
  export interface AgentConfig {
@@ -109,7 +109,7 @@ function parseAgentFile(content: string, filePath: string, source: AgentSource):
109
109
  }
110
110
 
111
111
  const systemPromptMode = (data.systemPrompt ?? 'append') as SystemPromptMode;
112
- if (!['replace', 'append'].includes(systemPromptMode)) {
112
+ if (!['replace', 'replace-all', 'append'].includes(systemPromptMode)) {
113
113
  throw new Error(`invalid systemPrompt: ${data.systemPrompt}`);
114
114
  }
115
115
 
@@ -1,4 +1,4 @@
1
- export type SystemPromptModeEnv = 'replace' | 'append';
1
+ export type SystemPromptModeEnv = 'replace' | 'replace-all' | 'append';
2
2
 
3
3
  export type RecursionEnv = Partial<
4
4
  Record<
@@ -41,10 +41,11 @@ export function isSubagentProcess(env: RecursionEnv): boolean {
41
41
 
42
42
  export function subagentSystemPromptMode(env: RecursionEnv): SystemPromptModeEnv | undefined {
43
43
  const mode = env?.PI_SUBAGENT_SYSTEM_PROMPT_MODE;
44
- if (mode === 'replace' || mode === 'append') return mode;
44
+ if (mode === 'replace' || mode === 'replace-all' || mode === 'append') return mode;
45
45
  return undefined;
46
46
  }
47
47
 
48
48
  export function isSubagentReplaceSystemPrompt(env: RecursionEnv): boolean {
49
- return isSubagentProcess(env) && subagentSystemPromptMode(env) === 'replace';
49
+ const mode = subagentSystemPromptMode(env);
50
+ return isSubagentProcess(env) && (mode === 'replace' || mode === 'replace-all');
50
51
  }
@@ -25,6 +25,15 @@ export interface AgentToolLog {
25
25
  nested?: AgentProgress;
26
26
  }
27
27
 
28
+ export type SubagentSessionMode = 'none' | 'fork';
29
+
30
+ export interface SubagentSessionInfo {
31
+ requested: SubagentSessionMode;
32
+ effective: SubagentSessionMode;
33
+ warning?: string;
34
+ file?: string;
35
+ }
36
+
28
37
  export interface AgentProgress {
29
38
  agent: string;
30
39
  status: 'running' | 'done' | 'error';
@@ -34,6 +43,7 @@ export interface AgentProgress {
34
43
  startedAt: number;
35
44
  elapsedMs: number;
36
45
  model?: string;
46
+ session?: SubagentSessionInfo;
37
47
  }
38
48
 
39
49
  export interface AgentResult extends AgentProgress {
@@ -82,6 +92,7 @@ export interface RunSubagentOptions {
82
92
  tempRoot?: string;
83
93
  outputArchiveDir?: string;
84
94
  agentDir?: string;
95
+ session?: SubagentSessionInfo;
85
96
  resolvePi?: () => Promise<PiResolution> | PiResolution;
86
97
  runner?: ProcessRunner;
87
98
  fs?: ExecutorFs;
@@ -424,6 +435,7 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
424
435
  startedAt,
425
436
  elapsedMs: now() - startedAt,
426
437
  model,
438
+ session: options.session ?? { requested: 'none', effective: 'none' },
427
439
  });
428
440
 
429
441
  const emit = (status: AgentProgress['status'] = 'running') =>
@@ -461,7 +473,7 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
461
473
  );
462
474
  const args = [pi.entryPoint, '--mode', 'json', '-p', '--no-skills', '--no-prompt-templates'];
463
475
 
464
- if (options.agent.systemPromptMode === 'replace') args.push('--no-context-files');
476
+ if (options.agent.systemPromptMode === 'replace-all') args.push('--no-context-files');
465
477
  if (options.agent.model) args.push('--model', options.agent.model);
466
478
  args.push('--thinking', options.agent.thinking);
467
479
  if (options.agent.tools.length > 0) args.push('--tools', options.agent.tools.join(','));
@@ -470,6 +482,9 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
470
482
  promptFilePath,
471
483
  );
472
484
  args.push('--session-dir', subagentSessionDir(options.cwd, options.agentDir));
485
+ if (options.session?.effective === 'fork' && options.session.file) {
486
+ args.push('--session', options.session.file);
487
+ }
473
488
  args.push(buildTaskArgument(options.task, taskFilePath));
474
489
 
475
490
  const env: NodeJS.ProcessEnv = {
@@ -478,6 +493,7 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
478
493
  PI_SUBAGENT_MAX_DEPTH: String(options.agent.maxDepth),
479
494
  PI_SUBAGENT_NAME: options.agent.name,
480
495
  PI_SUBAGENT_SYSTEM_PROMPT_MODE: options.agent.systemPromptMode,
496
+ PI_SUBAGENT_SESSION: options.session?.effective ?? 'none',
481
497
  };
482
498
  const visibleAgents = availableSubagentsForAgent(options.agent, options.availableAgents);
483
499
  if (visibleAgents.length > 0) {
@@ -65,7 +65,7 @@ export function appendAvailableToolsAndGuidelinesBlock(
65
65
  options: ToolGuidelinePromptOptions,
66
66
  ): string {
67
67
  const block = formatAvailableToolsAndGuidelinesBlock(options);
68
- if (!block || systemPrompt.includes('Available tools:') || systemPrompt.includes('Guidelines:')) {
68
+ if (!block || systemPrompt.includes(block)) {
69
69
  return systemPrompt;
70
70
  }
71
71
 
@@ -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": "1.5.0",
3
+ "version": "2.1.0",
4
4
  "description": "Sub-agents extension for pi coding agent.",
5
5
  "homepage": "https://github.com/jwu/pi-subagents#readme",
6
6
  "repository": {