pi-subagents 0.24.3 → 0.25.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 +26 -5
- package/README.md +19 -11
- package/package.json +4 -8
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +46 -10
- package/src/agents/agent-management.ts +5 -0
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +30 -6
- package/src/agents/skills.ts +25 -23
- package/src/extension/config.ts +16 -0
- package/src/extension/fanout-child.ts +170 -0
- package/src/extension/index.ts +13 -25
- package/src/intercom/intercom-bridge.ts +2 -1
- package/src/intercom/result-intercom.ts +108 -0
- package/src/runs/background/async-execution.ts +107 -7
- package/src/runs/background/async-job-tracker.ts +57 -14
- package/src/runs/background/async-resume.ts +28 -15
- package/src/runs/background/async-status.ts +60 -30
- package/src/runs/background/result-watcher.ts +111 -54
- package/src/runs/background/run-id-resolver.ts +83 -0
- package/src/runs/background/run-status.ts +79 -3
- package/src/runs/background/stale-run-reconciler.ts +46 -1
- package/src/runs/background/subagent-runner.ts +66 -18
- package/src/runs/foreground/chain-execution.ts +6 -0
- package/src/runs/foreground/execution.ts +21 -5
- package/src/runs/foreground/subagent-executor.ts +314 -18
- package/src/runs/shared/completion-guard.ts +23 -1
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
- package/src/runs/shared/nested-events.ts +819 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +115 -0
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/pi-args.ts +67 -5
- package/src/runs/shared/run-history.ts +12 -7
- package/src/runs/shared/single-output.ts +12 -2
- package/src/runs/shared/subagent-prompt-runtime.ts +25 -5
- package/src/shared/artifacts.ts +2 -2
- package/src/shared/types.ts +95 -0
- package/src/shared/utils.ts +11 -1
- package/src/tui/render.ts +254 -153
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
const MAX_NESTED_ID_LENGTH = 128;
|
|
4
|
+
export const MAX_NESTED_PATH_ENTRIES = 4;
|
|
5
|
+
|
|
6
|
+
export type NestedPathEntry = { runId: string; stepIndex?: number; agent?: string };
|
|
7
|
+
|
|
8
|
+
export function isSafeNestedPathId(value: unknown): value is string {
|
|
9
|
+
return typeof value === "string"
|
|
10
|
+
&& value.length > 0
|
|
11
|
+
&& value.length <= MAX_NESTED_ID_LENGTH
|
|
12
|
+
&& !path.isAbsolute(value)
|
|
13
|
+
&& !value.includes("/")
|
|
14
|
+
&& !value.includes("\\")
|
|
15
|
+
&& !value.includes("..");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function finiteNumber(value: unknown): number | undefined {
|
|
19
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function nonEmptyString(value: unknown, max: number): string | undefined {
|
|
23
|
+
return typeof value === "string" && value.length > 0 ? value.slice(0, max) : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sanitizeNestedPath(value: unknown): NestedPathEntry[] {
|
|
27
|
+
if (!Array.isArray(value)) return [];
|
|
28
|
+
return value.map((part) => {
|
|
29
|
+
if (!part || typeof part !== "object") return undefined;
|
|
30
|
+
const record = part as Record<string, unknown>;
|
|
31
|
+
if (!isSafeNestedPathId(record.runId)) return undefined;
|
|
32
|
+
return {
|
|
33
|
+
runId: record.runId,
|
|
34
|
+
...(finiteNumber(record.stepIndex) !== undefined ? { stepIndex: finiteNumber(record.stepIndex) } : {}),
|
|
35
|
+
...(nonEmptyString(record.agent, 128) ? { agent: nonEmptyString(record.agent, 128) } : {}),
|
|
36
|
+
};
|
|
37
|
+
}).filter((part): part is NestedPathEntry => Boolean(part)).slice(0, MAX_NESTED_PATH_ENTRIES);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseNestedPathEnv(value: string | undefined): NestedPathEntry[] {
|
|
41
|
+
if (!value) return [];
|
|
42
|
+
try {
|
|
43
|
+
return sanitizeNestedPath(JSON.parse(value) as unknown);
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function encodeNestedPathEnv(value: NestedPathEntry[]): string {
|
|
50
|
+
const sanitized = sanitizeNestedPath(value);
|
|
51
|
+
return sanitized.length ? JSON.stringify(sanitized) : "";
|
|
52
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { formatDuration, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
2
|
+
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
3
|
+
import type { ActivityState, NestedRunSummary, NestedStepSummary } from "../../shared/types.ts";
|
|
4
|
+
|
|
5
|
+
export interface NestedRunCounts {
|
|
6
|
+
total: number;
|
|
7
|
+
running: number;
|
|
8
|
+
paused: number;
|
|
9
|
+
complete: number;
|
|
10
|
+
failed: number;
|
|
11
|
+
queued: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function countNestedRuns(children: NestedRunSummary[] | undefined): NestedRunCounts {
|
|
15
|
+
const counts: NestedRunCounts = { total: 0, running: 0, paused: 0, complete: 0, failed: 0, queued: 0 };
|
|
16
|
+
for (const child of children ?? []) {
|
|
17
|
+
counts.total++;
|
|
18
|
+
counts[child.state]++;
|
|
19
|
+
const nested = countNestedRuns([...(child.children ?? []), ...(child.steps?.flatMap((step) => step.children ?? []) ?? [])]);
|
|
20
|
+
counts.total += nested.total;
|
|
21
|
+
counts.running += nested.running;
|
|
22
|
+
counts.paused += nested.paused;
|
|
23
|
+
counts.complete += nested.complete;
|
|
24
|
+
counts.failed += nested.failed;
|
|
25
|
+
counts.queued += nested.queued;
|
|
26
|
+
}
|
|
27
|
+
return counts;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatNestedAggregate(children: NestedRunSummary[] | undefined): string | undefined {
|
|
31
|
+
const counts = countNestedRuns(children);
|
|
32
|
+
if (counts.total === 0) return undefined;
|
|
33
|
+
const parts = [
|
|
34
|
+
counts.running > 0 ? `${counts.running} running` : "",
|
|
35
|
+
counts.paused > 0 ? `${counts.paused} paused` : "",
|
|
36
|
+
counts.failed > 0 ? `${counts.failed} failed` : "",
|
|
37
|
+
counts.complete > 0 ? `${counts.complete} complete` : "",
|
|
38
|
+
counts.queued > 0 ? `${counts.queued} queued` : "",
|
|
39
|
+
].filter(Boolean);
|
|
40
|
+
return `+${counts.total} nested run${counts.total === 1 ? "" : "s"}${parts.length ? ` (${parts.join(", ")})` : ""}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nestedRunLabel(run: NestedRunSummary): string {
|
|
44
|
+
if (run.agent) return run.agent;
|
|
45
|
+
if (run.agents?.length) return run.agents.length === 1 ? run.agents[0]! : `${run.agents.slice(0, 2).join(", ")}${run.agents.length > 2 ? ` +${run.agents.length - 2}` : ""}`;
|
|
46
|
+
return run.id;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatNestedActivity(input: {
|
|
50
|
+
activityState?: ActivityState;
|
|
51
|
+
lastActivityAt?: number;
|
|
52
|
+
currentTool?: string;
|
|
53
|
+
currentToolStartedAt?: number;
|
|
54
|
+
currentPath?: string;
|
|
55
|
+
turnCount?: number;
|
|
56
|
+
toolCount?: number;
|
|
57
|
+
totalTokens?: NestedRunSummary["totalTokens"];
|
|
58
|
+
}): string | undefined {
|
|
59
|
+
const facts: string[] = [];
|
|
60
|
+
if (input.currentTool && input.currentToolStartedAt !== undefined) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
|
|
61
|
+
else if (input.currentTool) facts.push(`tool ${input.currentTool}`);
|
|
62
|
+
if (input.currentPath) facts.push(shortenPath(input.currentPath));
|
|
63
|
+
if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
|
|
64
|
+
if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
|
|
65
|
+
if (input.totalTokens) facts.push(`${formatTokens(input.totalTokens.total)} tok`);
|
|
66
|
+
const activity = formatActivityLabel(input.lastActivityAt, input.activityState as ActivityState | undefined);
|
|
67
|
+
return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatNestedRunLines(children: NestedRunSummary[] | undefined, options: { indent: string; maxDepth: number; maxLines: number; commandHints?: boolean }): string[] {
|
|
71
|
+
const lines: string[] = [];
|
|
72
|
+
const append = (items: NestedRunSummary[] | undefined, depth: number, indent: string): void => {
|
|
73
|
+
if (!items?.length || lines.length >= options.maxLines) return;
|
|
74
|
+
if (depth > options.maxDepth) {
|
|
75
|
+
const aggregate = formatNestedAggregate(items);
|
|
76
|
+
if (aggregate && lines.length < options.maxLines) lines.push(`${indent}↳ ${aggregate}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
for (let index = 0; index < items.length; index++) {
|
|
80
|
+
const child = items[index]!;
|
|
81
|
+
if (lines.length >= options.maxLines) {
|
|
82
|
+
const aggregate = formatNestedAggregate(items.slice(index));
|
|
83
|
+
if (aggregate) lines[lines.length - 1] = `${indent}↳ ${aggregate}`;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const activity = child.state === "running" ? formatNestedActivity(child) : undefined;
|
|
87
|
+
const error = child.error ? ` | error: ${child.error}` : "";
|
|
88
|
+
lines.push(`${indent}↳ ${nestedRunLabel(child)} [${child.id}] ${child.state}${activity ? ` | ${activity}` : ""}${error}`);
|
|
89
|
+
if (options.commandHints && lines.length < options.maxLines) lines.push(`${indent} Status: subagent({ action: "status", id: "${child.id}" })`);
|
|
90
|
+
if (depth === options.maxDepth) {
|
|
91
|
+
const aggregate = formatNestedAggregate([...(child.steps?.flatMap((step) => step.children ?? []) ?? []), ...(child.children ?? [])]);
|
|
92
|
+
if (aggregate && lines.length < options.maxLines) lines.push(`${indent} ↳ ${aggregate}`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const [stepIndex, step] of (child.steps ?? []).entries()) {
|
|
96
|
+
if (lines.length >= options.maxLines) return;
|
|
97
|
+
const stepActivity = step.status === "running" ? formatNestedActivity(step) : undefined;
|
|
98
|
+
lines.push(`${indent} ${stepIndex + 1}. ${step.agent} ${step.status}${stepActivity ? ` | ${stepActivity}` : ""}${step.error ? ` | error: ${step.error}` : ""}`);
|
|
99
|
+
append(step.children, depth + 1, `${indent} `);
|
|
100
|
+
}
|
|
101
|
+
append(child.children, depth + 1, `${indent} `);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
append(children, 0, options.indent);
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function formatNestedRunStatusLines(children: NestedRunSummary[] | undefined, options: { indent?: string; maxDepth?: number; maxLines?: number; commandHints?: boolean } = {}): string[] {
|
|
109
|
+
return formatNestedRunLines(children, {
|
|
110
|
+
indent: options.indent ?? " ",
|
|
111
|
+
maxDepth: options.maxDepth ?? 2,
|
|
112
|
+
maxLines: options.maxLines ?? 40,
|
|
113
|
+
commandHints: options.commandHints ?? false,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -2,15 +2,27 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { encodeNestedPathEnv, parseNestedPathEnv, type NestedPathEntry } from "./nested-path.ts";
|
|
6
|
+
import { resolveMcpDirectToolNames } from "./mcp-direct-tool-allowlist.ts";
|
|
5
7
|
|
|
6
8
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
7
9
|
const TASK_ARG_LIMIT = 8000;
|
|
8
10
|
const PROMPT_RUNTIME_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-prompt-runtime.ts");
|
|
11
|
+
const FANOUT_CHILD_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "extension", "fanout-child.ts");
|
|
9
12
|
export const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD";
|
|
10
13
|
export const SUBAGENT_ORCHESTRATOR_TARGET_ENV = "PI_SUBAGENT_ORCHESTRATOR_TARGET";
|
|
11
14
|
export const SUBAGENT_RUN_ID_ENV = "PI_SUBAGENT_RUN_ID";
|
|
12
15
|
export const SUBAGENT_CHILD_AGENT_ENV = "PI_SUBAGENT_CHILD_AGENT";
|
|
13
16
|
export const SUBAGENT_CHILD_INDEX_ENV = "PI_SUBAGENT_CHILD_INDEX";
|
|
17
|
+
export const SUBAGENT_FANOUT_CHILD_ENV = "PI_SUBAGENT_FANOUT_CHILD";
|
|
18
|
+
export const SUBAGENT_PARENT_EVENT_SINK_ENV = "PI_SUBAGENT_PARENT_EVENT_SINK";
|
|
19
|
+
export const SUBAGENT_PARENT_CONTROL_INBOX_ENV = "PI_SUBAGENT_PARENT_CONTROL_INBOX";
|
|
20
|
+
export const SUBAGENT_PARENT_ROOT_RUN_ID_ENV = "PI_SUBAGENT_PARENT_ROOT_RUN_ID";
|
|
21
|
+
export const SUBAGENT_PARENT_RUN_ID_ENV = "PI_SUBAGENT_PARENT_RUN_ID";
|
|
22
|
+
export const SUBAGENT_PARENT_CHILD_INDEX_ENV = "PI_SUBAGENT_PARENT_CHILD_INDEX";
|
|
23
|
+
export const SUBAGENT_PARENT_DEPTH_ENV = "PI_SUBAGENT_PARENT_DEPTH";
|
|
24
|
+
export const SUBAGENT_PARENT_PATH_ENV = "PI_SUBAGENT_PARENT_PATH";
|
|
25
|
+
export const SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV = "PI_SUBAGENT_PARENT_CAPABILITY_TOKEN";
|
|
14
26
|
|
|
15
27
|
interface BuildPiArgsInput {
|
|
16
28
|
baseArgs: string[];
|
|
@@ -27,12 +39,21 @@ interface BuildPiArgsInput {
|
|
|
27
39
|
extensions?: string[];
|
|
28
40
|
systemPrompt?: string | null;
|
|
29
41
|
mcpDirectTools?: string[];
|
|
42
|
+
cwd?: string;
|
|
30
43
|
promptFileStem?: string;
|
|
31
44
|
intercomSessionName?: string;
|
|
32
45
|
orchestratorIntercomTarget?: string;
|
|
33
46
|
runId?: string;
|
|
34
47
|
childAgentName?: string;
|
|
35
48
|
childIndex?: number;
|
|
49
|
+
parentEventSink?: string;
|
|
50
|
+
parentControlInbox?: string;
|
|
51
|
+
parentRootRunId?: string;
|
|
52
|
+
parentRunId?: string;
|
|
53
|
+
parentChildIndex?: number;
|
|
54
|
+
parentDepth?: number;
|
|
55
|
+
parentPath?: NestedPathEntry[];
|
|
56
|
+
parentCapabilityToken?: string;
|
|
36
57
|
}
|
|
37
58
|
|
|
38
59
|
interface BuildPiArgsResult {
|
|
@@ -69,22 +90,27 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
69
90
|
args.push("--model", modelArg);
|
|
70
91
|
}
|
|
71
92
|
|
|
93
|
+
const declaredBuiltinTools = input.tools?.filter((tool) => !(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) ?? [];
|
|
94
|
+
const fanoutAuthorized = declaredBuiltinTools.includes("subagent");
|
|
72
95
|
const toolExtensionPaths: string[] = [];
|
|
73
96
|
if (input.tools?.length) {
|
|
74
|
-
const builtinTools
|
|
97
|
+
const builtinTools = [...declaredBuiltinTools];
|
|
75
98
|
for (const tool of input.tools) {
|
|
76
|
-
if (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")) {
|
|
99
|
+
if (!declaredBuiltinTools.includes(tool) && (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) {
|
|
77
100
|
toolExtensionPaths.push(tool);
|
|
78
|
-
} else {
|
|
79
|
-
builtinTools.push(tool);
|
|
80
101
|
}
|
|
81
102
|
}
|
|
82
103
|
if (builtinTools.length > 0) {
|
|
104
|
+
if (input.mcpDirectTools?.length) {
|
|
105
|
+
builtinTools.push(...resolveMcpDirectToolNames(input.mcpDirectTools, input.cwd));
|
|
106
|
+
}
|
|
83
107
|
args.push("--tools", builtinTools.join(","));
|
|
84
108
|
}
|
|
85
109
|
}
|
|
86
110
|
|
|
87
|
-
const runtimeExtensions =
|
|
111
|
+
const runtimeExtensions = fanoutAuthorized
|
|
112
|
+
? [PROMPT_RUNTIME_EXTENSION_PATH, FANOUT_CHILD_EXTENSION_PATH]
|
|
113
|
+
: [PROMPT_RUNTIME_EXTENSION_PATH];
|
|
88
114
|
if (input.extensions !== undefined) {
|
|
89
115
|
args.push("--no-extensions");
|
|
90
116
|
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...input.extensions])]) {
|
|
@@ -122,6 +148,40 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
122
148
|
|
|
123
149
|
const env: Record<string, string | undefined> = {};
|
|
124
150
|
env[SUBAGENT_CHILD_ENV] = "1";
|
|
151
|
+
env[SUBAGENT_FANOUT_CHILD_ENV] = fanoutAuthorized ? "1" : "0";
|
|
152
|
+
const inheritedNestedRoute = Boolean(process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] && process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] && process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV]);
|
|
153
|
+
const parentRunId = input.parentRunId ?? input.runId ?? (inheritedNestedRoute ? process.env[SUBAGENT_RUN_ID_ENV] : undefined) ?? process.env[SUBAGENT_PARENT_RUN_ID_ENV] ?? "";
|
|
154
|
+
const parentChildIndex = input.parentChildIndex !== undefined
|
|
155
|
+
? String(input.parentChildIndex)
|
|
156
|
+
: input.childIndex !== undefined
|
|
157
|
+
? String(input.childIndex)
|
|
158
|
+
: process.env[SUBAGENT_PARENT_CHILD_INDEX_ENV] ?? "";
|
|
159
|
+
const inheritedDepth = Number(process.env[SUBAGENT_PARENT_DEPTH_ENV]);
|
|
160
|
+
const parentDepth = input.parentDepth ?? (inheritedNestedRoute && Number.isFinite(inheritedDepth) ? inheritedDepth + 1 : 1);
|
|
161
|
+
const parentPath = input.parentPath ?? [
|
|
162
|
+
...parseNestedPathEnv(process.env[SUBAGENT_PARENT_PATH_ENV]),
|
|
163
|
+
...(parentRunId ? [{
|
|
164
|
+
runId: parentRunId,
|
|
165
|
+
...(parentChildIndex && /^\d+$/.test(parentChildIndex) ? { stepIndex: Number(parentChildIndex) } : {}),
|
|
166
|
+
...(input.childAgentName ? { agent: input.childAgentName } : {}),
|
|
167
|
+
}] : []),
|
|
168
|
+
];
|
|
169
|
+
env[SUBAGENT_PARENT_EVENT_SINK_ENV] = fanoutAuthorized
|
|
170
|
+
? input.parentEventSink ?? process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] ?? ""
|
|
171
|
+
: "";
|
|
172
|
+
env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] = fanoutAuthorized
|
|
173
|
+
? input.parentControlInbox ?? process.env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] ?? ""
|
|
174
|
+
: "";
|
|
175
|
+
env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] = fanoutAuthorized
|
|
176
|
+
? input.parentRootRunId ?? process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] ?? input.runId ?? ""
|
|
177
|
+
: "";
|
|
178
|
+
env[SUBAGENT_PARENT_RUN_ID_ENV] = fanoutAuthorized ? parentRunId : "";
|
|
179
|
+
env[SUBAGENT_PARENT_CHILD_INDEX_ENV] = fanoutAuthorized ? parentChildIndex : "";
|
|
180
|
+
env[SUBAGENT_PARENT_DEPTH_ENV] = fanoutAuthorized ? String(parentDepth) : "";
|
|
181
|
+
env[SUBAGENT_PARENT_PATH_ENV] = fanoutAuthorized ? encodeNestedPathEnv(parentPath) : "";
|
|
182
|
+
env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] = fanoutAuthorized
|
|
183
|
+
? input.parentCapabilityToken ?? process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] ?? ""
|
|
184
|
+
: "";
|
|
125
185
|
env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext ? "1" : "0";
|
|
126
186
|
env.PI_SUBAGENT_INHERIT_SKILLS = input.inheritSkills ? "1" : "0";
|
|
127
187
|
if (input.intercomSessionName) {
|
|
@@ -148,6 +208,8 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
148
208
|
return { args, env, tempDir };
|
|
149
209
|
}
|
|
150
210
|
|
|
211
|
+
export const parseParentPathEnv = parseNestedPathEnv;
|
|
212
|
+
|
|
151
213
|
export function cleanupTempDir(tempDir: string | null | undefined): void {
|
|
152
214
|
if (!tempDir) return;
|
|
153
215
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
2
|
import * as path from "node:path";
|
|
3
|
+
import { getAgentDir } from "../../shared/utils.ts";
|
|
4
4
|
|
|
5
5
|
export interface RunEntry {
|
|
6
6
|
agent: string;
|
|
@@ -11,10 +11,13 @@ export interface RunEntry {
|
|
|
11
11
|
exit?: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const HISTORY_PATH = path.join(os.homedir(), ".pi", "agent", "run-history.jsonl");
|
|
15
14
|
const ROTATE_READ_THRESHOLD = 1200;
|
|
16
15
|
const ROTATE_KEEP = 1000;
|
|
17
16
|
|
|
17
|
+
function getHistoryPath(): string {
|
|
18
|
+
return path.join(getAgentDir(), "run-history.jsonl");
|
|
19
|
+
}
|
|
20
|
+
|
|
18
21
|
export function recordRun(agent: string, task: string, exitCode: number, durationMs: number): void {
|
|
19
22
|
try {
|
|
20
23
|
const entry: RunEntry = {
|
|
@@ -25,18 +28,20 @@ export function recordRun(agent: string, task: string, exitCode: number, duratio
|
|
|
25
28
|
duration: durationMs,
|
|
26
29
|
...(exitCode !== 0 ? { exit: exitCode } : {}),
|
|
27
30
|
};
|
|
28
|
-
|
|
29
|
-
fs.
|
|
31
|
+
const historyPath = getHistoryPath();
|
|
32
|
+
fs.mkdirSync(path.dirname(historyPath), { recursive: true });
|
|
33
|
+
fs.appendFileSync(historyPath, `${JSON.stringify(entry)}\n`);
|
|
30
34
|
} catch {
|
|
31
35
|
// Best-effort — never crash the execution flow for history recording
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
export function loadRunsForAgent(agent: string): RunEntry[] {
|
|
36
|
-
|
|
40
|
+
const historyPath = getHistoryPath();
|
|
41
|
+
if (!fs.existsSync(historyPath)) return [];
|
|
37
42
|
let raw: string;
|
|
38
43
|
try {
|
|
39
|
-
raw = fs.readFileSync(
|
|
44
|
+
raw = fs.readFileSync(historyPath, "utf-8");
|
|
40
45
|
} catch {
|
|
41
46
|
return [];
|
|
42
47
|
}
|
|
@@ -45,7 +50,7 @@ export function loadRunsForAgent(agent: string): RunEntry[] {
|
|
|
45
50
|
|
|
46
51
|
if (lines.length > ROTATE_READ_THRESHOLD) {
|
|
47
52
|
lines = lines.slice(-ROTATE_KEEP);
|
|
48
|
-
try { fs.writeFileSync(
|
|
53
|
+
try { fs.writeFileSync(historyPath, `${lines.join("\n")}\n`, "utf-8"); } catch {}
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
return lines
|
|
@@ -8,12 +8,22 @@ export interface SingleOutputSnapshot {
|
|
|
8
8
|
size?: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export function normalizeSingleOutputOverride(
|
|
12
|
+
output: string | boolean | undefined,
|
|
13
|
+
defaultOutput: string | undefined,
|
|
14
|
+
): string | false | undefined {
|
|
15
|
+
if (output === false || output === "false") return false;
|
|
16
|
+
if (output === true || output === "true") return defaultOutput;
|
|
17
|
+
if (typeof output === "string" && output.length > 0) return output;
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
export function resolveSingleOutputPath(
|
|
12
|
-
output: string |
|
|
22
|
+
output: string | boolean | undefined,
|
|
13
23
|
runtimeCwd: string,
|
|
14
24
|
requestedCwd?: string,
|
|
15
25
|
): string | undefined {
|
|
16
|
-
if (typeof output !== "string" || !output) return undefined;
|
|
26
|
+
if (typeof output !== "string" || !output || output === "false" || output === "true") return undefined;
|
|
17
27
|
if (path.isAbsolute(output)) return output;
|
|
18
28
|
const baseCwd = requestedCwd
|
|
19
29
|
? (path.isAbsolute(requestedCwd) ? requestedCwd : path.resolve(runtimeCwd, requestedCwd))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { SUBAGENT_FANOUT_CHILD_ENV } from "./pi-args.ts";
|
|
2
3
|
|
|
3
4
|
const SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_PROJECT_CONTEXT";
|
|
4
5
|
const SUBAGENT_INHERIT_SKILLS_ENV = "PI_SUBAGENT_INHERIT_SKILLS";
|
|
@@ -12,6 +13,15 @@ export const CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS = [
|
|
|
12
13
|
"If you need to edit files, call the actual edit/write tools. Do not print tool-call syntax, patches, or pseudo-tool calls as text.",
|
|
13
14
|
].join("\n");
|
|
14
15
|
|
|
16
|
+
export const CHILD_FANOUT_BOUNDARY_INSTRUCTIONS = [
|
|
17
|
+
"You are a child subagent with explicit fanout responsibility for this assigned task.",
|
|
18
|
+
"The parent session owns final orchestration, acceptance, and follow-up implementation launches.",
|
|
19
|
+
"You may use the `subagent` tool only for the fanout work explicitly requested in this task.",
|
|
20
|
+
"Do not broaden yourself into general parent orchestration. Do not launch follow-up workers unless the task explicitly asks for that.",
|
|
21
|
+
"The maxSubagentDepth cap still applies and may block further fanout.",
|
|
22
|
+
"If you need to edit files, call the actual edit/write tools. Do not print tool-call syntax, patches, or pseudo-tool calls as text.",
|
|
23
|
+
].join("\n");
|
|
24
|
+
|
|
15
25
|
const PARENT_ONLY_CUSTOM_MESSAGE_TYPES = new Set([
|
|
16
26
|
"subagent-orchestration-instructions",
|
|
17
27
|
"subagent-slash-result",
|
|
@@ -62,9 +72,17 @@ export function stripSubagentOrchestrationSkill(prompt: string): string {
|
|
|
62
72
|
.replace(/[ \t]*<skill>\s*[\s\S]*?<\/skill>\s*/g, (block) => SUBAGENT_ORCHESTRATION_SKILL_NAME_PATTERN.test(block) ? "" : block);
|
|
63
73
|
}
|
|
64
74
|
|
|
75
|
+
function stripChildBoundaryInstructions(prompt: string): string {
|
|
76
|
+
let rewritten = prompt;
|
|
77
|
+
for (const boundary of [CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS, CHILD_FANOUT_BOUNDARY_INSTRUCTIONS]) {
|
|
78
|
+
rewritten = rewritten.split(boundary).join("");
|
|
79
|
+
}
|
|
80
|
+
return rewritten.replace(/^(?:[ \t]*\r?\n)+/, "");
|
|
81
|
+
}
|
|
82
|
+
|
|
65
83
|
export function rewriteSubagentPrompt(
|
|
66
84
|
prompt: string,
|
|
67
|
-
options: { inheritProjectContext: boolean; inheritSkills: boolean },
|
|
85
|
+
options: { inheritProjectContext: boolean; inheritSkills: boolean; fanoutChild?: boolean },
|
|
68
86
|
): string {
|
|
69
87
|
let rewritten = prompt;
|
|
70
88
|
if (!options.inheritProjectContext) {
|
|
@@ -74,9 +92,9 @@ export function rewriteSubagentPrompt(
|
|
|
74
92
|
rewritten = stripInheritedSkills(rewritten);
|
|
75
93
|
}
|
|
76
94
|
rewritten = stripSubagentOrchestrationSkill(rewritten);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
95
|
+
rewritten = stripChildBoundaryInstructions(rewritten);
|
|
96
|
+
const boundary = options.fanoutChild ? CHILD_FANOUT_BOUNDARY_INSTRUCTIONS : CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS;
|
|
97
|
+
return `${boundary}\n\n${rewritten}`;
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
function isParentOnlySubagentMessage(message: unknown): boolean {
|
|
@@ -139,10 +157,12 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
139
157
|
|
|
140
158
|
const inheritProjectContext = readBooleanEnv(SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV);
|
|
141
159
|
const inheritSkills = readBooleanEnv(SUBAGENT_INHERIT_SKILLS_ENV);
|
|
142
|
-
|
|
160
|
+
const fanoutChild = readBooleanEnv(SUBAGENT_FANOUT_CHILD_ENV);
|
|
161
|
+
if (inheritProjectContext === undefined && inheritSkills === undefined && fanoutChild === undefined) return;
|
|
143
162
|
const rewritten = rewriteSubagentPrompt(event.systemPrompt, {
|
|
144
163
|
inheritProjectContext: inheritProjectContext ?? true,
|
|
145
164
|
inheritSkills: inheritSkills ?? true,
|
|
165
|
+
fanoutChild: fanoutChild === true,
|
|
146
166
|
});
|
|
147
167
|
if (rewritten === event.systemPrompt) return;
|
|
148
168
|
return { systemPrompt: rewritten };
|
package/src/shared/artifacts.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
2
|
import * as path from "node:path";
|
|
4
3
|
import { TEMP_ARTIFACTS_DIR, type ArtifactPaths } from "./types.ts";
|
|
4
|
+
import { getAgentDir } from "./utils.ts";
|
|
5
5
|
const CLEANUP_MARKER_FILE = ".last-cleanup";
|
|
6
6
|
|
|
7
7
|
export function getArtifactsDir(sessionFile: string | null): string {
|
|
@@ -74,7 +74,7 @@ export function cleanupOldArtifacts(dir: string, maxAgeDays: number): void {
|
|
|
74
74
|
export function cleanupAllArtifactDirs(maxAgeDays: number): void {
|
|
75
75
|
cleanupOldArtifacts(TEMP_ARTIFACTS_DIR, maxAgeDays);
|
|
76
76
|
|
|
77
|
-
const sessionsBase = path.join(
|
|
77
|
+
const sessionsBase = path.join(getAgentDir(), "sessions");
|
|
78
78
|
if (!fs.existsSync(sessionsBase)) return;
|
|
79
79
|
|
|
80
80
|
let dirs: string[];
|
package/src/shared/types.ts
CHANGED
|
@@ -83,6 +83,8 @@ export interface ControlEvent {
|
|
|
83
83
|
agent: string;
|
|
84
84
|
index?: number;
|
|
85
85
|
runId: string;
|
|
86
|
+
nestedRunId?: string;
|
|
87
|
+
nestingPath?: NestedRunAddress["path"];
|
|
86
88
|
message: string;
|
|
87
89
|
reason?: "idle" | "completion_guard" | "active_long_running" | "tool_failures" | "time_threshold" | "turn_threshold" | "token_threshold";
|
|
88
90
|
turns?: number;
|
|
@@ -98,6 +100,21 @@ export interface ControlEvent {
|
|
|
98
100
|
export type SubagentResultStatus = "completed" | "failed" | "paused" | "detached";
|
|
99
101
|
export type SubagentRunMode = "single" | "parallel" | "chain";
|
|
100
102
|
|
|
103
|
+
export type PublicNestedStepSummary = Pick<
|
|
104
|
+
NestedStepSummary,
|
|
105
|
+
"agent" | "status" | "sessionFile" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "startedAt" | "endedAt" | "error"
|
|
106
|
+
> & {
|
|
107
|
+
children?: PublicNestedRunSummary[];
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type PublicNestedRunSummary = Pick<
|
|
111
|
+
NestedRunSummary,
|
|
112
|
+
"id" | "parentRunId" | "parentStepIndex" | "parentAgent" | "depth" | "path" | "asyncDir" | "sessionId" | "sessionFile" | "intercomTarget" | "ownerIntercomTarget" | "leafIntercomTarget" | "ownerState" | "mode" | "state" | "agent" | "agents" | "currentStep" | "chainStepCount" | "parallelGroups" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "totalTokens" | "startedAt" | "endedAt" | "lastUpdate" | "error"
|
|
113
|
+
> & {
|
|
114
|
+
steps?: PublicNestedStepSummary[];
|
|
115
|
+
children?: PublicNestedRunSummary[];
|
|
116
|
+
};
|
|
117
|
+
|
|
101
118
|
export interface SubagentResultIntercomChild {
|
|
102
119
|
agent: string;
|
|
103
120
|
status: SubagentResultStatus;
|
|
@@ -106,6 +123,7 @@ export interface SubagentResultIntercomChild {
|
|
|
106
123
|
artifactPath?: string;
|
|
107
124
|
sessionPath?: string;
|
|
108
125
|
intercomTarget?: string;
|
|
126
|
+
children?: PublicNestedRunSummary[];
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
export interface SubagentResultIntercomPayload {
|
|
@@ -261,6 +279,76 @@ export interface AsyncParallelGroupStatus {
|
|
|
261
279
|
stepIndex: number;
|
|
262
280
|
}
|
|
263
281
|
|
|
282
|
+
export type NestedRunState = "queued" | "running" | "complete" | "failed" | "paused";
|
|
283
|
+
export type NestedOwnerState = "live" | "gone" | "unknown";
|
|
284
|
+
|
|
285
|
+
export interface NestedRunAddress {
|
|
286
|
+
id: string;
|
|
287
|
+
parentRunId: string;
|
|
288
|
+
parentStepIndex?: number;
|
|
289
|
+
parentAgent?: string;
|
|
290
|
+
depth: number;
|
|
291
|
+
path: Array<{ runId: string; stepIndex?: number; agent?: string }>;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export interface NestedStepSummary {
|
|
295
|
+
agent: string;
|
|
296
|
+
status: "pending" | "running" | "complete" | "completed" | "failed" | "paused";
|
|
297
|
+
sessionFile?: string;
|
|
298
|
+
activityState?: ActivityState;
|
|
299
|
+
lastActivityAt?: number;
|
|
300
|
+
currentTool?: string;
|
|
301
|
+
currentToolStartedAt?: number;
|
|
302
|
+
currentPath?: string;
|
|
303
|
+
turnCount?: number;
|
|
304
|
+
toolCount?: number;
|
|
305
|
+
startedAt?: number;
|
|
306
|
+
endedAt?: number;
|
|
307
|
+
error?: string;
|
|
308
|
+
children?: NestedRunSummary[];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface NestedRunSummary extends NestedRunAddress {
|
|
312
|
+
asyncDir?: string;
|
|
313
|
+
pid?: number;
|
|
314
|
+
sessionId?: string;
|
|
315
|
+
sessionFile?: string;
|
|
316
|
+
intercomTarget?: string;
|
|
317
|
+
ownerIntercomTarget?: string;
|
|
318
|
+
leafIntercomTarget?: string;
|
|
319
|
+
ownerState?: NestedOwnerState;
|
|
320
|
+
controlInbox?: string;
|
|
321
|
+
capabilityToken?: string;
|
|
322
|
+
mode?: SubagentRunMode;
|
|
323
|
+
state: NestedRunState;
|
|
324
|
+
agent?: string;
|
|
325
|
+
agents?: string[];
|
|
326
|
+
currentStep?: number;
|
|
327
|
+
chainStepCount?: number;
|
|
328
|
+
parallelGroups?: AsyncParallelGroupStatus[];
|
|
329
|
+
steps?: NestedStepSummary[];
|
|
330
|
+
children?: NestedRunSummary[];
|
|
331
|
+
activityState?: ActivityState;
|
|
332
|
+
lastActivityAt?: number;
|
|
333
|
+
currentTool?: string;
|
|
334
|
+
currentToolStartedAt?: number;
|
|
335
|
+
currentPath?: string;
|
|
336
|
+
turnCount?: number;
|
|
337
|
+
toolCount?: number;
|
|
338
|
+
totalTokens?: TokenUsage;
|
|
339
|
+
startedAt?: number;
|
|
340
|
+
endedAt?: number;
|
|
341
|
+
lastUpdate?: number;
|
|
342
|
+
error?: string;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export interface NestedRouteInfo {
|
|
346
|
+
rootRunId: string;
|
|
347
|
+
eventSink: string;
|
|
348
|
+
controlInbox: string;
|
|
349
|
+
capabilityToken: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
264
352
|
export interface AsyncStartedEvent {
|
|
265
353
|
id?: string;
|
|
266
354
|
asyncDir?: string;
|
|
@@ -272,6 +360,7 @@ export interface AsyncStartedEvent {
|
|
|
272
360
|
chain?: string[];
|
|
273
361
|
chainStepCount?: number;
|
|
274
362
|
parallelGroups?: AsyncParallelGroupStatus[];
|
|
363
|
+
nestedRoute?: NestedRouteInfo;
|
|
275
364
|
}
|
|
276
365
|
|
|
277
366
|
export interface AsyncStatus {
|
|
@@ -297,6 +386,7 @@ export interface AsyncStatus {
|
|
|
297
386
|
steps?: Array<{
|
|
298
387
|
agent: string;
|
|
299
388
|
status: "pending" | "running" | "complete" | "completed" | "failed" | "paused";
|
|
389
|
+
children?: NestedRunSummary[];
|
|
300
390
|
sessionFile?: string;
|
|
301
391
|
activityState?: ActivityState;
|
|
302
392
|
lastActivityAt?: number;
|
|
@@ -361,6 +451,8 @@ export interface AsyncJobState {
|
|
|
361
451
|
totalTokens?: TokenUsage;
|
|
362
452
|
sessionFile?: string;
|
|
363
453
|
controlEventCursor?: number;
|
|
454
|
+
nestedRoute?: NestedRouteInfo;
|
|
455
|
+
nestedChildren?: NestedRunSummary[];
|
|
364
456
|
}
|
|
365
457
|
|
|
366
458
|
export interface ForegroundResumeChild {
|
|
@@ -398,6 +490,8 @@ export interface SubagentState {
|
|
|
398
490
|
turnCount?: number;
|
|
399
491
|
tokens?: number;
|
|
400
492
|
toolCount?: number;
|
|
493
|
+
nestedRoute?: NestedRouteInfo;
|
|
494
|
+
nestedChildren?: NestedRunSummary[];
|
|
401
495
|
interrupt?: () => boolean;
|
|
402
496
|
}>;
|
|
403
497
|
lastForegroundControlId: string | null;
|
|
@@ -473,6 +567,7 @@ export interface RunSyncOptions {
|
|
|
473
567
|
outputPath?: string;
|
|
474
568
|
outputMode?: OutputMode;
|
|
475
569
|
maxSubagentDepth?: number;
|
|
570
|
+
nestedRoute?: NestedRouteInfo;
|
|
476
571
|
/** Override the agent's default model (format: "provider/id" or just "id") */
|
|
477
572
|
modelOverride?: string;
|
|
478
573
|
/** Registry models available for heuristic bare-model resolution */
|
package/src/shared/utils.ts
CHANGED
|
@@ -13,6 +13,13 @@ import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, Singl
|
|
|
13
13
|
// File System Utilities
|
|
14
14
|
// ============================================================================
|
|
15
15
|
|
|
16
|
+
export function getAgentDir(): string {
|
|
17
|
+
const configured = process.env.PI_CODING_AGENT_DIR;
|
|
18
|
+
if (configured === "~") return os.homedir();
|
|
19
|
+
if (configured?.startsWith("~/")) return path.join(os.homedir(), configured.slice(2));
|
|
20
|
+
return configured || path.join(os.homedir(), ".pi", "agent");
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
const statusCache = new Map<string, { mtime: number; status: AsyncStatus }>();
|
|
17
24
|
|
|
18
25
|
function getErrorMessage(error: unknown): string {
|
|
@@ -182,8 +189,11 @@ export function getFinalOutput(messages: Message[]): string {
|
|
|
182
189
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
183
190
|
const msg = messages[i];
|
|
184
191
|
if (msg.role === "assistant") {
|
|
192
|
+
const hasAssistantError = ("errorMessage" in msg && typeof msg.errorMessage === "string" && msg.errorMessage.length > 0)
|
|
193
|
+
|| ("stopReason" in msg && msg.stopReason === "error");
|
|
194
|
+
if (hasAssistantError) continue;
|
|
185
195
|
for (const part of msg.content) {
|
|
186
|
-
if (part.type === "text") return part.text;
|
|
196
|
+
if (part.type === "text" && part.text.trim().length > 0) return part.text;
|
|
187
197
|
}
|
|
188
198
|
}
|
|
189
199
|
}
|