pi-subagents 0.35.1 → 0.37.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 +71 -0
- package/README.md +224 -31
- package/agents/advisor.md +73 -0
- package/agents/delegate.md +2 -0
- package/agents/worker.md +2 -0
- package/package.json +11 -13
- package/skills/pi-subagents/SKILL.md +42 -13
- package/src/agents/agent-management.ts +7 -1
- package/src/agents/agents.ts +121 -17
- package/src/api/capability-ceiling.ts +17 -0
- package/src/api/delegation.ts +127 -0
- package/src/api/preflight.ts +399 -0
- package/src/extension/config.ts +7 -1
- package/src/extension/index.ts +52 -38
- package/src/extension/rpc.ts +31 -2
- package/src/extension/schemas.ts +29 -3
- package/src/extension/tool-description.ts +4 -2
- package/src/intercom/intercom-bridge.ts +1 -1
- package/src/intercom/native-supervisor-channel.ts +45 -6
- package/src/intercom/result-intercom.ts +7 -0
- package/src/runs/background/async-execution.ts +158 -17
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +47 -8
- package/src/runs/background/async-status.ts +94 -3
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/completion-batcher.ts +6 -4
- package/src/runs/background/completion-dedupe.ts +2 -11
- package/src/runs/background/fleet-view.ts +9 -4
- package/src/runs/background/notify.ts +132 -120
- package/src/runs/background/process-terminal.ts +280 -0
- package/src/runs/background/result-watcher.ts +138 -78
- package/src/runs/background/run-status.ts +10 -2
- package/src/runs/background/scheduled-runs.ts +6 -1
- package/src/runs/background/stale-run-reconciler.ts +6 -0
- package/src/runs/background/subagent-runner.ts +406 -54
- package/src/runs/background/subagent-wait.ts +130 -4
- package/src/runs/background/wait-tool.ts +2 -2
- package/src/runs/foreground/chain-execution.ts +181 -111
- package/src/runs/foreground/execution.ts +147 -47
- package/src/runs/foreground/foreground-control.ts +90 -0
- package/src/runs/foreground/subagent-executor.ts +426 -165
- package/src/runs/shared/acceptance.ts +94 -41
- package/src/runs/shared/agent-contract.ts +38 -0
- package/src/runs/shared/capability-ceiling.ts +177 -0
- package/src/runs/shared/child-protocol.ts +1 -1
- package/src/runs/shared/completion-guard.ts +36 -5
- package/src/runs/shared/context-mode.ts +44 -0
- package/src/runs/shared/dynamic-fanout.ts +5 -5
- package/src/runs/shared/long-running-guard.ts +4 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
- package/src/runs/shared/nested-events.ts +35 -3
- package/src/runs/shared/parallel-handoff.ts +154 -0
- package/src/runs/shared/parallel-utils.ts +11 -0
- package/src/runs/shared/pi-args.ts +148 -56
- package/src/runs/shared/run-history.ts +90 -5
- package/src/runs/shared/session-lease.ts +25 -5
- package/src/runs/shared/structured-output.ts +112 -7
- package/src/runs/shared/subagent-control.ts +4 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +31 -19
- package/src/runs/shared/task-intent.ts +10 -5
- package/src/runs/shared/tool-availability.ts +21 -3
- package/src/runs/shared/tool-budget.ts +11 -5
- package/src/runs/shared/turn-budget.ts +2 -1
- package/src/runs/shared/worktree.ts +63 -14
- package/src/shared/accessible-dir.ts +25 -0
- package/src/shared/artifacts.ts +37 -7
- package/src/shared/atomic-json.ts +14 -42
- package/src/shared/child-transcript.ts +52 -0
- package/src/shared/file-system-retry.ts +47 -0
- package/src/shared/launch-contract.ts +123 -0
- package/src/shared/settings.ts +9 -1
- package/src/shared/types.ts +314 -28
- package/src/shared/utils.ts +17 -42
- package/src/slash/delegation-adapters.ts +153 -6
- package/src/slash/delegation-json.ts +108 -0
- package/src/slash/delegation-request.ts +182 -36
- package/src/slash/prompt-template-bridge.ts +222 -37
- package/src/slash/selector.ts +147 -0
- package/src/slash/slash-commands.ts +15 -6
- package/src/slash/slash-live-state.ts +2 -2
- package/src/slash/subagents-admin.ts +42 -42
- package/src/tui/fleet-status.ts +362 -0
- package/src/tui/fleet-transcript.ts +472 -0
- package/src/tui/fleet.ts +318 -59
- package/src/tui/render.ts +17 -15
- package/src/watchdog/change-signature.ts +105 -12
- package/src/watchdog/review.ts +7 -2
- package/src/watchdog/runtime.ts +5 -3
- package/src/slash/subagents-editor.ts +0 -86
|
@@ -3,14 +3,17 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { encodeNestedPathEnv, parseNestedPathEnv, type NestedPathEntry } from "./nested-path.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveMcpDirectToolSelections, type ResolvedMcpDirectToolSelection } from "./mcp-direct-tool-allowlist.ts";
|
|
7
|
+
import { resolvePiPackageRoot } from "./pi-spawn.ts";
|
|
7
8
|
import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV } from "./structured-output.ts";
|
|
8
9
|
import { TEMP_ROOT_DIR, type JsonSchemaObject, type ResolvedToolBudget } from "../../shared/types.ts";
|
|
9
10
|
import { THINKING_LEVELS } from "../../shared/model-info.ts";
|
|
10
|
-
import { TOOL_BUDGET_ENV, encodeToolBudgetEnv } from "./tool-budget.ts";
|
|
11
|
-
import { CHILD_TOOL_DIAGNOSTIC_PATH_ENV, REQUIRED_CHILD_TOOLS_ENV } from "./tool-availability.ts";
|
|
11
|
+
import { TOOL_BUDGET_ENV, TOOL_BUDGET_ZERO_AUTH_ENV, encodeToolBudgetEnv } from "./tool-budget.ts";
|
|
12
|
+
import { CHILD_TOOL_DIAGNOSTIC_PATH_ENV, MCP_DIRECT_CHILD_TOOLS_ENV, REQUIRED_CHILD_TOOLS_ENV } from "./tool-availability.ts";
|
|
12
13
|
import { CHILD_WATCHDOG_CONFIG_ENV, encodeChildWatchdogConfig, type ChildWatchdogConfig } from "../../watchdog/child-status.ts";
|
|
13
14
|
import { WAIT_TOOL_ENABLED_ENV } from "../background/wait-config.ts";
|
|
15
|
+
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts";
|
|
16
|
+
import { SUBAGENT_CAPABILITY_CEILING_ENV, decodeSubagentCapabilityCeiling, encodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, type ResolvedSubagentCapabilityCeiling, type SubagentCapabilityAudit } from "./capability-ceiling.ts";
|
|
14
17
|
|
|
15
18
|
const TASK_ARG_LIMIT = 8000;
|
|
16
19
|
const PROMPT_RUNTIME_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-prompt-runtime.ts");
|
|
@@ -36,7 +39,7 @@ export const SUBAGENT_STEER_INBOX_ENV = "PI_SUBAGENT_STEER_INBOX";
|
|
|
36
39
|
export const SUBAGENT_STEER_CAPABILITY_ENV = "PI_SUBAGENT_STEER_CAPABILITY";
|
|
37
40
|
export const SUBAGENT_STEER_ACK_DIR_ENV = "PI_SUBAGENT_STEER_ACK_DIR";
|
|
38
41
|
|
|
39
|
-
interface BuildPiArgsInput {
|
|
42
|
+
export interface BuildPiArgsInput {
|
|
40
43
|
parentSessionId?: string;
|
|
41
44
|
baseArgs: string[];
|
|
42
45
|
task: string;
|
|
@@ -78,15 +81,18 @@ interface BuildPiArgsInput {
|
|
|
78
81
|
outputPath: string;
|
|
79
82
|
};
|
|
80
83
|
toolBudget?: ResolvedToolBudget;
|
|
84
|
+
allowZeroToolBudget?: boolean;
|
|
81
85
|
childWatchdog?: ChildWatchdogConfig;
|
|
82
86
|
waitToolEnabled?: boolean;
|
|
87
|
+
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
interface BuildPiArgsResult {
|
|
90
|
+
export interface BuildPiArgsResult {
|
|
86
91
|
args: string[];
|
|
87
92
|
env: Record<string, string | undefined>;
|
|
88
93
|
tempDir?: string;
|
|
89
94
|
toolDiagnosticPath?: string;
|
|
95
|
+
capabilityAudit?: SubagentCapabilityAudit;
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
function sanitizeSupervisorChannelSegment(value: string): string {
|
|
@@ -106,6 +112,106 @@ export function applyThinkingSuffix(model: string | undefined, thinking: string
|
|
|
106
112
|
return `${model}:${thinking}`;
|
|
107
113
|
}
|
|
108
114
|
|
|
115
|
+
export interface ResolvePiLaunchToolPlanInput {
|
|
116
|
+
tools?: string[];
|
|
117
|
+
extensions?: string[];
|
|
118
|
+
subagentOnlyExtensions?: string[];
|
|
119
|
+
mcpDirectTools?: string[];
|
|
120
|
+
cwd?: string;
|
|
121
|
+
requireReadTool?: boolean;
|
|
122
|
+
structuredOutput?: boolean;
|
|
123
|
+
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
124
|
+
inheritedCapabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface PiLaunchToolPlan {
|
|
128
|
+
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
129
|
+
requestedBuiltinTools: string[];
|
|
130
|
+
declaredBuiltinTools: string[];
|
|
131
|
+
toolExtensionPaths: string[];
|
|
132
|
+
resolvedMcpSelections: ResolvedMcpDirectToolSelection[];
|
|
133
|
+
effectiveMcpSelections: ResolvedMcpDirectToolSelection[];
|
|
134
|
+
effectiveMcpTools: string[];
|
|
135
|
+
explicitToolAllowlist: boolean;
|
|
136
|
+
internalTools: string[];
|
|
137
|
+
effectiveToolAllowlist: string[];
|
|
138
|
+
requiredChildTools: string[];
|
|
139
|
+
fanoutAuthorized: boolean;
|
|
140
|
+
runtimeExtensions: string[];
|
|
141
|
+
configuredExtensions: string[];
|
|
142
|
+
extensionArgs: string[];
|
|
143
|
+
disableAmbientExtensions: boolean;
|
|
144
|
+
capabilityAudit?: SubagentCapabilityAudit;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function resolvePiLaunchToolPlan(input: ResolvePiLaunchToolPlanInput): PiLaunchToolPlan {
|
|
148
|
+
const capabilityCeiling = intersectSubagentCapabilityCeilings(input.capabilityCeiling, input.inheritedCapabilityCeiling);
|
|
149
|
+
const allowedToolSet = capabilityCeiling?.allowedTools === undefined ? undefined : new Set(capabilityCeiling.allowedTools);
|
|
150
|
+
const requestedBuiltinTools = input.tools?.filter((tool) => !(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) ?? [];
|
|
151
|
+
if (input.requireReadTool && allowedToolSet && !allowedToolSet.has("read")) {
|
|
152
|
+
throw new Error(`Capability ceiling from ${capabilityCeiling?.sources.join(", ") || "unknown source"} excludes required tool 'read' for lazy skill loading.`);
|
|
153
|
+
}
|
|
154
|
+
const declaredBuiltinTools = input.tools === undefined
|
|
155
|
+
? (allowedToolSet ? [...allowedToolSet] : [])
|
|
156
|
+
: (input.requireReadTool && requestedBuiltinTools.length > 0 && !requestedBuiltinTools.includes("read") && !allowedToolSet
|
|
157
|
+
? ["read", ...requestedBuiltinTools]
|
|
158
|
+
: requestedBuiltinTools).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
|
|
159
|
+
const fanoutAuthorized = declaredBuiltinTools.includes("subagent");
|
|
160
|
+
const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions ? [] : (input.tools ?? []).filter((tool) => !requestedBuiltinTools.includes(tool) && (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")));
|
|
161
|
+
const resolvedMcpSelections = capabilityCeiling?.denyExtensions ? [] : resolveMcpDirectToolSelections(input.mcpDirectTools, input.cwd);
|
|
162
|
+
const effectiveMcpSelections = resolvedMcpSelections.filter((selection) => !allowedToolSet || allowedToolSet.has(selection.name));
|
|
163
|
+
const effectiveMcpTools = effectiveMcpSelections.map((selection) => selection.name);
|
|
164
|
+
const explicitToolAllowlist = input.tools !== undefined || (input.mcpDirectTools?.length ?? 0) > 0 || allowedToolSet !== undefined;
|
|
165
|
+
const internalTools = input.structuredOutput ? ["structured_output"] : [];
|
|
166
|
+
const effectiveToolAllowlist = [...new Set([...declaredBuiltinTools, ...effectiveMcpTools, ...internalTools])];
|
|
167
|
+
const requiredChildTools = explicitToolAllowlist ? [...new Set([
|
|
168
|
+
...(input.tools !== undefined ? declaredBuiltinTools : []),
|
|
169
|
+
...(input.mcpDirectTools?.length ? effectiveMcpTools : []),
|
|
170
|
+
...internalTools,
|
|
171
|
+
])] : [];
|
|
172
|
+
const runtimeExtensions = fanoutAuthorized
|
|
173
|
+
? [PROMPT_RUNTIME_EXTENSION_PATH, FANOUT_CHILD_EXTENSION_PATH]
|
|
174
|
+
: [PROMPT_RUNTIME_EXTENSION_PATH];
|
|
175
|
+
const disableAmbientExtensions = capabilityCeiling?.denyExtensions === true || input.extensions !== undefined;
|
|
176
|
+
const configuredExtensions = capabilityCeiling?.denyExtensions ? [] : [...toolExtensionPaths, ...(input.extensions ?? []), ...(input.subagentOnlyExtensions ?? [])];
|
|
177
|
+
const extensionArgs = disableAmbientExtensions
|
|
178
|
+
? [...new Set([...runtimeExtensions, ...configuredExtensions])]
|
|
179
|
+
: [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...(input.subagentOnlyExtensions ?? [])])];
|
|
180
|
+
const requestedToolNames = input.tools !== undefined
|
|
181
|
+
? [...new Set([...requestedBuiltinTools, ...resolvedMcpSelections.map((selection) => selection.name)])]
|
|
182
|
+
: undefined;
|
|
183
|
+
const capabilityAudit = capabilityCeiling ? {
|
|
184
|
+
ceiling: capabilityCeiling,
|
|
185
|
+
...(requestedToolNames ? { requestedTools: requestedToolNames } : {}),
|
|
186
|
+
effectiveTools: effectiveToolAllowlist,
|
|
187
|
+
removedTools: requestedToolNames?.filter((tool) => !effectiveToolAllowlist.includes(tool)) ?? [],
|
|
188
|
+
internalTools,
|
|
189
|
+
extensionsDenied: capabilityCeiling.denyExtensions,
|
|
190
|
+
removedExtensionCount: capabilityCeiling.denyExtensions ? (input.extensions?.length ?? 0) + (input.subagentOnlyExtensions?.length ?? 0) + ((input.tools ?? []).filter((tool) => tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")).length) : 0,
|
|
191
|
+
requestedMcpToolCount: input.mcpDirectTools?.length ?? 0,
|
|
192
|
+
effectiveMcpTools,
|
|
193
|
+
} satisfies SubagentCapabilityAudit : undefined;
|
|
194
|
+
return {
|
|
195
|
+
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
196
|
+
requestedBuiltinTools,
|
|
197
|
+
declaredBuiltinTools,
|
|
198
|
+
toolExtensionPaths,
|
|
199
|
+
resolvedMcpSelections,
|
|
200
|
+
effectiveMcpSelections,
|
|
201
|
+
effectiveMcpTools,
|
|
202
|
+
explicitToolAllowlist,
|
|
203
|
+
internalTools,
|
|
204
|
+
effectiveToolAllowlist,
|
|
205
|
+
requiredChildTools,
|
|
206
|
+
fanoutAuthorized,
|
|
207
|
+
runtimeExtensions,
|
|
208
|
+
configuredExtensions,
|
|
209
|
+
extensionArgs,
|
|
210
|
+
disableAmbientExtensions,
|
|
211
|
+
...(capabilityAudit ? { capabilityAudit } : {}),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
109
215
|
export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
110
216
|
const args = [...input.baseArgs];
|
|
111
217
|
|
|
@@ -127,43 +233,25 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
127
233
|
args.push("--model", modelArg);
|
|
128
234
|
}
|
|
129
235
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (allowedTools.length > 0) {
|
|
145
|
-
if (input.mcpDirectTools?.length) {
|
|
146
|
-
allowedTools.push(...resolveMcpDirectToolNames(input.mcpDirectTools, input.cwd));
|
|
147
|
-
}
|
|
148
|
-
if (input.structuredOutput) allowedTools.push("structured_output");
|
|
149
|
-
requiredChildTools = [...new Set(allowedTools)];
|
|
150
|
-
args.push("--tools", requiredChildTools.join(","));
|
|
151
|
-
}
|
|
236
|
+
const toolPlan = resolvePiLaunchToolPlan({
|
|
237
|
+
tools: input.tools,
|
|
238
|
+
extensions: input.extensions,
|
|
239
|
+
subagentOnlyExtensions: input.subagentOnlyExtensions,
|
|
240
|
+
mcpDirectTools: input.mcpDirectTools,
|
|
241
|
+
cwd: input.cwd,
|
|
242
|
+
requireReadTool: input.requireReadTool,
|
|
243
|
+
structuredOutput: input.structuredOutput,
|
|
244
|
+
capabilityCeiling: input.capabilityCeiling,
|
|
245
|
+
inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
|
|
246
|
+
});
|
|
247
|
+
if (toolPlan.explicitToolAllowlist) {
|
|
248
|
+
args.push(toolPlan.effectiveToolAllowlist.length > 0 ? "--tools" : "--no-tools");
|
|
249
|
+
if (toolPlan.effectiveToolAllowlist.length > 0) args.push(toolPlan.effectiveToolAllowlist.join(","));
|
|
152
250
|
}
|
|
153
|
-
|
|
154
|
-
const runtimeExtensions = fanoutAuthorized
|
|
155
|
-
? [PROMPT_RUNTIME_EXTENSION_PATH, FANOUT_CHILD_EXTENSION_PATH]
|
|
156
|
-
: [PROMPT_RUNTIME_EXTENSION_PATH];
|
|
157
|
-
if (input.extensions !== undefined) {
|
|
251
|
+
if (toolPlan.disableAmbientExtensions) {
|
|
158
252
|
args.push("--no-extensions");
|
|
159
|
-
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...input.extensions, ...(input.subagentOnlyExtensions ?? [])])]) {
|
|
160
|
-
args.push("--extension", extPath);
|
|
161
|
-
}
|
|
162
|
-
} else {
|
|
163
|
-
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...(input.subagentOnlyExtensions ?? [])])]) {
|
|
164
|
-
args.push("--extension", extPath);
|
|
165
|
-
}
|
|
166
253
|
}
|
|
254
|
+
for (const extPath of toolPlan.extensionArgs) args.push("--extension", extPath);
|
|
167
255
|
|
|
168
256
|
if (!input.inheritSkills) {
|
|
169
257
|
args.push("--no-skills");
|
|
@@ -190,15 +278,18 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
190
278
|
}
|
|
191
279
|
|
|
192
280
|
const env: Record<string, string | undefined> = {};
|
|
281
|
+
const piPackageRoot = process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] ?? resolvePiPackageRoot();
|
|
282
|
+
if (piPackageRoot) env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] = piPackageRoot;
|
|
193
283
|
let toolDiagnosticPath: string | undefined;
|
|
194
|
-
if (requiredChildTools.length > 0) {
|
|
284
|
+
if (toolPlan.requiredChildTools.length > 0) {
|
|
195
285
|
if (!tempDir) tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
196
286
|
toolDiagnosticPath = path.join(tempDir, "tool-diagnostic.json");
|
|
197
|
-
env[REQUIRED_CHILD_TOOLS_ENV] = JSON.stringify(requiredChildTools);
|
|
287
|
+
env[REQUIRED_CHILD_TOOLS_ENV] = JSON.stringify(toolPlan.requiredChildTools);
|
|
198
288
|
env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV] = toolDiagnosticPath;
|
|
199
289
|
}
|
|
290
|
+
env[MCP_DIRECT_CHILD_TOOLS_ENV] = toolPlan.effectiveMcpTools.length > 0 ? JSON.stringify(toolPlan.effectiveMcpTools) : undefined;
|
|
200
291
|
env[SUBAGENT_CHILD_ENV] = "1";
|
|
201
|
-
env[SUBAGENT_FANOUT_CHILD_ENV] = fanoutAuthorized ? "1" : "0";
|
|
292
|
+
env[SUBAGENT_FANOUT_CHILD_ENV] = toolPlan.fanoutAuthorized ? "1" : "0";
|
|
202
293
|
if (input.waitToolEnabled !== undefined) {
|
|
203
294
|
env[WAIT_TOOL_ENABLED_ENV] = input.waitToolEnabled ? "true" : "false";
|
|
204
295
|
}
|
|
@@ -219,20 +310,20 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
219
310
|
...(input.childAgentName ? { agent: input.childAgentName } : {}),
|
|
220
311
|
}] : []),
|
|
221
312
|
];
|
|
222
|
-
env[SUBAGENT_PARENT_EVENT_SINK_ENV] = fanoutAuthorized
|
|
313
|
+
env[SUBAGENT_PARENT_EVENT_SINK_ENV] = toolPlan.fanoutAuthorized
|
|
223
314
|
? input.parentEventSink ?? process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] ?? ""
|
|
224
315
|
: "";
|
|
225
|
-
env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] = fanoutAuthorized
|
|
316
|
+
env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] = toolPlan.fanoutAuthorized
|
|
226
317
|
? input.parentControlInbox ?? process.env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] ?? ""
|
|
227
318
|
: "";
|
|
228
|
-
env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] = fanoutAuthorized
|
|
319
|
+
env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] = toolPlan.fanoutAuthorized
|
|
229
320
|
? input.parentRootRunId ?? process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] ?? input.runId ?? ""
|
|
230
321
|
: "";
|
|
231
|
-
env[SUBAGENT_PARENT_RUN_ID_ENV] = fanoutAuthorized ? parentRunId : "";
|
|
232
|
-
env[SUBAGENT_PARENT_CHILD_INDEX_ENV] = fanoutAuthorized ? parentChildIndex : "";
|
|
233
|
-
env[SUBAGENT_PARENT_DEPTH_ENV] = fanoutAuthorized ? String(parentDepth) : "";
|
|
234
|
-
env[SUBAGENT_PARENT_PATH_ENV] = fanoutAuthorized ? encodeNestedPathEnv(parentPath) : "";
|
|
235
|
-
env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] = fanoutAuthorized
|
|
322
|
+
env[SUBAGENT_PARENT_RUN_ID_ENV] = toolPlan.fanoutAuthorized ? parentRunId : "";
|
|
323
|
+
env[SUBAGENT_PARENT_CHILD_INDEX_ENV] = toolPlan.fanoutAuthorized ? parentChildIndex : "";
|
|
324
|
+
env[SUBAGENT_PARENT_DEPTH_ENV] = toolPlan.fanoutAuthorized ? String(parentDepth) : "";
|
|
325
|
+
env[SUBAGENT_PARENT_PATH_ENV] = toolPlan.fanoutAuthorized ? encodeNestedPathEnv(parentPath) : "";
|
|
326
|
+
env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] = toolPlan.fanoutAuthorized
|
|
236
327
|
? input.parentCapabilityToken ?? process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] ?? ""
|
|
237
328
|
: "";
|
|
238
329
|
env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext ? "1" : "0";
|
|
@@ -262,11 +353,11 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
262
353
|
if (input.childIndex !== undefined) {
|
|
263
354
|
env[SUBAGENT_CHILD_INDEX_ENV] = String(input.childIndex);
|
|
264
355
|
}
|
|
265
|
-
if (input.mcpDirectTools?.length)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
356
|
+
if (!toolPlan.capabilityCeiling && input.mcpDirectTools?.length) env.MCP_DIRECT_TOOLS = input.mcpDirectTools.join(",");
|
|
357
|
+
else if (toolPlan.capabilityCeiling && toolPlan.effectiveMcpSelections.length && !toolPlan.capabilityCeiling.denyExtensions) env.MCP_DIRECT_TOOLS = toolPlan.effectiveMcpSelections.map((selection) => selection.selector).join(",");
|
|
358
|
+
else env.MCP_DIRECT_TOOLS = "__none__";
|
|
359
|
+
const encodedCapabilityCeiling = encodeSubagentCapabilityCeiling(toolPlan.capabilityCeiling);
|
|
360
|
+
if (encodedCapabilityCeiling) env[SUBAGENT_CAPABILITY_CEILING_ENV] = encodedCapabilityCeiling;
|
|
270
361
|
if (input.structuredOutput) {
|
|
271
362
|
env[STRUCTURED_OUTPUT_CAPTURE_ENV] = input.structuredOutput.outputPath;
|
|
272
363
|
env[STRUCTURED_OUTPUT_SCHEMA_ENV] = input.structuredOutput.schemaPath;
|
|
@@ -278,12 +369,13 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
278
369
|
if (input.steerAckDir) env[SUBAGENT_STEER_ACK_DIR_ENV] = input.steerAckDir;
|
|
279
370
|
const encodedToolBudget = encodeToolBudgetEnv(input.toolBudget);
|
|
280
371
|
if (encodedToolBudget) env[TOOL_BUDGET_ENV] = encodedToolBudget;
|
|
372
|
+
env[TOOL_BUDGET_ZERO_AUTH_ENV] = input.allowZeroToolBudget ? "1" : undefined;
|
|
281
373
|
const encodedChildWatchdog = encodeChildWatchdogConfig(input.childWatchdog);
|
|
282
374
|
if (encodedChildWatchdog) env[CHILD_WATCHDOG_CONFIG_ENV] = encodedChildWatchdog;
|
|
283
375
|
|
|
284
376
|
env[SUBAGENT_PARENT_SESSION_ENV] = input.parentSessionId ?? process.env[SUBAGENT_PARENT_SESSION_ENV] ?? "";
|
|
285
377
|
|
|
286
|
-
return { args, env, tempDir, toolDiagnosticPath };
|
|
378
|
+
return { args, env, tempDir, toolDiagnosticPath, capabilityAudit: toolPlan.capabilityAudit };
|
|
287
379
|
}
|
|
288
380
|
|
|
289
381
|
export const parseParentPathEnv = parseNestedPathEnv;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { getAgentDir } from "../../shared/utils.ts";
|
|
@@ -5,6 +6,7 @@ import { getAgentDir } from "../../shared/utils.ts";
|
|
|
5
6
|
export interface RunEntry {
|
|
6
7
|
agent: string;
|
|
7
8
|
task: string;
|
|
9
|
+
taskHash?: string;
|
|
8
10
|
ts: number;
|
|
9
11
|
status: "ok" | "error";
|
|
10
12
|
duration: number;
|
|
@@ -13,24 +15,103 @@ export interface RunEntry {
|
|
|
13
15
|
|
|
14
16
|
const ROTATE_READ_THRESHOLD = 1200;
|
|
15
17
|
const ROTATE_KEEP = 1000;
|
|
18
|
+
const PRIVATE_DIR_MODE = 0o700;
|
|
19
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
20
|
+
const REDACTED_TASK = "[redacted]";
|
|
16
21
|
|
|
17
22
|
function getHistoryPath(): string {
|
|
18
23
|
return path.join(getAgentDir(), "run-history.jsonl");
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
function hashTask(task: string): string {
|
|
27
|
+
return createHash("sha256").update(task).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hardenHistoryStorage(historyPath: string): void {
|
|
31
|
+
const historyDir = path.dirname(historyPath);
|
|
32
|
+
fs.mkdirSync(historyDir, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
33
|
+
try { fs.chmodSync(historyDir, PRIVATE_DIR_MODE); } catch {}
|
|
34
|
+
try { if (fs.existsSync(historyPath)) fs.chmodSync(historyPath, PRIVATE_FILE_MODE); } catch {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sanitizeHistoryLine(line: string): string | undefined {
|
|
38
|
+
let value: unknown;
|
|
39
|
+
try {
|
|
40
|
+
value = JSON.parse(line);
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (!value || typeof value !== "object") return undefined;
|
|
45
|
+
|
|
46
|
+
const record = value as Record<string, unknown>;
|
|
47
|
+
const task = typeof record.task === "string" ? record.task : "";
|
|
48
|
+
const taskHash = typeof record.taskHash === "string" && record.taskHash
|
|
49
|
+
? record.taskHash
|
|
50
|
+
: task && task !== REDACTED_TASK
|
|
51
|
+
? hashTask(task)
|
|
52
|
+
: undefined;
|
|
53
|
+
|
|
54
|
+
return JSON.stringify({
|
|
55
|
+
...record,
|
|
56
|
+
task: REDACTED_TASK,
|
|
57
|
+
...(taskHash ? { taskHash } : {}),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sanitizeHistoryLines(raw: string): { lines: string[]; changed: boolean } {
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
let changed = false;
|
|
64
|
+
for (const line of raw.split("\n")) {
|
|
65
|
+
const trimmed = line.trim();
|
|
66
|
+
if (!trimmed) continue;
|
|
67
|
+
const sanitized = sanitizeHistoryLine(trimmed);
|
|
68
|
+
if (!sanitized) {
|
|
69
|
+
changed = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (sanitized !== trimmed) changed = true;
|
|
73
|
+
lines.push(sanitized);
|
|
74
|
+
}
|
|
75
|
+
return { lines, changed };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function writePrivateHistory(historyPath: string, lines: string[]): void {
|
|
79
|
+
fs.writeFileSync(historyPath, lines.length ? `${lines.join("\n")}\n` : "", { encoding: "utf-8", mode: PRIVATE_FILE_MODE });
|
|
80
|
+
try { fs.chmodSync(historyPath, PRIVATE_FILE_MODE); } catch {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sanitizeHistoryFile(historyPath: string): void {
|
|
84
|
+
if (!fs.existsSync(historyPath)) return;
|
|
85
|
+
const raw = fs.readFileSync(historyPath, "utf-8");
|
|
86
|
+
const { lines, changed } = sanitizeHistoryLines(raw);
|
|
87
|
+
if (changed) writePrivateHistory(historyPath, lines);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function appendPrivateHistoryLine(historyPath: string, line: string): void {
|
|
91
|
+
const fd = fs.openSync(historyPath, fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY, PRIVATE_FILE_MODE);
|
|
92
|
+
try {
|
|
93
|
+
fs.writeSync(fd, `${line}\n`);
|
|
94
|
+
} finally {
|
|
95
|
+
fs.closeSync(fd);
|
|
96
|
+
}
|
|
97
|
+
try { fs.chmodSync(historyPath, PRIVATE_FILE_MODE); } catch {}
|
|
98
|
+
}
|
|
99
|
+
|
|
21
100
|
export function recordRun(agent: string, task: string, exitCode: number, durationMs: number): void {
|
|
22
101
|
try {
|
|
23
102
|
const entry: RunEntry = {
|
|
24
103
|
agent,
|
|
25
|
-
task:
|
|
104
|
+
task: REDACTED_TASK,
|
|
105
|
+
taskHash: hashTask(task),
|
|
26
106
|
ts: Math.floor(Date.now() / 1000),
|
|
27
107
|
status: exitCode === 0 ? "ok" : "error",
|
|
28
108
|
duration: durationMs,
|
|
29
109
|
...(exitCode !== 0 ? { exit: exitCode } : {}),
|
|
30
110
|
};
|
|
31
111
|
const historyPath = getHistoryPath();
|
|
32
|
-
|
|
33
|
-
|
|
112
|
+
hardenHistoryStorage(historyPath);
|
|
113
|
+
try { sanitizeHistoryFile(historyPath); } catch {}
|
|
114
|
+
appendPrivateHistoryLine(historyPath, JSON.stringify(entry));
|
|
34
115
|
} catch {
|
|
35
116
|
// Best-effort — never crash the execution flow for history recording
|
|
36
117
|
}
|
|
@@ -38,6 +119,7 @@ export function recordRun(agent: string, task: string, exitCode: number, duratio
|
|
|
38
119
|
|
|
39
120
|
export function loadRunsForAgent(agent: string): RunEntry[] {
|
|
40
121
|
const historyPath = getHistoryPath();
|
|
122
|
+
try { hardenHistoryStorage(historyPath); } catch {}
|
|
41
123
|
if (!fs.existsSync(historyPath)) return [];
|
|
42
124
|
let raw: string;
|
|
43
125
|
try {
|
|
@@ -46,11 +128,14 @@ export function loadRunsForAgent(agent: string): RunEntry[] {
|
|
|
46
128
|
return [];
|
|
47
129
|
}
|
|
48
130
|
|
|
49
|
-
let lines
|
|
131
|
+
let { lines, changed } = sanitizeHistoryLines(raw);
|
|
50
132
|
|
|
51
133
|
if (lines.length > ROTATE_READ_THRESHOLD) {
|
|
52
134
|
lines = lines.slice(-ROTATE_KEEP);
|
|
53
|
-
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
if (changed) {
|
|
138
|
+
try { writePrivateHistory(historyPath, lines); } catch {}
|
|
54
139
|
}
|
|
55
140
|
|
|
56
141
|
return lines
|
|
@@ -37,9 +37,14 @@ export interface SessionLeaseHandle {
|
|
|
37
37
|
leaseDir: string;
|
|
38
38
|
owner: SessionLeaseOwner;
|
|
39
39
|
updateWriter(writer: { state: "none" | "spawning" } | { state: "running"; pid: number }): void;
|
|
40
|
-
release():
|
|
40
|
+
release(): boolean;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export type SessionLeaseState =
|
|
44
|
+
| { state: "free"; canonicalSessionFile: string; canonicalSessionId: string }
|
|
45
|
+
| { state: "owned"; canonicalSessionFile: string; canonicalSessionId: string; owner: SessionLeaseOwner }
|
|
46
|
+
| { state: "unreadable"; canonicalSessionFile: string; canonicalSessionId: string };
|
|
47
|
+
|
|
43
48
|
interface SessionLeaseOptions {
|
|
44
49
|
rootDir?: string;
|
|
45
50
|
now?: () => number;
|
|
@@ -98,11 +103,25 @@ export function canonicalSessionFilePath(sessionFile: string): string {
|
|
|
98
103
|
return fs.realpathSync.native(path.resolve(sessionFile));
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
export function
|
|
106
|
+
export function canonicalSessionId(sessionFile: string): string {
|
|
102
107
|
const canonical = canonicalSessionFilePath(sessionFile);
|
|
103
108
|
const key = process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
return createHash("sha256").update(key).digest("hex");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function sessionLeaseDir(sessionFile: string, rootDir = SESSION_LEASES_DIR): string {
|
|
113
|
+
return path.join(rootDir, canonicalSessionId(sessionFile));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function inspectSessionLease(sessionFile: string, rootDir = SESSION_LEASES_DIR): SessionLeaseState {
|
|
117
|
+
const canonicalSessionFile = canonicalSessionFilePath(sessionFile);
|
|
118
|
+
const canonicalSessionIdValue = canonicalSessionId(canonicalSessionFile);
|
|
119
|
+
const leaseDir = path.join(rootDir, canonicalSessionIdValue);
|
|
120
|
+
if (!fs.existsSync(leaseDir)) return { state: "free", canonicalSessionFile, canonicalSessionId: canonicalSessionIdValue };
|
|
121
|
+
const owner = readLeaseOwner(leaseDir);
|
|
122
|
+
return owner
|
|
123
|
+
? { state: "owned", canonicalSessionFile, canonicalSessionId: canonicalSessionIdValue, owner }
|
|
124
|
+
: { state: "unreadable", canonicalSessionFile, canonicalSessionId: canonicalSessionIdValue };
|
|
106
125
|
}
|
|
107
126
|
|
|
108
127
|
function parseOwner(value: unknown): SessionLeaseOwner | undefined {
|
|
@@ -251,8 +270,9 @@ export function acquireSessionLease(request: SessionLeaseRequest, options: Sessi
|
|
|
251
270
|
},
|
|
252
271
|
release() {
|
|
253
272
|
const currentOwner = readLeaseOwner(leaseDir);
|
|
254
|
-
if (!currentOwner || currentOwner.token !== owner.token) return;
|
|
273
|
+
if (!currentOwner || currentOwner.token !== owner.token) return false;
|
|
255
274
|
fs.rmSync(leaseDir, { recursive: true, force: true });
|
|
275
|
+
return !fs.existsSync(leaseDir);
|
|
256
276
|
},
|
|
257
277
|
};
|
|
258
278
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import * as os from "node:os";
|
|
3
4
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts";
|
|
5
7
|
import type { JsonSchemaObject } from "../../shared/types.ts";
|
|
6
8
|
|
|
7
9
|
export const STRUCTURED_OUTPUT_SCHEMA_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA";
|
|
8
10
|
export const STRUCTURED_OUTPUT_CAPTURE_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE";
|
|
11
|
+
export const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output.";
|
|
9
12
|
|
|
10
13
|
export interface StructuredOutputRuntime {
|
|
11
14
|
schema: JsonSchemaObject;
|
|
@@ -13,11 +16,108 @@ export interface StructuredOutputRuntime {
|
|
|
13
16
|
outputPath: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
const SCHEMA_MAP_KEYWORDS = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"] as const;
|
|
20
|
+
const SCHEMA_SINGLE_KEYWORDS = ["additionalItems", "additionalProperties", "contains", "not", "propertyNames", "if", "then", "else", "unevaluatedItems", "unevaluatedProperties", "contentSchema"] as const;
|
|
21
|
+
const SCHEMA_ARRAY_KEYWORDS = ["allOf", "anyOf", "oneOf", "prefixItems"] as const;
|
|
22
|
+
|
|
23
|
+
function rewriteLocalJsonPointerRefs(schema: unknown, pointerPrefix: string, inheritsWrapperResource = true): unknown {
|
|
24
|
+
if (typeof schema === "boolean" || !schema || typeof schema !== "object" || Array.isArray(schema)) return schema;
|
|
25
|
+
const source = schema as Record<string, unknown>;
|
|
26
|
+
const rewritten: Record<string, unknown> = { ...source };
|
|
27
|
+
const sharesWrapperResource = inheritsWrapperResource && typeof source.$id !== "string";
|
|
28
|
+
if (sharesWrapperResource) {
|
|
29
|
+
for (const keyword of ["$ref", "$dynamicRef", "$recursiveRef"] as const) {
|
|
30
|
+
const ref = source[keyword];
|
|
31
|
+
if (ref === "#") rewritten[keyword] = pointerPrefix;
|
|
32
|
+
else if (typeof ref === "string" && ref.startsWith("#/")) rewritten[keyword] = `${pointerPrefix}${ref.slice(1)}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const keyword of SCHEMA_MAP_KEYWORDS) {
|
|
36
|
+
const entries = source[keyword];
|
|
37
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) continue;
|
|
38
|
+
rewritten[keyword] = Object.fromEntries(Object.entries(entries).map(([name, nested]) => [
|
|
39
|
+
name,
|
|
40
|
+
rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource),
|
|
41
|
+
]));
|
|
42
|
+
}
|
|
43
|
+
const items = source.items;
|
|
44
|
+
if (Array.isArray(items)) rewritten.items = items.map((nested) => rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource));
|
|
45
|
+
else if (items !== undefined) rewritten.items = rewriteLocalJsonPointerRefs(items, pointerPrefix, sharesWrapperResource);
|
|
46
|
+
for (const keyword of SCHEMA_SINGLE_KEYWORDS) {
|
|
47
|
+
if (source[keyword] !== undefined) rewritten[keyword] = rewriteLocalJsonPointerRefs(source[keyword], pointerPrefix, sharesWrapperResource);
|
|
48
|
+
}
|
|
49
|
+
for (const keyword of SCHEMA_ARRAY_KEYWORDS) {
|
|
50
|
+
if (Array.isArray(source[keyword])) rewritten[keyword] = source[keyword].map((nested) => rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource));
|
|
51
|
+
}
|
|
52
|
+
const dependencies = source.dependencies;
|
|
53
|
+
if (dependencies && typeof dependencies === "object" && !Array.isArray(dependencies)) {
|
|
54
|
+
rewritten.dependencies = Object.fromEntries(Object.entries(dependencies).map(([name, nested]) => [
|
|
55
|
+
name,
|
|
56
|
+
Array.isArray(nested) ? nested : rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource),
|
|
57
|
+
]));
|
|
58
|
+
}
|
|
59
|
+
return rewritten;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createStructuredOutputToolParameters(schema: JsonSchemaObject): JsonSchemaObject {
|
|
63
|
+
return {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: { value: rewriteLocalJsonPointerRefs(schema, "#/properties/value") },
|
|
66
|
+
required: ["value"],
|
|
67
|
+
additionalProperties: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
16
71
|
interface CompiledJsonSchema {
|
|
17
72
|
Check(value: unknown): boolean;
|
|
18
73
|
Errors(value: unknown): Iterable<{ instancePath?: string; message?: string }>;
|
|
19
74
|
}
|
|
20
75
|
|
|
76
|
+
type CompileJsonSchema = (schema: unknown) => CompiledJsonSchema;
|
|
77
|
+
|
|
78
|
+
let cachedCompile: Promise<CompileJsonSchema> | undefined;
|
|
79
|
+
|
|
80
|
+
export async function resolveCompileFromPackageRoot(packageRoot: string): Promise<CompileJsonSchema | undefined> {
|
|
81
|
+
const requireFromRoot = createRequire(path.join(packageRoot, "package.json"));
|
|
82
|
+
const resolved = requireFromRoot.resolve("typebox/compile");
|
|
83
|
+
const mod = (await import(pathToFileURL(resolved).href)) as { Compile?: unknown };
|
|
84
|
+
return typeof mod.Compile === "function" ? (mod.Compile as CompileJsonSchema) : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function importCompile(): Promise<CompileJsonSchema> {
|
|
88
|
+
const failures: string[] = [];
|
|
89
|
+
try {
|
|
90
|
+
const mod = (await import("typebox/compile")) as { Compile?: unknown };
|
|
91
|
+
if (typeof mod.Compile === "function") return mod.Compile as CompileJsonSchema;
|
|
92
|
+
failures.push("typebox/compile did not export a Compile function");
|
|
93
|
+
} catch (error) {
|
|
94
|
+
failures.push(`direct import failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
95
|
+
}
|
|
96
|
+
const packageRoot = process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV];
|
|
97
|
+
if (packageRoot) {
|
|
98
|
+
try {
|
|
99
|
+
const compile = await resolveCompileFromPackageRoot(packageRoot);
|
|
100
|
+
if (compile) return compile;
|
|
101
|
+
failures.push("Pi package root typebox/compile did not export a Compile function");
|
|
102
|
+
} catch (error) {
|
|
103
|
+
failures.push(`Pi package root import failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
failures.push(`${PI_CODING_AGENT_PACKAGE_ROOT_ENV} is not set`);
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`Cannot load typebox/compile for structured output validation (${failures.join("; ")})`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function loadCompile(): Promise<CompileJsonSchema> {
|
|
112
|
+
if (!cachedCompile) {
|
|
113
|
+
cachedCompile = importCompile().catch((error) => {
|
|
114
|
+
cachedCompile = undefined;
|
|
115
|
+
throw error;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return cachedCompile;
|
|
119
|
+
}
|
|
120
|
+
|
|
21
121
|
export function assertJsonSchemaObject(schema: unknown, label = "outputSchema"): asserts schema is JsonSchemaObject {
|
|
22
122
|
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
23
123
|
throw new Error(`${label} must be a JSON Schema object.`);
|
|
@@ -35,10 +135,11 @@ export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?
|
|
|
35
135
|
return { schema, schemaPath, outputPath };
|
|
36
136
|
}
|
|
37
137
|
|
|
38
|
-
export function validateStructuredOutputValue(schema: JsonSchemaObject, value: unknown): { status: "valid" } | { status: "invalid"; message: string } {
|
|
138
|
+
export async function validateStructuredOutputValue(schema: JsonSchemaObject, value: unknown): Promise<{ status: "valid" } | { status: "invalid"; message: string }> {
|
|
139
|
+
const compile = await loadCompile();
|
|
39
140
|
let validator: CompiledJsonSchema;
|
|
40
141
|
try {
|
|
41
|
-
validator = (
|
|
142
|
+
validator = compile(schema);
|
|
42
143
|
} catch (error) {
|
|
43
144
|
return { status: "invalid", message: `invalid outputSchema: ${error instanceof Error ? error.message : String(error)}` };
|
|
44
145
|
}
|
|
@@ -52,9 +153,9 @@ export function validateStructuredOutputValue(schema: JsonSchemaObject, value: u
|
|
|
52
153
|
return { status: "invalid", message: errors.join("; ") || "schema validation failed" };
|
|
53
154
|
}
|
|
54
155
|
|
|
55
|
-
export function readStructuredOutput(runtime: StructuredOutputRuntime): { value?: unknown; error?: string } {
|
|
156
|
+
export async function readStructuredOutput(runtime: StructuredOutputRuntime): Promise<{ value?: unknown; error?: string }> {
|
|
56
157
|
if (!fs.existsSync(runtime.outputPath)) {
|
|
57
|
-
return { error:
|
|
158
|
+
return { error: MISSING_STRUCTURED_OUTPUT_CALL_ERROR };
|
|
58
159
|
}
|
|
59
160
|
let value: unknown;
|
|
60
161
|
try {
|
|
@@ -62,8 +163,12 @@ export function readStructuredOutput(runtime: StructuredOutputRuntime): { value?
|
|
|
62
163
|
} catch (error) {
|
|
63
164
|
return { error: `Failed to read structured output: ${error instanceof Error ? error.message : String(error)}` };
|
|
64
165
|
}
|
|
65
|
-
|
|
66
|
-
|
|
166
|
+
try {
|
|
167
|
+
const validation = await validateStructuredOutputValue(runtime.schema, value);
|
|
168
|
+
if (validation.status === "invalid") return { error: `Structured output validation failed: ${validation.message}` };
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return { error: `Failed to validate structured output: ${error instanceof Error ? error.message : String(error)}` };
|
|
171
|
+
}
|
|
67
172
|
return { value };
|
|
68
173
|
}
|
|
69
174
|
|