pi-subagents 0.61.0 → 0.63.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 +52 -1
- package/docs/agents.md +11 -6
- package/docs/configuration.md +31 -5
- package/docs/extension-api.md +2 -2
- package/docs/models.md +5 -5
- package/docs/observability.md +5 -2
- package/docs/tool-reference.md +3 -3
- package/install.mjs +0 -1
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +3 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +3 -3
- package/src/agents/agent-management.ts +41 -4
- package/src/agents/agent-serializer.ts +3 -0
- package/src/agents/agents.ts +120 -124
- package/src/agents/runtime-agent-registry.ts +5 -1
- package/src/api/preflight.ts +4 -0
- package/src/api/shared-types.ts +3 -0
- package/src/extension/config.ts +20 -0
- package/src/extension/public-execution.ts +1 -0
- package/src/extension/schemas.ts +6 -2
- package/src/extension/tool-description.ts +1 -1
- package/src/inspectors/herdr/inspector-runner.ts +19 -13
- package/src/runs/background/active-async-capacity.ts +26 -8
- package/src/runs/background/async-execution.ts +100 -23
- package/src/runs/background/async-resume.ts +6 -2
- package/src/runs/background/async-status.ts +18 -2
- package/src/runs/background/notify.ts +13 -1
- package/src/runs/background/process-terminal.ts +16 -0
- package/src/runs/background/run-status.ts +22 -2
- package/src/runs/background/scheduled-runs.ts +63 -6
- package/src/runs/background/steering.ts +4 -1
- package/src/runs/background/subagent-runner.ts +44 -9
- package/src/runs/background/wait-completions.ts +13 -0
- package/src/runs/background/wait-tool.ts +1 -7
- package/src/runs/foreground/execution.ts +14 -7
- package/src/runs/foreground/subagent-executor.ts +38 -5
- package/src/runs/shared/acceptance.ts +85 -18
- package/src/runs/shared/capability-ceiling.ts +1 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/lane-metadata.ts +24 -3
- package/src/runs/shared/parallel-handoff.ts +4 -0
- package/src/runs/shared/parallel-utils.ts +2 -6
- package/src/runs/shared/permissions.ts +1 -1
- package/src/runs/shared/pi-args.ts +32 -14
- package/src/runs/shared/pi-spawn.ts +69 -35
- package/src/runs/shared/structured-output.ts +33 -6
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
- package/src/runs/shared/task-intent.ts +21 -7
- package/src/runs/shared/tool-timeout.ts +1 -1
- package/src/runs/shared/worktree.ts +467 -63
- package/src/shared/atomic-json.ts +3 -1
- package/src/shared/fork-context.ts +0 -12
- package/src/shared/fork-session-cwd.ts +27 -0
- package/src/shared/launch-contract.ts +3 -0
- package/src/shared/types.ts +32 -1
- package/src/shared/utils.ts +18 -7
- package/src/slash/slash-commands.ts +1 -1
- package/src/slash/subagents-admin.ts +26 -12
- package/src/tui/fleet-status.ts +61 -2
- package/src/tui/fleet.ts +12 -7
- package/src/tui/render.ts +222 -14
- package/src/workflows/workflow-checklist.ts +441 -0
|
@@ -20,8 +20,10 @@ import { encodeExtensionBindings, PI_SUBAGENT_EXTENSION_BINDINGS_ENV, type Exten
|
|
|
20
20
|
import { RUNTIME_EXTENSION_ACK_PATH_ENV } from "./runtime-acknowledged-extensions.ts";
|
|
21
21
|
import {
|
|
22
22
|
STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV,
|
|
23
|
+
STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV,
|
|
23
24
|
STRUCTURED_OUTPUT_CAPTURE_ENV,
|
|
24
25
|
STRUCTURED_OUTPUT_SCHEMA_ENV,
|
|
26
|
+
type StructuredOutputRuntime,
|
|
25
27
|
} from "./structured-output.ts";
|
|
26
28
|
import {
|
|
27
29
|
TEMP_ROOT_DIR,
|
|
@@ -91,11 +93,12 @@ export function resolveSubagentTaskDelivery(
|
|
|
91
93
|
: "auto";
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
function shouldDeliverTaskViaFile(
|
|
96
|
+
export function shouldDeliverTaskViaFile(
|
|
95
97
|
task: string,
|
|
96
98
|
delivery: SubagentTaskDelivery,
|
|
99
|
+
platform: NodeJS.Platform = process.platform,
|
|
97
100
|
): boolean {
|
|
98
|
-
return delivery === "file" || task.length > TASK_ARG_LIMIT;
|
|
101
|
+
return delivery === "file" || platform === "darwin" || task.length > TASK_ARG_LIMIT;
|
|
99
102
|
}
|
|
100
103
|
const MAX_LAUNCH_RESOLVED_EXTENSION_IDS = 32;
|
|
101
104
|
const PROMPT_RUNTIME_EXTENSION_PATH = path.join(
|
|
@@ -171,6 +174,7 @@ export interface BuildPiArgsInput {
|
|
|
171
174
|
inheritSkills: boolean;
|
|
172
175
|
requireReadTool?: boolean;
|
|
173
176
|
tools?: string[];
|
|
177
|
+
excludeTools?: string[];
|
|
174
178
|
extensions?: string[];
|
|
175
179
|
subagentOnlyExtensions?: string[];
|
|
176
180
|
systemPrompt?: string | null;
|
|
@@ -201,12 +205,7 @@ export interface BuildPiArgsInput {
|
|
|
201
205
|
steerInboxDir?: string;
|
|
202
206
|
steerCapabilityPath?: string;
|
|
203
207
|
steerAckDir?: string;
|
|
204
|
-
structuredOutput?:
|
|
205
|
-
schema: JsonSchemaObject;
|
|
206
|
-
schemaPath: string;
|
|
207
|
-
outputPath: string;
|
|
208
|
-
acceptanceReportPath?: string;
|
|
209
|
-
};
|
|
208
|
+
structuredOutput?: StructuredOutputRuntime;
|
|
210
209
|
fast?: boolean;
|
|
211
210
|
modelCandidates?: readonly string[];
|
|
212
211
|
toolBudget?: ResolvedToolBudget;
|
|
@@ -300,6 +299,7 @@ function resolveFastModeExtension(input: Pick<ResolvePiLaunchToolPlanInput, "fas
|
|
|
300
299
|
|
|
301
300
|
export interface ResolvePiLaunchToolPlanInput {
|
|
302
301
|
tools?: string[];
|
|
302
|
+
excludeTools?: string[];
|
|
303
303
|
allowNestedSubagents?: boolean;
|
|
304
304
|
extensions?: string[];
|
|
305
305
|
subagentOnlyExtensions?: string[];
|
|
@@ -329,6 +329,7 @@ export interface PiLaunchToolPlan {
|
|
|
329
329
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
330
330
|
requestedBuiltinTools: string[];
|
|
331
331
|
declaredBuiltinTools: string[];
|
|
332
|
+
excludeTools: string[];
|
|
332
333
|
toolExtensionPaths: string[];
|
|
333
334
|
resolvedMcpSelections: ResolvedMcpDirectToolSelection[];
|
|
334
335
|
effectiveMcpSelections: ResolvedMcpDirectToolSelection[];
|
|
@@ -499,8 +500,12 @@ export function resolvePiLaunchToolPlan(
|
|
|
499
500
|
? ["read", ...requestedBuiltinTools]
|
|
500
501
|
: requestedBuiltinTools
|
|
501
502
|
).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
|
|
502
|
-
const
|
|
503
|
+
const excludeTools = [...new Set((input.excludeTools ?? []).map((tool) => tool.trim()).filter(Boolean))];
|
|
504
|
+
const excludedToolSet = new Set(excludeTools);
|
|
505
|
+
const effectiveDeclaredBuiltinTools = declaredBuiltinTools.filter((tool) => !excludedToolSet.has(tool));
|
|
506
|
+
const fanoutAuthorized = effectiveDeclaredBuiltinTools.includes("subagent") || (
|
|
503
507
|
input.allowNestedSubagents === true &&
|
|
508
|
+
!excludedToolSet.has("subagent") &&
|
|
504
509
|
(!allowedToolSet || allowedToolSet.has("subagent"))
|
|
505
510
|
);
|
|
506
511
|
const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions
|
|
@@ -524,7 +529,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
524
529
|
!allowedToolSet ||
|
|
525
530
|
allowedToolSet.has(selection.name) ||
|
|
526
531
|
isLegacyUnderscoreMcpToolAllowed(selection, allowedToolSet, resolvedMcpNames, legacyMcpNameCounts),
|
|
527
|
-
);
|
|
532
|
+
).filter((selection) => !excludedToolSet.has(selection.name));
|
|
528
533
|
const effectiveMcpTools = effectiveMcpSelections.map(
|
|
529
534
|
(selection) => selection.name,
|
|
530
535
|
);
|
|
@@ -540,10 +545,10 @@ export function resolvePiLaunchToolPlan(
|
|
|
540
545
|
input.tools !== undefined ||
|
|
541
546
|
(input.mcpDirectTools?.length ?? 0) > 0 ||
|
|
542
547
|
allowedToolSet !== undefined;
|
|
543
|
-
const internalTools = input.structuredOutput ? ["structured_output"] : [];
|
|
548
|
+
const internalTools = (input.structuredOutput ? ["structured_output"] : []).filter((tool) => !excludedToolSet.has(tool));
|
|
544
549
|
const effectiveToolAllowlist = [
|
|
545
550
|
...new Set([
|
|
546
|
-
...
|
|
551
|
+
...effectiveDeclaredBuiltinTools,
|
|
547
552
|
...effectiveMcpTools,
|
|
548
553
|
...internalTools,
|
|
549
554
|
]),
|
|
@@ -554,11 +559,11 @@ export function resolvePiLaunchToolPlan(
|
|
|
554
559
|
// appended intercom alongside contact_supervisor, so that exact pairing is
|
|
555
560
|
// legacy plumbing, not a user demand for an external intercom provider;
|
|
556
561
|
// a lone intercom entry stays strictly required (#1207).
|
|
557
|
-
const legacySupervisorPairing =
|
|
562
|
+
const legacySupervisorPairing = effectiveDeclaredBuiltinTools.includes("contact_supervisor");
|
|
558
563
|
const requiredChildTools = explicitToolAllowlist
|
|
559
564
|
? [
|
|
560
565
|
...new Set([
|
|
561
|
-
...(input.tools !== undefined ?
|
|
566
|
+
...(input.tools !== undefined ? effectiveDeclaredBuiltinTools : []),
|
|
562
567
|
...(input.mcpDirectTools?.length ? effectiveMcpTools : []),
|
|
563
568
|
...internalTools,
|
|
564
569
|
].filter((tool) => tool !== "contact_supervisor" && (!legacySupervisorPairing || tool !== "intercom"))),
|
|
@@ -620,6 +625,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
620
625
|
ceiling: capabilityCeiling,
|
|
621
626
|
...(requestedToolNames ? { requestedTools: requestedToolNames } : {}),
|
|
622
627
|
effectiveTools: effectiveToolAllowlist,
|
|
628
|
+
...(excludeTools.length > 0 ? { excludeTools } : {}),
|
|
623
629
|
removedTools:
|
|
624
630
|
requestedToolNames?.filter(
|
|
625
631
|
(tool) => !effectiveToolAllowlist.includes(tool),
|
|
@@ -657,6 +663,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
657
663
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
658
664
|
requestedBuiltinTools,
|
|
659
665
|
declaredBuiltinTools,
|
|
666
|
+
excludeTools,
|
|
660
667
|
toolExtensionPaths,
|
|
661
668
|
resolvedMcpSelections,
|
|
662
669
|
effectiveMcpSelections,
|
|
@@ -740,6 +747,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
740
747
|
|
|
741
748
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
742
749
|
tools: input.tools,
|
|
750
|
+
excludeTools: input.excludeTools,
|
|
743
751
|
allowNestedSubagents: input.allowNestedSubagents,
|
|
744
752
|
extensions: input.extensions,
|
|
745
753
|
subagentOnlyExtensions: input.subagentOnlyExtensions,
|
|
@@ -766,6 +774,8 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
766
774
|
);
|
|
767
775
|
if (toolPlan.effectiveToolAllowlist.length > 0)
|
|
768
776
|
args.push(toolPlan.effectiveToolAllowlist.join(","));
|
|
777
|
+
} else if (toolPlan.excludeTools.length > 0) {
|
|
778
|
+
args.push("--exclude-tools", toolPlan.excludeTools.join(","));
|
|
769
779
|
}
|
|
770
780
|
if (toolPlan.disableAmbientExtensions) {
|
|
771
781
|
args.push("--no-extensions");
|
|
@@ -836,6 +846,11 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
836
846
|
);
|
|
837
847
|
env[RUNTIME_EXTENSION_ACK_PATH_ENV] = runtimeAcknowledgedExtensionsPath;
|
|
838
848
|
let toolDiagnosticPath: string | undefined;
|
|
849
|
+
// Child launch environments are merged over process.env. Explicitly clear
|
|
850
|
+
// parent-scoped diagnostics so a nested zero-tool child cannot validate
|
|
851
|
+
// against, or overwrite, its parent's required-tool report.
|
|
852
|
+
env[REQUIRED_CHILD_TOOLS_ENV] = undefined;
|
|
853
|
+
env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV] = undefined;
|
|
839
854
|
if (toolPlan.requiredChildTools.length > 0) {
|
|
840
855
|
if (!tempDir)
|
|
841
856
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
@@ -999,10 +1014,13 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
999
1014
|
env[SUBAGENT_CAPABILITY_CEILING_ENV] = encodedCapabilityCeiling;
|
|
1000
1015
|
if (encodedPermissionRules)
|
|
1001
1016
|
env[PERMISSION_POLICY_ENV] = encodedPermissionRules;
|
|
1017
|
+
env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = undefined;
|
|
1018
|
+
env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] = undefined;
|
|
1002
1019
|
if (input.structuredOutput) {
|
|
1003
1020
|
env[STRUCTURED_OUTPUT_CAPTURE_ENV] = input.structuredOutput.outputPath;
|
|
1004
1021
|
env[STRUCTURED_OUTPUT_SCHEMA_ENV] = input.structuredOutput.schemaPath;
|
|
1005
1022
|
if (input.structuredOutput.acceptanceReportPath) env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = input.structuredOutput.acceptanceReportPath;
|
|
1023
|
+
if (input.structuredOutput.acceptanceReportRequired) env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] = "1";
|
|
1006
1024
|
}
|
|
1007
1025
|
if (input.steerInboxDir) {
|
|
1008
1026
|
env[SUBAGENT_STEER_INBOX_ENV] = input.steerInboxDir;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts";
|
|
4
5
|
|
|
5
6
|
export const PI_CODING_AGENT_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
6
7
|
export const PI_SUBAGENT_PI_BINARY_ENV = "PI_SUBAGENT_PI_BINARY";
|
|
@@ -23,9 +24,13 @@ export function findPiPackageRootFromEntry(
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function resolveInstalledPiPackageRoot(): string | undefined {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
try {
|
|
28
|
+
return findPiPackageRootFromEntry(
|
|
29
|
+
fileURLToPath(import.meta.resolve(PI_CODING_AGENT_PACKAGE)),
|
|
30
|
+
);
|
|
31
|
+
} catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export function resolvePiPackageRoot(): string | undefined {
|
|
@@ -58,12 +63,21 @@ interface PiSpawnCommand {
|
|
|
58
63
|
args: string[];
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
interface PiPackageJson {
|
|
67
|
+
name?: unknown;
|
|
68
|
+
bin?: string | Record<string, string>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isNodeScriptPath(filePath: string): boolean {
|
|
72
|
+
return /\.(?:mjs|cjs|js)$/i.test(filePath);
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
function isRunnableNodeScript(
|
|
62
76
|
filePath: string,
|
|
63
77
|
existsSync: (filePath: string) => boolean,
|
|
64
78
|
): boolean {
|
|
65
79
|
if (!existsSync(filePath)) return false;
|
|
66
|
-
return
|
|
80
|
+
return isNodeScriptPath(filePath);
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
function normalizePath(filePath: string): string {
|
|
@@ -75,6 +89,23 @@ function isStandalonePiExecutable(execPath: string): boolean {
|
|
|
75
89
|
return /^pi(?:\.exe)?$/i.test(executableName ?? "");
|
|
76
90
|
}
|
|
77
91
|
|
|
92
|
+
function resolvePiCliScriptFromPackageJson(
|
|
93
|
+
packageJsonPath: string,
|
|
94
|
+
readFileSync: (filePath: string, encoding: "utf-8") => string,
|
|
95
|
+
existsSync: (filePath: string) => boolean,
|
|
96
|
+
): string | undefined {
|
|
97
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as PiPackageJson;
|
|
98
|
+
if (packageJson.name !== PI_CODING_AGENT_PACKAGE) return undefined;
|
|
99
|
+
const binField = packageJson.bin;
|
|
100
|
+
const binPath =
|
|
101
|
+
typeof binField === "string"
|
|
102
|
+
? binField
|
|
103
|
+
: (binField?.pi ?? Object.values(binField ?? {})[0]);
|
|
104
|
+
if (!binPath) return undefined;
|
|
105
|
+
const candidate = path.resolve(path.dirname(packageJsonPath), binPath);
|
|
106
|
+
return isRunnableNodeScript(candidate, existsSync) ? candidate : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
78
109
|
export function resolvePiCliScript(
|
|
79
110
|
deps: PiSpawnDeps = {},
|
|
80
111
|
): string | undefined {
|
|
@@ -84,6 +115,7 @@ export function resolvePiCliScript(
|
|
|
84
115
|
deps.readFileSync ??
|
|
85
116
|
((filePath, encoding) => fs.readFileSync(filePath, encoding));
|
|
86
117
|
const argv1 = deps.argv1 ?? process.argv[1];
|
|
118
|
+
const env = deps.env ?? process.env;
|
|
87
119
|
|
|
88
120
|
if (argv1) {
|
|
89
121
|
const argvPath = normalizePath(argv1);
|
|
@@ -99,38 +131,28 @@ export function resolvePiCliScript(
|
|
|
99
131
|
}
|
|
100
132
|
}
|
|
101
133
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
typeof binField === "string"
|
|
124
|
-
? binField
|
|
125
|
-
: (binField?.pi ?? Object.values(binField ?? {})[0]);
|
|
126
|
-
if (!binPath) return undefined;
|
|
127
|
-
const candidate = path.resolve(path.dirname(packageJsonPath), binPath);
|
|
128
|
-
if (isRunnableNodeScript(candidate, existsSync)) {
|
|
129
|
-
return candidate;
|
|
134
|
+
const packageJsonCandidates: Array<() => string | undefined> = [];
|
|
135
|
+
if (deps.resolvePackageJson) packageJsonCandidates.push(deps.resolvePackageJson);
|
|
136
|
+
for (const root of [deps.piPackageRoot, env[PI_CODING_AGENT_PACKAGE_ROOT_ENV], resolvePiPackageRoot()]) {
|
|
137
|
+
const trimmed = root?.trim();
|
|
138
|
+
if (trimmed) packageJsonCandidates.push(() => path.join(trimmed, "package.json"));
|
|
139
|
+
}
|
|
140
|
+
packageJsonCandidates.push(() => {
|
|
141
|
+
const packageRoot = deps.resolvePackageEntry
|
|
142
|
+
? findPiPackageRootFromEntry(deps.resolvePackageEntry())
|
|
143
|
+
: resolveInstalledPiPackageRoot();
|
|
144
|
+
return packageRoot ? path.join(packageRoot, "package.json") : undefined;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
for (const candidatePackageJson of packageJsonCandidates) {
|
|
148
|
+
try {
|
|
149
|
+
const packageJsonPath = candidatePackageJson();
|
|
150
|
+
if (!packageJsonPath) continue;
|
|
151
|
+
const candidate = resolvePiCliScriptFromPackageJson(packageJsonPath, readFileSync, existsSync);
|
|
152
|
+
if (candidate) return candidate;
|
|
153
|
+
} catch {
|
|
154
|
+
// Keep resolving; callers decide whether a PATH fallback is safe.
|
|
130
155
|
}
|
|
131
|
-
} catch {
|
|
132
|
-
// Verified CLI resolution is optional; falling back to `pi` lets PATH handle execution.
|
|
133
|
-
return undefined;
|
|
134
156
|
}
|
|
135
157
|
|
|
136
158
|
return undefined;
|
|
@@ -140,9 +162,16 @@ export function getPiSpawnCommand(
|
|
|
140
162
|
args: string[],
|
|
141
163
|
deps: PiSpawnDeps = {},
|
|
142
164
|
): PiSpawnCommand {
|
|
165
|
+
const platform = deps.platform ?? process.platform;
|
|
143
166
|
const env = deps.env ?? process.env;
|
|
144
167
|
const piBinary = env[PI_SUBAGENT_PI_BINARY_ENV]?.trim();
|
|
145
168
|
if (piBinary) {
|
|
169
|
+
if (platform === "win32" && isNodeScriptPath(piBinary)) {
|
|
170
|
+
return {
|
|
171
|
+
command: deps.execPath ?? process.execPath,
|
|
172
|
+
args: [piBinary, ...args],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
146
175
|
return { command: piBinary, args };
|
|
147
176
|
}
|
|
148
177
|
|
|
@@ -158,6 +187,11 @@ export function getPiSpawnCommand(
|
|
|
158
187
|
args: [piCliPath, ...args],
|
|
159
188
|
};
|
|
160
189
|
}
|
|
190
|
+
if (platform === "win32") {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Could not resolve the Pi CLI on Windows. Set ${PI_SUBAGENT_PI_BINARY_ENV} or ensure ${PI_CODING_AGENT_PACKAGE} is installed.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
161
195
|
|
|
162
196
|
return { command: "pi", args };
|
|
163
197
|
}
|
|
@@ -5,17 +5,21 @@ import * as path from "node:path";
|
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts";
|
|
7
7
|
import type { JsonSchemaObject } from "../../shared/types.ts";
|
|
8
|
+
import type { ResolvedAcceptanceReportMode } from "./acceptance.ts";
|
|
8
9
|
|
|
9
10
|
export const STRUCTURED_OUTPUT_SCHEMA_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA";
|
|
10
11
|
export const STRUCTURED_OUTPUT_CAPTURE_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE";
|
|
11
12
|
export const STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE";
|
|
13
|
+
export const STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED";
|
|
12
14
|
export const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output.";
|
|
15
|
+
export const MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR = "Missing acceptanceReport in structured_output call; acceptance.report is \"on\".";
|
|
13
16
|
|
|
14
17
|
export interface StructuredOutputRuntime {
|
|
15
18
|
schema: JsonSchemaObject;
|
|
16
19
|
schemaPath: string;
|
|
17
20
|
outputPath: string;
|
|
18
21
|
acceptanceReportPath?: string;
|
|
22
|
+
acceptanceReportRequired?: boolean;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
const SCHEMA_MAP_KEYWORDS = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"] as const;
|
|
@@ -61,14 +65,14 @@ function rewriteLocalJsonPointerRefs(schema: unknown, pointerPrefix: string, inh
|
|
|
61
65
|
return rewritten;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
export function createStructuredOutputToolParameters(schema: JsonSchemaObject, options: { acceptanceReport?:
|
|
68
|
+
export function createStructuredOutputToolParameters(schema: JsonSchemaObject, options: { acceptanceReport?: "optional" | "required" } = {}): JsonSchemaObject {
|
|
65
69
|
return {
|
|
66
70
|
type: "object",
|
|
67
71
|
properties: {
|
|
68
72
|
value: rewriteLocalJsonPointerRefs(schema, "#/properties/value"),
|
|
69
73
|
...(options.acceptanceReport ? { acceptanceReport: { type: "object" } } : {}),
|
|
70
74
|
},
|
|
71
|
-
required: ["value"],
|
|
75
|
+
required: ["value", ...(options.acceptanceReport === "required" ? ["acceptanceReport"] : [])],
|
|
72
76
|
additionalProperties: false,
|
|
73
77
|
};
|
|
74
78
|
}
|
|
@@ -129,7 +133,7 @@ export function assertJsonSchemaObject(schema: unknown, label = "outputSchema"):
|
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?: string, options: {
|
|
136
|
+
export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?: string, options: { acceptanceReport?: ResolvedAcceptanceReportMode } = {}): StructuredOutputRuntime {
|
|
133
137
|
assertJsonSchemaObject(schema);
|
|
134
138
|
const rootDir = baseDir ?? os.tmpdir();
|
|
135
139
|
fs.mkdirSync(rootDir, { recursive: true });
|
|
@@ -137,7 +141,14 @@ export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?
|
|
|
137
141
|
const schemaPath = path.join(dir, "schema.json");
|
|
138
142
|
const outputPath = path.join(dir, "output.json");
|
|
139
143
|
fs.writeFileSync(schemaPath, JSON.stringify(schema), { mode: 0o600 });
|
|
140
|
-
return {
|
|
144
|
+
return {
|
|
145
|
+
schema,
|
|
146
|
+
schemaPath,
|
|
147
|
+
outputPath,
|
|
148
|
+
...(options.acceptanceReport && options.acceptanceReport !== "off"
|
|
149
|
+
? { acceptanceReportPath: path.join(dir, "acceptance-report.json"), acceptanceReportRequired: options.acceptanceReport === "required" }
|
|
150
|
+
: {}),
|
|
151
|
+
};
|
|
141
152
|
}
|
|
142
153
|
|
|
143
154
|
export async function validateStructuredOutputValue(schema: JsonSchemaObject, value: unknown): Promise<{ status: "valid" } | { status: "invalid"; message: string }> {
|
|
@@ -178,14 +189,30 @@ export async function readStructuredOutput(runtime: StructuredOutputRuntime): Pr
|
|
|
178
189
|
}
|
|
179
190
|
|
|
180
191
|
export function readStructuredOutputAcceptanceReport(runtime: StructuredOutputRuntime): { value?: unknown; error?: string } {
|
|
181
|
-
if (!runtime.acceptanceReportPath
|
|
192
|
+
if (!runtime.acceptanceReportPath) return {};
|
|
193
|
+
if (!fs.existsSync(runtime.acceptanceReportPath)) {
|
|
194
|
+
return runtime.acceptanceReportRequired ? { error: MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR } : {};
|
|
195
|
+
}
|
|
182
196
|
try {
|
|
183
|
-
return { value: JSON.parse(fs.readFileSync(runtime.acceptanceReportPath, "utf-8")) };
|
|
197
|
+
return { value: JSON.parse(fs.readFileSync(runtime.acceptanceReportPath, "utf-8")) as unknown };
|
|
184
198
|
} catch (error) {
|
|
185
199
|
return { error: `Failed to read structured output acceptance report: ${error instanceof Error ? error.message : String(error)}` };
|
|
186
200
|
}
|
|
187
201
|
}
|
|
188
202
|
|
|
203
|
+
export function clearStructuredOutputCaptures(runtime: StructuredOutputRuntime): string | undefined {
|
|
204
|
+
let cleanupError: string | undefined;
|
|
205
|
+
for (const filePath of [runtime.outputPath, runtime.acceptanceReportPath]) {
|
|
206
|
+
if (!filePath) continue;
|
|
207
|
+
try {
|
|
208
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
cleanupError ??= `Failed to clear stale structured output capture ${filePath}: ${error instanceof Error ? error.message : String(error)}`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return cleanupError;
|
|
214
|
+
}
|
|
215
|
+
|
|
189
216
|
export function cleanupStructuredOutputRuntime(runtime: StructuredOutputRuntime | undefined): void {
|
|
190
217
|
if (!runtime) return;
|
|
191
218
|
try {
|
|
@@ -8,7 +8,8 @@ import { decodePermissionRules, permissionDecision, PERMISSION_AUDIT_PATH_ENV, P
|
|
|
8
8
|
import { consumeSteerRequestsFromDir, MAX_STEER_QUEUE_SIZE, steerAckPathFromDir, writeSteerAckAt, writeSteerCapabilityAt, writeSteerRequestToDir, type SteerDeliveryStatus, type SteerRequest } from "../background/control-channel.ts";
|
|
9
9
|
import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_FORK_CACHE_KEY_ENV, SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
|
|
10
10
|
import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV, isRuntimeAcknowledgedExtensionId, writeRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
|
|
11
|
-
import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
|
|
11
|
+
import { createStructuredOutputToolParameters, MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
|
|
12
|
+
import { validateAcceptanceReport } from "./acceptance.ts";
|
|
12
13
|
import {
|
|
13
14
|
CHILD_TOOL_DIAGNOSTIC_PATH_ENV,
|
|
14
15
|
formatChildToolDiagnostic,
|
|
@@ -729,10 +730,14 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
729
730
|
});
|
|
730
731
|
const structuredOutputPath = process.env[STRUCTURED_OUTPUT_CAPTURE_ENV];
|
|
731
732
|
const structuredAcceptanceReportPath = process.env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV];
|
|
733
|
+
const structuredAcceptanceReportRequired = process.env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] === "1";
|
|
732
734
|
const structuredSchemaPath = process.env[STRUCTURED_OUTPUT_SCHEMA_ENV];
|
|
733
735
|
if (structuredOutputPath && structuredSchemaPath) {
|
|
734
736
|
const schema = JSON.parse(fs.readFileSync(structuredSchemaPath, "utf-8")) as JsonSchemaObject;
|
|
735
|
-
const
|
|
737
|
+
const acceptanceReportMode = structuredAcceptanceReportPath
|
|
738
|
+
? structuredAcceptanceReportRequired ? "required" as const : "optional" as const
|
|
739
|
+
: undefined;
|
|
740
|
+
const parameters = createStructuredOutputToolParameters(schema, { acceptanceReport: acceptanceReportMode });
|
|
736
741
|
const registerTool = pi.registerTool as unknown as (tool: {
|
|
737
742
|
name: string;
|
|
738
743
|
label: string;
|
|
@@ -750,11 +755,23 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
750
755
|
if (validation.status === "invalid") {
|
|
751
756
|
throw new Error(`Structured output validation failed: ${validation.message}`);
|
|
752
757
|
}
|
|
758
|
+
if (structuredAcceptanceReportRequired && params.acceptanceReport === undefined) {
|
|
759
|
+
throw new Error(MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR);
|
|
760
|
+
}
|
|
761
|
+
if (structuredAcceptanceReportRequired && params.acceptanceReport !== undefined) {
|
|
762
|
+
const acceptanceValidation = validateAcceptanceReport(params.acceptanceReport, "acceptanceReport");
|
|
763
|
+
if (!acceptanceValidation.report) {
|
|
764
|
+
throw new Error(`Invalid structured output acceptance report: ${acceptanceValidation.errors.join("; ")}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
753
767
|
fs.mkdirSync(path.dirname(structuredOutputPath), { recursive: true });
|
|
754
|
-
fs.writeFileSync(structuredOutputPath, JSON.stringify(params.value), { mode: 0o600 });
|
|
755
768
|
if (structuredAcceptanceReportPath && params.acceptanceReport !== undefined) {
|
|
769
|
+
fs.mkdirSync(path.dirname(structuredAcceptanceReportPath), { recursive: true });
|
|
756
770
|
fs.writeFileSync(structuredAcceptanceReportPath, JSON.stringify(params.acceptanceReport), { mode: 0o600 });
|
|
771
|
+
} else if (structuredAcceptanceReportPath && fs.existsSync(structuredAcceptanceReportPath)) {
|
|
772
|
+
fs.unlinkSync(structuredAcceptanceReportPath);
|
|
757
773
|
}
|
|
774
|
+
fs.writeFileSync(structuredOutputPath, JSON.stringify(params.value), { mode: 0o600 });
|
|
758
775
|
return {
|
|
759
776
|
content: [{ type: "text", text: "Structured output captured." }],
|
|
760
777
|
details: { path: structuredOutputPath },
|
|
@@ -34,13 +34,23 @@ const REVIEWER_REQUIRED_EDIT_PATTERNS = [
|
|
|
34
34
|
/\bmake\s+(?:the\s+)?code\s+changes\b/i,
|
|
35
35
|
];
|
|
36
36
|
|
|
37
|
-
// The prohibition's object ends at punctuation
|
|
38
|
-
// (but/and/then), so a follow-on clause like "but
|
|
39
|
-
// the text for write-intent testing instead of
|
|
40
|
-
|
|
37
|
+
// The prohibition's object ends at punctuation, a serialized line separator,
|
|
38
|
+
// or a coordinating word (but/and/then), so a follow-on clause like "but
|
|
39
|
+
// implement the fix" stays in the text for write-intent testing instead of
|
|
40
|
+
// being swallowed as the object.
|
|
41
|
+
// Accept serialized line separators too: workflow prompts can carry literal
|
|
42
|
+
// `\\n`/`\\r\\n` between clauses instead of decoded newlines.
|
|
43
|
+
const NO_EDIT_PROHIBITION_PATTERN = /(?:\b|\\(?:r\\n|n))(?:do not|don't|must not)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b((?:(?!\b(?:but|and|then)\b|\\(?:r\\n|n))[^.;,:!?\n–—-])*)/gi;
|
|
44
|
+
const COORDINATED_NO_EDIT_PROHIBITION_PATTERN = /(?:\b|\\(?:r\\n|n))(?:do not|don't|must not)\s+((?=(?:(?!\\(?:r\\n|n))[^.;:!?\n–—-])*\b(?:and|or)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b)(?:(?!\\(?:r\\n|n))[^.;:!?\n–—-])*?\b(?:and|or)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b(?:(?!\b(?:but|and|then)\b|\\(?:r\\n|n))[^.;,:!?\n–—-])*)/gi;
|
|
41
45
|
|
|
42
46
|
/** Objects of a no-edit prohibition that mean "the codebase in general" rather than a named scope. */
|
|
43
47
|
const GENERIC_PROHIBITION_OBJECT = /^\s*(?:(?:any|all|the|these|those|your|our|existing|project|product|source|sources|config|configs|repo|repository)[\s/,-]*)*(?:files?|code|codebase|sources?|anything|repo(?:sitory)?)?\s*$/i;
|
|
48
|
+
const SCOPED_PROHIBITION_CONTINUATION = /^(?:\\(?:r\\n|n)|\r?\n)\s*(?:in|inside|under|within|outside|except|other\s+than|besides)\b/i;
|
|
49
|
+
const OUTPUT_INSTRUCTION_CONTINUATION = /^(?:\\(?:r\\n|n)|\r?\n)\s*(?:in|inside|within)\s+(?:(?:(?:your|the)\s+)(?:final\s+)?|final\s+)(?:output|response|report|answer|reply|message|summary|findings?|analysis|recommendations?)(?:\s*\/\s*(?:output|response|report|answer|reply|message|summary|findings?|analysis|recommendations?))*\b(?![\\/])/i;
|
|
50
|
+
|
|
51
|
+
function hasScopedProhibitionContinuation(text: string): boolean {
|
|
52
|
+
return SCOPED_PROHIBITION_CONTINUATION.test(text) && !OUTPUT_INSTRUCTION_CONTINUATION.test(text);
|
|
53
|
+
}
|
|
44
54
|
|
|
45
55
|
const SCOPED_NO_EDIT_CONSTRAINT_PATTERNS = [
|
|
46
56
|
/\bdo not edit files?\s+outside\b/i,
|
|
@@ -135,11 +145,15 @@ function analyzeNoEditProhibitions(taskText: string): NoEditProhibitionAnalysis
|
|
|
135
145
|
|| NO_TOOL_INTENT_PATTERNS.some((pattern) => pattern.test(taskText));
|
|
136
146
|
let blanket = present;
|
|
137
147
|
let strippedText = stripPatterns(taskText, [...REVIEW_ONLY_PATTERNS, ...NO_TOOL_INTENT_PATTERNS]);
|
|
138
|
-
|
|
148
|
+
const stripNoEditProhibition = (match: string, object: string, offset: number, source: string): string => {
|
|
139
149
|
present = true;
|
|
140
|
-
if (GENERIC_PROHIBITION_OBJECT.test(object)) blanket = true;
|
|
150
|
+
if (GENERIC_PROHIBITION_OBJECT.test(object) && !hasScopedProhibitionContinuation(source.slice(offset + match.length))) blanket = true;
|
|
141
151
|
return " ";
|
|
142
|
-
}
|
|
152
|
+
};
|
|
153
|
+
strippedText = strippedText.replace(new RegExp(COORDINATED_NO_EDIT_PROHIBITION_PATTERN.source, COORDINATED_NO_EDIT_PROHIBITION_PATTERN.flags), stripNoEditProhibition);
|
|
154
|
+
strippedText = strippedText.replace(new RegExp(NO_EDIT_PROHIBITION_PATTERN.source, NO_EDIT_PROHIBITION_PATTERN.flags), stripNoEditProhibition);
|
|
155
|
+
// Restore boundaries after stripping a prohibition from serialized prompts.
|
|
156
|
+
strippedText = strippedText.replace(/\\(?:r\\n|n)/g, "\n");
|
|
143
157
|
return { present, blanket, strippedText };
|
|
144
158
|
}
|
|
145
159
|
|
|
@@ -16,7 +16,7 @@ export const DEFAULT_FAST_TOOL_TIMEOUT_TOOLS = new Set([
|
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
18
|
/** Tools whose normal job can be to wait for a person or another run. */
|
|
19
|
-
export const TOOL_TIMEOUT_EXEMPT_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait"
|
|
19
|
+
export const TOOL_TIMEOUT_EXEMPT_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait"]);
|
|
20
20
|
|
|
21
21
|
// Backward-compatible export name for existing callers/tests.
|
|
22
22
|
export const TOOL_TIMEOUT_ALLOWLIST = TOOL_TIMEOUT_EXEMPT_TOOLS;
|