pi-subagents 0.61.0 → 0.62.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 +24 -1
- package/docs/agents.md +6 -2
- package/docs/configuration.md +3 -3
- package/docs/extension-api.md +2 -2
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +2 -2
- package/install.mjs +0 -1
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +3 -4
- package/src/agents/agent-management.ts +19 -0
- package/src/agents/agent-serializer.ts +3 -0
- package/src/agents/agents.ts +16 -2
- package/src/agents/runtime-agent-registry.ts +5 -1
- package/src/api/preflight.ts +4 -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/runs/background/active-async-capacity.ts +26 -7
- package/src/runs/background/async-execution.ts +47 -16
- package/src/runs/background/async-resume.ts +3 -2
- package/src/runs/background/process-terminal.ts +16 -0
- package/src/runs/background/scheduled-runs.ts +63 -6
- package/src/runs/background/steering.ts +4 -1
- package/src/runs/background/subagent-runner.ts +14 -7
- package/src/runs/background/wait-tool.ts +1 -7
- package/src/runs/foreground/execution.ts +14 -7
- package/src/runs/foreground/subagent-executor.ts +3 -2
- package/src/runs/shared/acceptance.ts +70 -9
- package/src/runs/shared/capability-ceiling.ts +1 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +2 -6
- package/src/runs/shared/permissions.ts +1 -1
- package/src/runs/shared/pi-args.ts +24 -12
- 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 +17 -6
- package/src/runs/shared/tool-timeout.ts +1 -1
- 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 +3 -1
- package/src/slash/slash-commands.ts +1 -1
- package/src/slash/subagents-admin.ts +2 -0
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { existsSync
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import type { Message } from "@earendil-works/pi-ai";
|
|
9
9
|
import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig } from "../../agents/agents.ts";
|
|
10
10
|
import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
|
|
11
|
-
import { alignForkedSessionCwd } from "../../shared/fork-
|
|
11
|
+
import { alignForkedSessionCwd } from "../../shared/fork-session-cwd.ts";
|
|
12
12
|
import {
|
|
13
13
|
ensureArtifactsDir,
|
|
14
14
|
formatOutputArtifactContent,
|
|
@@ -73,7 +73,7 @@ import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledge
|
|
|
73
73
|
import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
|
|
74
74
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
75
75
|
import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV } from "../../shared/thinking-ceiling.ts";
|
|
76
|
-
import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
|
|
76
|
+
import { clearStructuredOutputCaptures, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
|
|
77
77
|
import { formatMidToolExitError, formatProcessSignalError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
|
|
78
78
|
import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
|
|
79
79
|
import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
|
|
@@ -411,6 +411,7 @@ async function runSingleAttempt(
|
|
|
411
411
|
inheritSkills: agent.inheritSkills,
|
|
412
412
|
requireReadTool: Boolean(shared.resolvedSkillNames?.length),
|
|
413
413
|
tools: agent.tools,
|
|
414
|
+
excludeTools: agent.excludeTools,
|
|
414
415
|
allowNestedSubagents: agent.allowNestedSubagents,
|
|
415
416
|
extensions: agent.extensions,
|
|
416
417
|
subagentOnlyExtensions: agent.subagentOnlyExtensions,
|
|
@@ -464,6 +465,7 @@ async function runSingleAttempt(
|
|
|
464
465
|
cwd: options.cwd ?? runtimeCwd,
|
|
465
466
|
requireReadTool: Boolean(shared.resolvedSkillNames?.length),
|
|
466
467
|
structuredOutput: Boolean(options.structuredOutput),
|
|
468
|
+
excludeTools: agent.excludeTools,
|
|
467
469
|
fast: options.fast ?? agent.fast,
|
|
468
470
|
model: modelArg,
|
|
469
471
|
modelCandidates: shared.modelCandidates,
|
|
@@ -520,6 +522,7 @@ async function runSingleAttempt(
|
|
|
520
522
|
inheritSkills: agent.inheritSkills,
|
|
521
523
|
skills: shared.resolvedSkillNames ?? [],
|
|
522
524
|
tools: toolPlan.effectiveToolAllowlist,
|
|
525
|
+
...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
|
|
523
526
|
extensions: toolPlan.extensionArgs,
|
|
524
527
|
mcpDirectTools: toolPlan.effectiveMcpTools,
|
|
525
528
|
...(options.outputPath ? { outputPath: options.outputPath } : {}),
|
|
@@ -551,10 +554,14 @@ async function runSingleAttempt(
|
|
|
551
554
|
}, options.context);
|
|
552
555
|
const startTime = Date.now();
|
|
553
556
|
if (options.structuredOutput) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
557
|
+
const cleanupError = clearStructuredOutputCaptures(options.structuredOutput);
|
|
558
|
+
if (cleanupError) {
|
|
559
|
+
cleanupTempDir(tempDir);
|
|
560
|
+
result.exitCode = 1;
|
|
561
|
+
result.error = cleanupError;
|
|
562
|
+
result.finalOutput = cleanupError;
|
|
563
|
+
result.progressSummary = { toolCount: 0, tokens: 0, durationMs: Date.now() - startTime };
|
|
564
|
+
return result;
|
|
558
565
|
}
|
|
559
566
|
}
|
|
560
567
|
const controlConfig = options.controlConfig ?? DEFAULT_CONTROL_CONFIG;
|
|
@@ -50,7 +50,7 @@ import { acquireActiveAsyncCapacity, ActiveAsyncCapacityError, getActiveAsyncCap
|
|
|
50
50
|
import { isScheduledRunAction } from "../background/scheduled-runs.ts";
|
|
51
51
|
import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
|
|
52
52
|
import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
|
|
53
|
-
import { normalizeGateAcceptance, validateExecutionAcceptance } from "../shared/acceptance.ts";
|
|
53
|
+
import { normalizeGateAcceptance, resolveAcceptanceReportMode, validateExecutionAcceptance } from "../shared/acceptance.ts";
|
|
54
54
|
import { canPreferFork, createForkContextResolver, forkedChildRequiresThinkingOff, resolveSubagentLaunchContext } from "../../shared/fork-context.ts";
|
|
55
55
|
import { createPrunedForkSessionWriter } from "../../shared/pruned-fork.ts";
|
|
56
56
|
import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
|
|
@@ -393,6 +393,7 @@ export interface SubagentParamsLike {
|
|
|
393
393
|
agentContract?: AgentContract;
|
|
394
394
|
at?: string;
|
|
395
395
|
every?: string;
|
|
396
|
+
sessionOnly?: boolean;
|
|
396
397
|
on?: string | number;
|
|
397
398
|
timezone?: string;
|
|
398
399
|
overlap?: "skip";
|
|
@@ -3690,7 +3691,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3690
3691
|
return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
|
|
3691
3692
|
}
|
|
3692
3693
|
const structuredRuntime = params.outputSchema
|
|
3693
|
-
? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined, {
|
|
3694
|
+
? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined, { acceptanceReport: resolveAcceptanceReportMode(params.acceptance) })
|
|
3694
3695
|
: undefined;
|
|
3695
3696
|
// Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
|
|
3696
3697
|
// absolute paths pass through; relative paths resolve against the child cwd.
|
|
@@ -47,7 +47,7 @@ const VALID_EVIDENCE_KINDS: AcceptanceEvidenceKind[] = [
|
|
|
47
47
|
const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>(VALID_EVIDENCE_KINDS);
|
|
48
48
|
const ACCEPTANCE_EVIDENCE_HELP = `Supported evidence kinds: ${VALID_EVIDENCE_KINDS.join(", ")}. Example: { level: "checked", evidence: ["commands-run", "changed-files"] }.`;
|
|
49
49
|
const ACCEPTANCE_OBJECT_EXAMPLE = "Example: { level: \"checked\", evidence: [\"commands-run\", \"changed-files\"] }.";
|
|
50
|
-
const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify", "review", "stopRules", "reason"]);
|
|
50
|
+
const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "report", "criteria", "evidence", "verify", "review", "stopRules", "reason"]);
|
|
51
51
|
const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
|
|
52
52
|
const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
|
|
53
53
|
const ACCEPTANCE_REVIEW_KEYS = new Set(["agent", "focus", "required"]);
|
|
@@ -146,11 +146,46 @@ function inferLevel(input: {
|
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
type AcceptanceInputNormalizationResult = { value: unknown; error?: string };
|
|
150
|
+
|
|
151
|
+
function normalizeAcceptanceValue(input: unknown, pathLabel = "acceptance"): AcceptanceInputNormalizationResult {
|
|
152
|
+
if (typeof input !== "string") return { value: input };
|
|
153
|
+
const trimmed = input.trim();
|
|
154
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return { value: input };
|
|
155
|
+
let parsed: unknown;
|
|
156
|
+
try {
|
|
157
|
+
parsed = JSON.parse(trimmed);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
value: input,
|
|
161
|
+
error: `${pathLabel} JSON string must encode a valid acceptance object: ${error instanceof Error ? error.message : String(error)}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
165
|
+
const kind = parsed === null ? "null" : Array.isArray(parsed) ? "an array" : typeof parsed;
|
|
166
|
+
return { value: input, error: `${pathLabel} JSON string must encode an object; got ${kind}.` };
|
|
167
|
+
}
|
|
168
|
+
return { value: parsed };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function normalizeAcceptanceInput(input: unknown): AcceptanceConfig {
|
|
172
|
+
const normalized = normalizeAcceptanceValue(input);
|
|
173
|
+
if (normalized.error) throw new Error(normalized.error);
|
|
174
|
+
input = normalized.value;
|
|
150
175
|
if (input === undefined || input === "auto") return { level: "auto" };
|
|
151
176
|
if (input === false) return { level: "none", reason: "disabled by deprecated false shorthand" };
|
|
152
|
-
if (typeof input === "string") return { level: input };
|
|
153
|
-
return { ...input };
|
|
177
|
+
if (typeof input === "string") return { level: input as AcceptanceConfig["level"] };
|
|
178
|
+
return { ...(input as AcceptanceConfig) };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type ResolvedAcceptanceReportMode = "off" | "optional" | "required";
|
|
182
|
+
|
|
183
|
+
export function resolveAcceptanceReportMode(input: unknown): ResolvedAcceptanceReportMode {
|
|
184
|
+
const normalized = normalizeAcceptanceValue(input);
|
|
185
|
+
if (normalized.error) throw new Error(normalized.error);
|
|
186
|
+
const value = normalized.value;
|
|
187
|
+
const report = value && typeof value === "object" && !Array.isArray(value) ? (value as AcceptanceConfig).report : undefined;
|
|
188
|
+
return value === false || report === "off" ? "off" : report === "on" ? "required" : "optional";
|
|
154
189
|
}
|
|
155
190
|
|
|
156
191
|
type GateAcceptanceNormalizationResult =
|
|
@@ -158,7 +193,11 @@ type GateAcceptanceNormalizationResult =
|
|
|
158
193
|
| { ok: false; error: string };
|
|
159
194
|
|
|
160
195
|
export function normalizeGateAcceptance(gate: unknown, acceptance: AcceptanceInput | undefined): GateAcceptanceNormalizationResult {
|
|
161
|
-
if (gate === undefined)
|
|
196
|
+
if (gate === undefined) {
|
|
197
|
+
if (acceptance === undefined) return { ok: true };
|
|
198
|
+
const normalized = normalizeAcceptanceValue(acceptance);
|
|
199
|
+
return normalized.error ? { ok: false, error: normalized.error } : { ok: true, acceptance: normalized.value as AcceptanceInput };
|
|
200
|
+
}
|
|
162
201
|
if (typeof gate !== "string" || !gate.trim()) return { ok: false, error: "gate must be a non-empty command string." };
|
|
163
202
|
if (acceptance !== undefined) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." };
|
|
164
203
|
return { ok: true, acceptance: { level: "verified", verify: [{ id: "gate", command: gate.trim() }] } };
|
|
@@ -177,6 +216,9 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
177
216
|
const errors: string[] = [];
|
|
178
217
|
if (input === undefined) return errors;
|
|
179
218
|
if (input === false) return errors;
|
|
219
|
+
const normalized = normalizeAcceptanceValue(input, pathLabel);
|
|
220
|
+
if (normalized.error) return [normalized.error];
|
|
221
|
+
input = normalized.value;
|
|
180
222
|
if (typeof input === "string") {
|
|
181
223
|
if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
182
224
|
else if (!VALID_LEVELS.has(input as AcceptanceLevel)) errors.push(`${pathLabel} has invalid level '${input}'.`);
|
|
@@ -197,6 +239,9 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
197
239
|
} else if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level as AcceptanceLevel))) {
|
|
198
240
|
errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified.`);
|
|
199
241
|
}
|
|
242
|
+
if (value.report !== undefined && value.report !== "on" && value.report !== "off") {
|
|
243
|
+
errors.push(`${pathLabel}.report must be on or off.`);
|
|
244
|
+
}
|
|
200
245
|
if (value.level === "none" && (typeof value.reason !== "string" || !value.reason.trim())) {
|
|
201
246
|
errors.push(`${pathLabel}.reason is required when level is none.`);
|
|
202
247
|
}
|
|
@@ -304,29 +349,45 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
304
349
|
|
|
305
350
|
export function validateExecutionAcceptance(input: {
|
|
306
351
|
acceptance?: unknown;
|
|
307
|
-
|
|
352
|
+
outputSchema?: unknown;
|
|
353
|
+
tasks?: Array<{ acceptance?: unknown; outputSchema?: unknown }>;
|
|
308
354
|
chain?: Array<{
|
|
309
355
|
acceptance?: unknown;
|
|
310
|
-
|
|
356
|
+
outputSchema?: unknown;
|
|
357
|
+
parallel?: Array<{ acceptance?: unknown; outputSchema?: unknown }> | { acceptance?: unknown; outputSchema?: unknown };
|
|
311
358
|
}>;
|
|
312
359
|
}): string[] {
|
|
313
360
|
const errors = validateAcceptanceInput(input.acceptance, "acceptance");
|
|
361
|
+
errors.push(...validateAcceptanceReportMode(input.acceptance, input.outputSchema, "acceptance"));
|
|
314
362
|
for (const [index, task] of (input.tasks ?? []).entries()) {
|
|
315
363
|
errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`));
|
|
364
|
+
errors.push(...validateAcceptanceReportMode(task.acceptance, task.outputSchema, `tasks[${index}].acceptance`));
|
|
316
365
|
}
|
|
317
366
|
for (const [stepIndex, step] of (input.chain ?? []).entries()) {
|
|
318
367
|
errors.push(...validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`));
|
|
368
|
+
errors.push(...validateAcceptanceReportMode(step.acceptance, step.outputSchema, `chain[${stepIndex}].acceptance`));
|
|
319
369
|
if (Array.isArray(step.parallel)) {
|
|
320
370
|
for (const [taskIndex, task] of step.parallel.entries()) {
|
|
321
371
|
errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
|
|
372
|
+
errors.push(...validateAcceptanceReportMode(task.acceptance, task.outputSchema, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
|
|
322
373
|
}
|
|
323
374
|
} else if (step.parallel) {
|
|
324
375
|
errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`));
|
|
376
|
+
errors.push(...validateAcceptanceReportMode(step.parallel.acceptance, step.parallel.outputSchema, `chain[${stepIndex}].parallel.acceptance`));
|
|
325
377
|
}
|
|
326
378
|
}
|
|
327
379
|
return errors;
|
|
328
380
|
}
|
|
329
381
|
|
|
382
|
+
function validateAcceptanceReportMode(acceptance: unknown, outputSchema: unknown, pathLabel: string): string[] {
|
|
383
|
+
const normalized = normalizeAcceptanceValue(acceptance, pathLabel);
|
|
384
|
+
if (normalized.error) return [];
|
|
385
|
+
acceptance = normalized.value;
|
|
386
|
+
if (!acceptance || typeof acceptance !== "object" || Array.isArray(acceptance)) return [];
|
|
387
|
+
if (!("report" in acceptance)) return [];
|
|
388
|
+
return outputSchema === undefined ? [`${pathLabel}.report requires outputSchema.`] : [];
|
|
389
|
+
}
|
|
390
|
+
|
|
330
391
|
function normalizeCriteria(criteria: Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }> | undefined, evidence: AcceptanceEvidenceKind[]): ResolvedAcceptanceGate[] {
|
|
331
392
|
return (criteria ?? []).map((criterion, index): ResolvedAcceptanceGate => {
|
|
332
393
|
if (typeof criterion === "string") {
|
|
@@ -851,7 +912,7 @@ function validateStringArrayField(errors: string[], value: unknown, pathLabel: s
|
|
|
851
912
|
}
|
|
852
913
|
}
|
|
853
914
|
|
|
854
|
-
function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: AcceptanceReport; errors: string[] } {
|
|
915
|
+
export function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: AcceptanceReport; errors: string[] } {
|
|
855
916
|
const normalized = normalizeAcceptanceReportValue(value, pathLabel);
|
|
856
917
|
value = normalized.value;
|
|
857
918
|
pathLabel = normalized.pathLabel;
|
|
@@ -1310,7 +1371,7 @@ export async function evaluateAcceptance(input: {
|
|
|
1310
1371
|
|
|
1311
1372
|
const parsed: AcceptanceReportParseResult = input.reportError
|
|
1312
1373
|
? { error: input.reportError }
|
|
1313
|
-
: input.report
|
|
1374
|
+
: input.report !== undefined
|
|
1314
1375
|
? (() => {
|
|
1315
1376
|
const validation = validateAcceptanceReport(input.report);
|
|
1316
1377
|
return validation.report
|
|
@@ -50,7 +50,7 @@ const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outpu
|
|
|
50
50
|
const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
|
|
51
51
|
...DYNAMIC_PARALLEL_KEYS,
|
|
52
52
|
"outputName", "structured", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills", "outputPath", "namespaceOutputPath", "maxSubagentDepth", "timeoutMs", "waitToolEnabled", "waitToolDefaultTimeoutMs",
|
|
53
|
-
"structuredOutput", "structuredOutputSchema", "tools", "allowNestedSubagents", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "capabilityCeiling", "completionGuard", "systemPrompt",
|
|
53
|
+
"structuredOutput", "structuredOutputSchema", "tools", "excludeTools", "allowNestedSubagents", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "capabilityCeiling", "completionGuard", "systemPrompt",
|
|
54
54
|
"systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "parentSessionId", "launchResolvedExtensions", "requestedCwd",
|
|
55
55
|
]);
|
|
56
56
|
const DYNAMIC_COLLECT_KEYS = new Set(["as", "outputSchema"]);
|
|
@@ -42,6 +42,7 @@ export interface RunnerSubagentStep {
|
|
|
42
42
|
skipPrimaryModelVerification?: boolean;
|
|
43
43
|
modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string; contextWindow?: number }>;
|
|
44
44
|
tools?: string[];
|
|
45
|
+
excludeTools?: string[];
|
|
45
46
|
allowNestedSubagents?: boolean;
|
|
46
47
|
extensions?: string[];
|
|
47
48
|
subagentOnlyExtensions?: string[];
|
|
@@ -68,12 +69,7 @@ export interface RunnerSubagentStep {
|
|
|
68
69
|
toolTimeoutMs?: number;
|
|
69
70
|
waitToolEnabled?: boolean;
|
|
70
71
|
waitToolDefaultTimeoutMs?: number;
|
|
71
|
-
structuredOutput?:
|
|
72
|
-
schema: import("../../shared/types.ts").JsonSchemaObject;
|
|
73
|
-
schemaPath: string;
|
|
74
|
-
outputPath: string;
|
|
75
|
-
acceptanceReportPath?: string;
|
|
76
|
-
};
|
|
72
|
+
structuredOutput?: import("./structured-output.ts").StructuredOutputRuntime;
|
|
77
73
|
structuredOutputSchema?: import("../../shared/types.ts").JsonSchemaObject;
|
|
78
74
|
agentContract?: import("../../shared/types.ts").AgentContract;
|
|
79
75
|
definitionDigest?: string;
|
|
@@ -7,7 +7,7 @@ export interface PermissionConfig { rules?: PermissionRules }
|
|
|
7
7
|
|
|
8
8
|
export const PERMISSION_POLICY_ENV = "PI_SUBAGENT_PERMISSION_POLICY";
|
|
9
9
|
export const PERMISSION_AUDIT_PATH_ENV = "PI_SUBAGENT_PERMISSION_AUDIT_PATH";
|
|
10
|
-
const INTERNAL_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait", "
|
|
10
|
+
const INTERNAL_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait", "structured_output"]);
|
|
11
11
|
const DECISIONS = new Set<PermissionDecision>(["allow", "ask", "deny"]);
|
|
12
12
|
const MAX_POLICY_BYTES = 16 * 1024;
|
|
13
13
|
const MAX_PREVIEW_BYTES = 2048;
|
|
@@ -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,
|
|
@@ -171,6 +173,7 @@ export interface BuildPiArgsInput {
|
|
|
171
173
|
inheritSkills: boolean;
|
|
172
174
|
requireReadTool?: boolean;
|
|
173
175
|
tools?: string[];
|
|
176
|
+
excludeTools?: string[];
|
|
174
177
|
extensions?: string[];
|
|
175
178
|
subagentOnlyExtensions?: string[];
|
|
176
179
|
systemPrompt?: string | null;
|
|
@@ -201,12 +204,7 @@ export interface BuildPiArgsInput {
|
|
|
201
204
|
steerInboxDir?: string;
|
|
202
205
|
steerCapabilityPath?: string;
|
|
203
206
|
steerAckDir?: string;
|
|
204
|
-
structuredOutput?:
|
|
205
|
-
schema: JsonSchemaObject;
|
|
206
|
-
schemaPath: string;
|
|
207
|
-
outputPath: string;
|
|
208
|
-
acceptanceReportPath?: string;
|
|
209
|
-
};
|
|
207
|
+
structuredOutput?: StructuredOutputRuntime;
|
|
210
208
|
fast?: boolean;
|
|
211
209
|
modelCandidates?: readonly string[];
|
|
212
210
|
toolBudget?: ResolvedToolBudget;
|
|
@@ -300,6 +298,7 @@ function resolveFastModeExtension(input: Pick<ResolvePiLaunchToolPlanInput, "fas
|
|
|
300
298
|
|
|
301
299
|
export interface ResolvePiLaunchToolPlanInput {
|
|
302
300
|
tools?: string[];
|
|
301
|
+
excludeTools?: string[];
|
|
303
302
|
allowNestedSubagents?: boolean;
|
|
304
303
|
extensions?: string[];
|
|
305
304
|
subagentOnlyExtensions?: string[];
|
|
@@ -329,6 +328,7 @@ export interface PiLaunchToolPlan {
|
|
|
329
328
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
330
329
|
requestedBuiltinTools: string[];
|
|
331
330
|
declaredBuiltinTools: string[];
|
|
331
|
+
excludeTools: string[];
|
|
332
332
|
toolExtensionPaths: string[];
|
|
333
333
|
resolvedMcpSelections: ResolvedMcpDirectToolSelection[];
|
|
334
334
|
effectiveMcpSelections: ResolvedMcpDirectToolSelection[];
|
|
@@ -499,8 +499,12 @@ export function resolvePiLaunchToolPlan(
|
|
|
499
499
|
? ["read", ...requestedBuiltinTools]
|
|
500
500
|
: requestedBuiltinTools
|
|
501
501
|
).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
|
|
502
|
-
const
|
|
502
|
+
const excludeTools = [...new Set((input.excludeTools ?? []).map((tool) => tool.trim()).filter(Boolean))];
|
|
503
|
+
const excludedToolSet = new Set(excludeTools);
|
|
504
|
+
const effectiveDeclaredBuiltinTools = declaredBuiltinTools.filter((tool) => !excludedToolSet.has(tool));
|
|
505
|
+
const fanoutAuthorized = effectiveDeclaredBuiltinTools.includes("subagent") || (
|
|
503
506
|
input.allowNestedSubagents === true &&
|
|
507
|
+
!excludedToolSet.has("subagent") &&
|
|
504
508
|
(!allowedToolSet || allowedToolSet.has("subagent"))
|
|
505
509
|
);
|
|
506
510
|
const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions
|
|
@@ -524,7 +528,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
524
528
|
!allowedToolSet ||
|
|
525
529
|
allowedToolSet.has(selection.name) ||
|
|
526
530
|
isLegacyUnderscoreMcpToolAllowed(selection, allowedToolSet, resolvedMcpNames, legacyMcpNameCounts),
|
|
527
|
-
);
|
|
531
|
+
).filter((selection) => !excludedToolSet.has(selection.name));
|
|
528
532
|
const effectiveMcpTools = effectiveMcpSelections.map(
|
|
529
533
|
(selection) => selection.name,
|
|
530
534
|
);
|
|
@@ -540,10 +544,10 @@ export function resolvePiLaunchToolPlan(
|
|
|
540
544
|
input.tools !== undefined ||
|
|
541
545
|
(input.mcpDirectTools?.length ?? 0) > 0 ||
|
|
542
546
|
allowedToolSet !== undefined;
|
|
543
|
-
const internalTools = input.structuredOutput ? ["structured_output"] : [];
|
|
547
|
+
const internalTools = (input.structuredOutput ? ["structured_output"] : []).filter((tool) => !excludedToolSet.has(tool));
|
|
544
548
|
const effectiveToolAllowlist = [
|
|
545
549
|
...new Set([
|
|
546
|
-
...
|
|
550
|
+
...effectiveDeclaredBuiltinTools,
|
|
547
551
|
...effectiveMcpTools,
|
|
548
552
|
...internalTools,
|
|
549
553
|
]),
|
|
@@ -554,11 +558,11 @@ export function resolvePiLaunchToolPlan(
|
|
|
554
558
|
// appended intercom alongside contact_supervisor, so that exact pairing is
|
|
555
559
|
// legacy plumbing, not a user demand for an external intercom provider;
|
|
556
560
|
// a lone intercom entry stays strictly required (#1207).
|
|
557
|
-
const legacySupervisorPairing =
|
|
561
|
+
const legacySupervisorPairing = effectiveDeclaredBuiltinTools.includes("contact_supervisor");
|
|
558
562
|
const requiredChildTools = explicitToolAllowlist
|
|
559
563
|
? [
|
|
560
564
|
...new Set([
|
|
561
|
-
...(input.tools !== undefined ?
|
|
565
|
+
...(input.tools !== undefined ? effectiveDeclaredBuiltinTools : []),
|
|
562
566
|
...(input.mcpDirectTools?.length ? effectiveMcpTools : []),
|
|
563
567
|
...internalTools,
|
|
564
568
|
].filter((tool) => tool !== "contact_supervisor" && (!legacySupervisorPairing || tool !== "intercom"))),
|
|
@@ -620,6 +624,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
620
624
|
ceiling: capabilityCeiling,
|
|
621
625
|
...(requestedToolNames ? { requestedTools: requestedToolNames } : {}),
|
|
622
626
|
effectiveTools: effectiveToolAllowlist,
|
|
627
|
+
...(excludeTools.length > 0 ? { excludeTools } : {}),
|
|
623
628
|
removedTools:
|
|
624
629
|
requestedToolNames?.filter(
|
|
625
630
|
(tool) => !effectiveToolAllowlist.includes(tool),
|
|
@@ -657,6 +662,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
657
662
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
658
663
|
requestedBuiltinTools,
|
|
659
664
|
declaredBuiltinTools,
|
|
665
|
+
excludeTools,
|
|
660
666
|
toolExtensionPaths,
|
|
661
667
|
resolvedMcpSelections,
|
|
662
668
|
effectiveMcpSelections,
|
|
@@ -740,6 +746,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
740
746
|
|
|
741
747
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
742
748
|
tools: input.tools,
|
|
749
|
+
excludeTools: input.excludeTools,
|
|
743
750
|
allowNestedSubagents: input.allowNestedSubagents,
|
|
744
751
|
extensions: input.extensions,
|
|
745
752
|
subagentOnlyExtensions: input.subagentOnlyExtensions,
|
|
@@ -766,6 +773,8 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
766
773
|
);
|
|
767
774
|
if (toolPlan.effectiveToolAllowlist.length > 0)
|
|
768
775
|
args.push(toolPlan.effectiveToolAllowlist.join(","));
|
|
776
|
+
} else if (toolPlan.excludeTools.length > 0) {
|
|
777
|
+
args.push("--exclude-tools", toolPlan.excludeTools.join(","));
|
|
769
778
|
}
|
|
770
779
|
if (toolPlan.disableAmbientExtensions) {
|
|
771
780
|
args.push("--no-extensions");
|
|
@@ -999,10 +1008,13 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
999
1008
|
env[SUBAGENT_CAPABILITY_CEILING_ENV] = encodedCapabilityCeiling;
|
|
1000
1009
|
if (encodedPermissionRules)
|
|
1001
1010
|
env[PERMISSION_POLICY_ENV] = encodedPermissionRules;
|
|
1011
|
+
env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = undefined;
|
|
1012
|
+
env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] = undefined;
|
|
1002
1013
|
if (input.structuredOutput) {
|
|
1003
1014
|
env[STRUCTURED_OUTPUT_CAPTURE_ENV] = input.structuredOutput.outputPath;
|
|
1004
1015
|
env[STRUCTURED_OUTPUT_SCHEMA_ENV] = input.structuredOutput.schemaPath;
|
|
1005
1016
|
if (input.structuredOutput.acceptanceReportPath) env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = input.structuredOutput.acceptanceReportPath;
|
|
1017
|
+
if (input.structuredOutput.acceptanceReportRequired) env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] = "1";
|
|
1006
1018
|
}
|
|
1007
1019
|
if (input.steerInboxDir) {
|
|
1008
1020
|
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
|
}
|