@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/README.md +45 -12
- package/package.json +1 -1
- package/src/config.test.ts +19 -1
- package/src/config.ts +13 -0
- package/src/history.ts +3 -0
- package/src/index.ts +34 -13
- package/src/inheritance.test.ts +217 -0
- package/src/inheritance.ts +188 -0
- package/src/render-async.ts +21 -2
- package/src/render.test.ts +63 -0
- package/src/render.ts +25 -3
- package/src/roles.ts +9 -2
- package/src/run.test.ts +51 -1
- package/src/run.ts +28 -6
- package/src/spawn.test.ts +34 -10
- package/src/spawn.ts +44 -10
- package/src/types.ts +17 -0
- package/src/utils.test.ts +13 -0
- package/src/utils.ts +6 -0
- package/src/view.ts +15 -3
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
|
|
198
|
-
"
|
|
199
|
-
|
|
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 —
|
|
222
|
-
* json mode (@file arguments wrapped by pi's
|
|
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(
|
|
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 (
|
|
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),
|
|
19
|
-
*
|
|
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
|
|
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);
|