@d3ara1n/pi-subagent 3.0.0 → 3.2.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/src/spawn.ts CHANGED
@@ -149,6 +149,7 @@ export function buildChildArgs(
149
149
  tools?: string[];
150
150
  excludeTools?: string[];
151
151
  systemPrompt?: string;
152
+ inheritConversation?: boolean;
152
153
  },
153
154
  tmpDir: string,
154
155
  ): string[] {
@@ -187,6 +188,17 @@ export function buildChildArgs(
187
188
  // does; this shapes HOW any subagent behaves when the task exceeds its
188
189
  // actual capabilities: report the gap and stop instead of improvising
189
190
  // workarounds until timeout.
191
+ const conversationPolicy = options.inheritConversation
192
+ ? [
193
+ "- You have an <inherited_conversation> block from the parent session.",
194
+ " It is text-only background and may be compacted or truncated. If needed",
195
+ " material is absent, report it as Missing — do not guess it.",
196
+ ]
197
+ : [
198
+ "- The task may reference material as 'discussed above' or 'the requirements'",
199
+ " — you have NO prior conversation; only this prompt exists. If referenced",
200
+ " material is not in this prompt, report it as Missing — do not guess it.",
201
+ ];
190
202
  args.push(
191
203
  "--append-system-prompt",
192
204
  [
@@ -194,11 +206,9 @@ export function buildChildArgs(
194
206
  "Before attempting the task, check it against your actual capabilities in this",
195
207
  "session — the tool list here is definitive.",
196
208
  "- If the task needs a capability you do not have (web access, bash, file",
197
- " writes, ...) or material that is not present locally or in the provided",
198
- " context/files, it is out of scope for you. Do NOT improvise workarounds.",
199
- "- The task may reference material as 'discussed above' or 'the requirements'",
200
- " — you have NO prior conversation; only this prompt exists. If referenced",
201
- " material is not in this prompt, report it as Missing — do not guess it.",
209
+ " writes, ...) or material that is absent from local files and the provided",
210
+ " prompt channels, it is out of scope for you. Do NOT improvise workarounds.",
211
+ ...conversationPolicy,
202
212
  '- "Cannot complete" means a capability or material gap — not "difficult" or',
203
213
  ' "uncertain". If it is merely hard, keep working within your tools.',
204
214
  "- When you hit a genuine gap, stop early and return:",
@@ -218,14 +228,15 @@ export function buildChildArgs(
218
228
 
219
229
  /**
220
230
  * Compose the initial RPC prompt message: reference files as <file> blocks,
221
- * then context and task as structured tags — the same shape the child saw in
222
- * json mode (@file arguments wrapped by pi's processFileArguments, followed by
223
- * the inline message body).
231
+ * optional inherited conversation, then context and task as structured tags —
232
+ * the same shape the child saw in json mode (@file arguments wrapped by pi's
233
+ * processFileArguments, followed by the inline message body).
224
234
  *
225
235
  * @internal — exported for testing.
226
236
  */
227
237
  export async function composeInitialMessage(
228
238
  files: string[] | undefined,
239
+ inheritedConversation: string | undefined,
229
240
  context: string | undefined,
230
241
  task: string,
231
242
  ): Promise<string> {
@@ -241,6 +252,17 @@ export async function composeInitialMessage(
241
252
  parts.push(`<file name="${f}">\n${content}\n</file>`);
242
253
  }
243
254
  }
255
+ if (inheritedConversation !== undefined) {
256
+ parts.push(
257
+ [
258
+ "<inherited_conversation>",
259
+ "Text-only background from the parent conversation; it may be compacted or truncated. The separate task block remains authoritative.",
260
+ "",
261
+ inheritedConversation,
262
+ "</inherited_conversation>",
263
+ ].join("\n"),
264
+ );
265
+ }
244
266
  if (context?.trim()) parts.push(`<context>\n${context}\n</context>`);
245
267
  if (task.trim()) parts.push(`<task>\n${task}\n</task>`);
246
268
  return parts.join("\n\n");
@@ -268,6 +290,10 @@ export async function spawnSubagent(
268
290
  systemPrompt?: string;
269
291
  /** Extra context delivered as a separate channel from the task. */
270
292
  context?: string;
293
+ /** Enables the inherited-conversation policy variant. */
294
+ inheritConversation?: boolean;
295
+ /** Immutable serialized parent-conversation body injected independently from context/task. */
296
+ inheritedConversation?: string;
271
297
  /** Reference files injected as <file> blocks in the initial prompt (child reads them directly). */
272
298
  contextFiles?: string[];
273
299
  subagentRoles?: string[];
@@ -332,7 +358,12 @@ export async function spawnSubagent(
332
358
  // (processFileArguments). Content still never enters the parent model's
333
359
  // context; this process reads the bytes off disk and pipes them straight
334
360
  // to the child.
335
- const initialMessage = await composeInitialMessage(options.contextFiles, options.context, task);
361
+ const initialMessage = await composeInitialMessage(
362
+ options.contextFiles,
363
+ options.inheritedConversation,
364
+ options.context,
365
+ task,
366
+ );
336
367
 
337
368
  // Spawn process
338
369
  const invocation = getPiInvocation(args);
@@ -384,7 +415,10 @@ export async function spawnSubagent(
384
415
  // Called after each assistant message_end (usage already accumulated).
385
416
  const checkBudget = () => {
386
417
  if (budgetExceeded || wasTimeout) return;
387
- if ((maxTurns > 0 && result.usage.turns >= maxTurns) || (maxCost > 0 && result.usage.cost >= maxCost)) {
418
+ if (
419
+ (maxTurns > 0 && result.usage.turns >= maxTurns) ||
420
+ (maxCost > 0 && result.usage.cost >= maxCost)
421
+ ) {
388
422
  budgetExceeded = true;
389
423
  killProc("budget");
390
424
  }
package/src/types.ts CHANGED
@@ -15,6 +15,8 @@ export interface SubagentConfig {
15
15
  /** Persist every spawned delegate run (finished/failed/aborted alike) to ~/.pi/subagent/history/{sessionId}/{toolCallId}.json for auditing. Pre-run failures that never spawned are not recorded. */
16
16
  history: SubagentHistoryConfig;
17
17
  summary: SubagentSummaryConfig;
18
+ /** Limits optional serialized parent-conversation inheritance. */
19
+ inheritance: SubagentInheritanceConfig;
18
20
  /**
19
21
  * Per-role overrides from settings.json. Keyed by role name.
20
22
  * - Override built-in roles: provide fields to merge.
@@ -32,6 +34,11 @@ export interface SubagentSummaryConfig {
32
34
  enabled: boolean;
33
35
  }
34
36
 
37
+ export interface SubagentInheritanceConfig {
38
+ /** Maximum characters in the inherited-conversation body. */
39
+ maxChars: number;
40
+ }
41
+
35
42
  export const DEFAULT_CONFIG: SubagentConfig = {
36
43
  maxConcurrency: 4,
37
44
  maxDepth: 3,
@@ -39,6 +46,7 @@ export const DEFAULT_CONFIG: SubagentConfig = {
39
46
  maxCost: 0,
40
47
  history: { enabled: true },
41
48
  summary: { role: "utility", enabled: true },
49
+ inheritance: { maxChars: 50_000 },
42
50
  agentOverrides: {},
43
51
  };
44
52
 
@@ -181,6 +189,12 @@ export interface SubagentResult {
181
189
  files?: string[];
182
190
  /** Extra context passed to delegate (params.context); used by the expanded view. */
183
191
  context?: string;
192
+ /** True when this run received a filtered parent-conversation snapshot. */
193
+ inheritConversation?: boolean;
194
+ /** Delivered inherited-conversation body size; safe metadata only, never the body itself. */
195
+ inheritedConversationChars?: number;
196
+ /** True when the inherited body was mechanically shortened to its configured limit. */
197
+ inheritedConversationTruncated?: boolean;
184
198
  }
185
199
 
186
200
  /** Snapshot of a failed first attempt that was retried on the fallback role. */
@@ -213,6 +227,9 @@ export interface BackgroundDelegateDetails {
213
227
  task: string;
214
228
  context?: string;
215
229
  files?: string[];
230
+ inheritConversation?: boolean;
231
+ inheritedConversationChars?: number;
232
+ inheritedConversationTruncated?: boolean;
216
233
  }
217
234
 
218
235
  /** One watched run inside a wait/check view. */
package/src/utils.test.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  previewArgs,
22
22
  truncateOutput,
23
23
  formatTokens,
24
+ formatInheritedConversationInput,
24
25
  formatUsageStats,
25
26
  effectiveTimeout,
26
27
  elapsedSeconds,
@@ -325,6 +326,18 @@ describe("truncateOutput", () => {
325
326
  });
326
327
  });
327
328
 
329
+ // ── inherited-conversation input metadata ──
330
+ describe("formatInheritedConversationInput", () => {
331
+ test("formats delivered chars, truncation, and empty inheritance", () => {
332
+ assert.equal(formatInheritedConversationInput(42, false), "conversation 42 chars");
333
+ assert.equal(
334
+ formatInheritedConversationInput(50_000, true),
335
+ "conversation 50000 chars · truncated",
336
+ );
337
+ assert.equal(formatInheritedConversationInput(0, false), "conversation inherited · empty");
338
+ });
339
+ });
340
+
328
341
  // ── formatTokens: boundary correctness ──
329
342
  describe("formatTokens", () => {
330
343
  test("under 1000 stays raw", () => {
package/src/utils.ts CHANGED
@@ -38,6 +38,12 @@ export function formatTokens(count: number): string {
38
38
  return `${(count / 1000000).toFixed(1)}M`;
39
39
  }
40
40
 
41
+ /** Safe TUI label for inherited-conversation input; never includes transcript text. */
42
+ export function formatInheritedConversationInput(chars: number, truncated: boolean): string {
43
+ if (chars === 0) return "conversation inherited · empty";
44
+ return `conversation ${chars} chars${truncated ? " · truncated" : ""}`;
45
+ }
46
+
41
47
  /**
42
48
  * Usage parts shared by the TUI stats line and the LLM usage footer.
43
49
  * `withCache` adds the cache-read/write and peak-context figures (TUI only —
package/src/view.ts CHANGED
@@ -15,8 +15,9 @@
15
15
  * (a "⋮ N earlier" marker appears), reaching the bottom again (or End)
16
16
  * re-pins.
17
17
  * - brief: the run's inputs and vitals — task and context verbatim (wrapped;
18
- * head+tail elided when huge), the reference file list annotated with ✓/·
19
- * for whether the child's tool calls touched each file, usage and time
18
+ * head+tail elided when huge), safe inherited-conversation size/truncation
19
+ * metadata, the reference file list annotated with ✓/· for whether the
20
+ * child's tool calls touched each file, usage and time
20
21
  * stats, the fallback trace, and a stderr tail on failures.
21
22
  *
22
23
  * Steer input is modal so keys never conflict with the editor: browse mode
@@ -51,6 +52,7 @@ import type { ActivityEntry } from "./types.ts";
51
52
  import {
52
53
  briefFilesUsed,
53
54
  formatFallback,
55
+ formatInheritedConversationInput,
54
56
  formatThinking,
55
57
  formatTimePart,
56
58
  formatToolCall,
@@ -339,7 +341,8 @@ export class SubagentViewPanel implements Component, Focusable {
339
341
  }
340
342
 
341
343
  /** Render the brief page's full content (pre-scroll): task/context verbatim,
342
- * annotated file list, stats, fallback trace, failure stderr tail. */
344
+ * inherited-conversation metadata, annotated files, stats, fallback trace,
345
+ * and failure stderr tail. */
343
346
  private renderBriefLines(run: RunHandle, width: number, fg: Fg): string[] {
344
347
  const snap = run.snapshot;
345
348
  const lines: string[] = [];
@@ -361,6 +364,15 @@ export class SubagentViewPanel implements Component, Focusable {
361
364
  body(run.context);
362
365
  }
363
366
 
367
+ if (run.inheritConversation) {
368
+ section(
369
+ formatInheritedConversationInput(
370
+ run.inheritedConversationChars ?? 0,
371
+ run.inheritedConversationTruncated === true,
372
+ ),
373
+ );
374
+ }
375
+
364
376
  if (run.files && run.files.length > 0) {
365
377
  section(`files · ${run.files.length}`);
366
378
  const used = briefFilesUsed(run.files, snap.activityLog);