pi-subagents 0.55.0 → 0.56.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 +28 -3
- package/docs/models.md +6 -0
- package/package.json +1 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +23 -1
- package/src/api/preflight.ts +20 -1
- package/src/extension/schemas.ts +5 -0
- package/src/runs/background/async-execution.ts +48 -18
- package/src/runs/background/async-resume.ts +3 -0
- package/src/runs/background/subagent-runner.ts +83 -11
- package/src/runs/foreground/execution.ts +35 -6
- package/src/runs/foreground/foreground-history.ts +3 -2
- package/src/runs/foreground/subagent-executor.ts +59 -8
- package/src/runs/foreground/workflow-detach-reconcile.ts +15 -6
- package/src/runs/shared/acceptance.ts +10 -5
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/completion-guard.ts +3 -2
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/extension-bindings.ts +78 -0
- package/src/runs/shared/external-cli-runner.ts +2 -0
- package/src/runs/shared/fast-mode-extension.ts +10 -0
- package/src/runs/shared/model-exclusions.ts +2 -1
- package/src/runs/shared/model-fallback.ts +2 -7
- package/src/runs/shared/mutation-evidence.ts +145 -0
- package/src/runs/shared/parallel-utils.ts +3 -0
- package/src/runs/shared/pi-args.ts +47 -0
- package/src/runs/shared/structured-output.ts +18 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +9 -5
- package/src/shared/launch-contract.ts +6 -0
- package/src/shared/settings.ts +6 -1
- package/src/shared/types.ts +48 -0
- package/src/workflows/scripted-workflow.ts +50 -3
|
@@ -73,6 +73,7 @@ import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
|
73
73
|
import { usageBudgetExceededMessage, usageBudgetState, validateUsageBudgetConfig } from "../shared/usage-budget.ts";
|
|
74
74
|
import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
75
75
|
import { isAgentContractV1 } from "../shared/agent-contract.ts";
|
|
76
|
+
import { normalizeExtensionBindings, type ExtensionBindings } from "../shared/extension-bindings.ts";
|
|
76
77
|
import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
77
78
|
import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
|
|
78
79
|
import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
|
|
@@ -88,7 +89,7 @@ import {
|
|
|
88
89
|
resolveSubagentResultStatus,
|
|
89
90
|
stripDetailsOutputsForIntercomReceipt,
|
|
90
91
|
} from "../../intercom/result-intercom.ts";
|
|
91
|
-
import { applySteeringRecoveryAgentConfig, asyncReviveRequiresRecoveryDescriptor, buildRevivedAsyncTask, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts";
|
|
92
|
+
import { applySteeringRecoveryAgentConfig, asyncReviveRequiresRecoveryDescriptor, buildRevivedAsyncTask, readAsyncRecoveryDescriptor, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts";
|
|
92
93
|
import { deliverInterruptRequest, readRevivalBriefs, requestAsyncSteer, type SteerDeliveryMode } from "../background/control-channel.ts";
|
|
93
94
|
import { updateSteeringTarget, waitForSteeringAction } from "../background/steering.ts";
|
|
94
95
|
import { canQueueRetainedAsyncFollowUp, steerAsyncRun } from "./async-steering-action.ts";
|
|
@@ -156,6 +157,7 @@ import {
|
|
|
156
157
|
type ResolvedTurnBudget,
|
|
157
158
|
type ResolvedToolBudget,
|
|
158
159
|
type RunFanoutBudgetDescriptor,
|
|
160
|
+
type SteeringRecoveryDescriptor,
|
|
159
161
|
type SingleResult,
|
|
160
162
|
type SubagentChildStatusEvent,
|
|
161
163
|
type ToolBudgetConfig,
|
|
@@ -264,6 +266,7 @@ interface TaskParam {
|
|
|
264
266
|
reads?: string[] | boolean;
|
|
265
267
|
progress?: boolean;
|
|
266
268
|
model?: string;
|
|
269
|
+
fast?: boolean;
|
|
267
270
|
skill?: string | string[] | boolean;
|
|
268
271
|
outputSchema?: JsonSchemaObject;
|
|
269
272
|
acceptance?: AcceptanceInput;
|
|
@@ -288,6 +291,7 @@ export interface SubagentParamsLike {
|
|
|
288
291
|
type?: string;
|
|
289
292
|
agent?: string;
|
|
290
293
|
task?: string;
|
|
294
|
+
extensionBindings?: ExtensionBindings;
|
|
291
295
|
/** Retained async child run id. Valid only on workflow runs.run items. */
|
|
292
296
|
resume?: string;
|
|
293
297
|
message?: string;
|
|
@@ -340,6 +344,7 @@ export interface SubagentParamsLike {
|
|
|
340
344
|
artifacts?: boolean;
|
|
341
345
|
includeProgress?: boolean;
|
|
342
346
|
model?: string;
|
|
347
|
+
fast?: boolean;
|
|
343
348
|
thinking?: string | false;
|
|
344
349
|
scope?: string;
|
|
345
350
|
target?: string;
|
|
@@ -644,7 +649,7 @@ function foregroundChildActivityFromProgress(progress: SingleResult["progress"]
|
|
|
644
649
|
};
|
|
645
650
|
}
|
|
646
651
|
|
|
647
|
-
function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[] }): void {
|
|
652
|
+
function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[]; extensionBindings?: ExtensionBindings }): void {
|
|
648
653
|
state.foregroundRuns ??= new Map();
|
|
649
654
|
const previous = state.foregroundRuns.get(input.runId);
|
|
650
655
|
const updatedAt = Date.now();
|
|
@@ -686,6 +691,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
|
|
|
686
691
|
...(result.detachedReason ? { detachedReason: result.detachedReason } : {}),
|
|
687
692
|
...(result.acceptance ? { acceptance: result.acceptance } : {}),
|
|
688
693
|
...(result.launchContractDigest ? { launchContractDigest: result.launchContractDigest } : {}),
|
|
694
|
+
...(input.extensionBindings ? { extensionBindings: input.extensionBindings } : {}),
|
|
689
695
|
...(result.launchResolvedExtensions ? { launchResolvedExtensions: result.launchResolvedExtensions } : {}),
|
|
690
696
|
...(result.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: result.runtimeAcknowledgedExtensions } : {}),
|
|
691
697
|
...(result.capabilityCeiling ? { capabilityCeiling: result.capabilityCeiling } : {}),
|
|
@@ -804,7 +810,7 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
|
|
|
804
810
|
});
|
|
805
811
|
}
|
|
806
812
|
|
|
807
|
-
function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState, options: { exactOnly?: boolean } = {}): { runId: string; mode: SubagentRunMode; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; model?: string; thinking?: string; launchContractDigest?: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } | undefined {
|
|
813
|
+
function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState, options: { exactOnly?: boolean } = {}): { runId: string; mode: SubagentRunMode; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; model?: string; thinking?: string; launchContractDigest?: string; extensionBindings?: ExtensionBindings; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } | undefined {
|
|
808
814
|
const requested = (params.id ?? params.runId)?.trim();
|
|
809
815
|
if (!requested || !state.foregroundRuns?.size || !state.currentSessionId) return undefined;
|
|
810
816
|
const direct = state.foregroundRuns.get(requested);
|
|
@@ -835,6 +841,7 @@ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: Subage
|
|
|
835
841
|
...(child.model ? { model: child.model } : {}),
|
|
836
842
|
...(child.thinking ? { thinking: child.thinking } : {}),
|
|
837
843
|
...(child.launchContractDigest ? { launchContractDigest: child.launchContractDigest } : {}),
|
|
844
|
+
...(child.extensionBindings ? { extensionBindings: normalizeExtensionBindings(child.extensionBindings)!.value } : {}),
|
|
838
845
|
...(child.capabilityCeiling ? { capabilityCeiling: child.capabilityCeiling } : {}),
|
|
839
846
|
};
|
|
840
847
|
}
|
|
@@ -854,6 +861,7 @@ type NestedResumeSourceTarget = {
|
|
|
854
861
|
thinking?: AgentConfig["thinking"];
|
|
855
862
|
launchContractDigest?: string;
|
|
856
863
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
864
|
+
recoveryDescriptor?: SteeringRecoveryDescriptor;
|
|
857
865
|
};
|
|
858
866
|
type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget | NestedResumeSourceTarget;
|
|
859
867
|
|
|
@@ -1339,6 +1347,14 @@ function validateNestedSessionFile(run: NestedRunSummary, trustedSessionRoots: s
|
|
|
1339
1347
|
return realSessionFile;
|
|
1340
1348
|
}
|
|
1341
1349
|
|
|
1350
|
+
export function readNestedRecoveryDescriptor(asyncDir: string | undefined, runId: string, agent: string): SteeringRecoveryDescriptor | undefined {
|
|
1351
|
+
const recoveryDescriptor = readAsyncRecoveryDescriptor(asyncDir);
|
|
1352
|
+
if (!recoveryDescriptor) return undefined;
|
|
1353
|
+
if (recoveryDescriptor.sourceRunId !== runId) throw new Error(`Nested run '${runId}' has a recovery descriptor for a different source run.`);
|
|
1354
|
+
if (recoveryDescriptor.agent !== agent) throw new Error(`Nested run '${runId}' has a recovery descriptor for a different agent.`);
|
|
1355
|
+
return recoveryDescriptor;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1342
1358
|
function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "nested" }, trustedSessionRoots: string[]): NestedResumeSourceTarget {
|
|
1343
1359
|
const run = match.match.run;
|
|
1344
1360
|
if (run.state === "running" || run.state === "queued") throw new Error(`Nested run '${run.id}' is live; route the follow-up to the owner process instead.`);
|
|
@@ -1347,6 +1363,7 @@ function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "neste
|
|
|
1347
1363
|
if (!agent) throw new Error(`Could not determine child agent for nested run '${run.id}'.`);
|
|
1348
1364
|
const state = run.state === "complete" || run.state === "failed" || run.state === "paused" ? run.state : "failed";
|
|
1349
1365
|
const asyncDir = resolveNestedAsyncDir(match.match.rootRunId, run);
|
|
1366
|
+
const recoveryDescriptor = readNestedRecoveryDescriptor(asyncDir, run.id, agent);
|
|
1350
1367
|
return compactOptional<NestedResumeSourceTarget>({
|
|
1351
1368
|
kind: "revive",
|
|
1352
1369
|
source: "nested",
|
|
@@ -1357,6 +1374,7 @@ function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "neste
|
|
|
1357
1374
|
cwd: asyncDir ? path.dirname(asyncDir) : undefined,
|
|
1358
1375
|
sessionFile: validateNestedSessionFile(run, trustedSessionRoots),
|
|
1359
1376
|
...(run.capabilityCeiling ? { capabilityCeiling: run.capabilityCeiling } : {}),
|
|
1377
|
+
...(recoveryDescriptor ? { recoveryDescriptor } : {}),
|
|
1360
1378
|
});
|
|
1361
1379
|
}
|
|
1362
1380
|
|
|
@@ -1835,6 +1853,7 @@ async function resumeAsyncRun(input: {
|
|
|
1835
1853
|
sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
|
|
1836
1854
|
chainSkills: normalized === false ? [] : (normalized ?? []),
|
|
1837
1855
|
agentContract: input.params.agentContract,
|
|
1856
|
+
fast: input.params.fast,
|
|
1838
1857
|
dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
|
|
1839
1858
|
maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
|
|
1840
1859
|
waitToolEnabled: input.deps.waitToolEnabled,
|
|
@@ -1938,9 +1957,11 @@ async function resumeAsyncRun(input: {
|
|
|
1938
1957
|
},
|
|
1939
1958
|
context: recoveryContext,
|
|
1940
1959
|
modelOverride: recoveryDescriptor?.model ?? target.model,
|
|
1960
|
+
fast: recoveryDescriptor?.fast,
|
|
1941
1961
|
modelOverrideFromParent: recoveryDescriptor?.modelOverrideFromParent,
|
|
1942
1962
|
thinkingOverride: recoveryDescriptor?.thinking ?? target.thinking,
|
|
1943
1963
|
thinkingCeiling: recoveryDescriptor?.thinkingCeiling ?? ("thinkingCeiling" in target ? target.thinkingCeiling : undefined),
|
|
1964
|
+
extensionBindings: recoveryDescriptor?.extensionBindings ?? ("extensionBindings" in target ? target.extensionBindings : undefined),
|
|
1944
1965
|
outputBaseDir: resolveSingleRunOutputBaseDir(input.deps, artifactsDir, runId),
|
|
1945
1966
|
maxSubagentDepth: recoveryDescriptor?.maxSubagentDepth ?? resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
|
|
1946
1967
|
waitToolEnabled: input.deps.waitToolEnabled,
|
|
@@ -2223,6 +2244,15 @@ function canonicalizeExecutionParams(params: SubagentParamsLike, agents: AgentCo
|
|
|
2223
2244
|
const result = resolve(params.agent);
|
|
2224
2245
|
if (result.error) return { error: result.error };
|
|
2225
2246
|
params = omitUndefinedProperties({ ...params, agent: result.name });
|
|
2247
|
+
const agent = agents.find((candidate) => candidate.name === result.name);
|
|
2248
|
+
if (params.extensionBindings !== undefined && (agent?.runner?.type === "external-cli" || agent?.runner?.type === "external-job")) return { error: `extensionBindings is not supported for runner.type='${agent.runner.type}'.` };
|
|
2249
|
+
}
|
|
2250
|
+
if (params.extensionBindings !== undefined) {
|
|
2251
|
+
try {
|
|
2252
|
+
params = { ...params, extensionBindings: normalizeExtensionBindings(params.extensionBindings)!.value };
|
|
2253
|
+
} catch (error) {
|
|
2254
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
2255
|
+
}
|
|
2226
2256
|
}
|
|
2227
2257
|
if (params.tasks) {
|
|
2228
2258
|
const tasks: TaskParam[] = [];
|
|
@@ -3119,6 +3149,9 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
3119
3149
|
const externalRunnerWithoutExplicitModel = (a.runner?.type === "external-cli" || a.runner?.type === "external-job")
|
|
3120
3150
|
&& params.model === undefined
|
|
3121
3151
|
&& (a.model === undefined || (a.modelSource?.type === "subagents.defaultModel" && a.model === a.modelSource.model));
|
|
3152
|
+
if ((a.runner?.type === "external-cli" || a.runner?.type === "external-job") && (params.fast ?? a.fast) === true) {
|
|
3153
|
+
return buildRequestedModeError(params, `Agent '${a.name}' uses runner.type='${a.runner.type}' and does not support fast mode.`);
|
|
3154
|
+
}
|
|
3122
3155
|
const modelScopes = resolveModelScopesForAgent(data.modelScope, a.name, parentModel);
|
|
3123
3156
|
const modelOverride = a.runner?.type === "external-cli" || a.runner?.type === "external-job"
|
|
3124
3157
|
? params.model ?? (externalRunnerWithoutExplicitModel ? undefined : a.model)
|
|
@@ -3147,6 +3180,7 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
3147
3180
|
...(params.reads !== undefined ? { reads: params.reads } : {}),
|
|
3148
3181
|
outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
|
|
3149
3182
|
modelOverride,
|
|
3183
|
+
fast: params.fast,
|
|
3150
3184
|
modelOverrideFromParent,
|
|
3151
3185
|
thinkingOverride: externalRunnerWithoutExplicitModel ? undefined : thinkingOverrideForTask(params.agent!, 0, modelOverride, modelOverrideFromParent),
|
|
3152
3186
|
thinkingCeiling: a.maxThinking,
|
|
@@ -3163,6 +3197,7 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
3163
3197
|
nestedRoute,
|
|
3164
3198
|
agentContract: params.agentContract,
|
|
3165
3199
|
structuredOutputSchema: params.outputSchema,
|
|
3200
|
+
extensionBindings: params.extensionBindings,
|
|
3166
3201
|
acceptance: params.acceptance,
|
|
3167
3202
|
timeoutMs: data.timeoutMs,
|
|
3168
3203
|
turnBudget: data.turnBudget,
|
|
@@ -3540,7 +3575,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3540
3575
|
return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
|
|
3541
3576
|
}
|
|
3542
3577
|
const structuredRuntime = params.outputSchema
|
|
3543
|
-
? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined)
|
|
3578
|
+
? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined, { captureAcceptanceReport: params.acceptance !== false })
|
|
3544
3579
|
: undefined;
|
|
3545
3580
|
// Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
|
|
3546
3581
|
// absolute paths pass through; relative paths resolve against the child cwd.
|
|
@@ -3624,9 +3659,11 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3624
3659
|
nestedRoute: foregroundControl?.nestedRoute,
|
|
3625
3660
|
index: 0,
|
|
3626
3661
|
modelOverride,
|
|
3662
|
+
fast: params.fast,
|
|
3627
3663
|
modelOverrideFromParent,
|
|
3628
3664
|
thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride, modelOverrideFromParent),
|
|
3629
3665
|
thinkingCeiling: agentConfig.maxThinking,
|
|
3666
|
+
extensionBindings: params.extensionBindings,
|
|
3630
3667
|
availableModels,
|
|
3631
3668
|
preferredModelProvider: currentProvider,
|
|
3632
3669
|
modelScope: modelScopes,
|
|
@@ -3739,7 +3776,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3739
3776
|
usageBudget: usageBudgetState(data.usageBudget, totalCost),
|
|
3740
3777
|
...(worktreeHandoff?.reference ? { parallelHandoff: worktreeHandoff.reference } : {}),
|
|
3741
3778
|
}));
|
|
3742
|
-
rememberForegroundRun(deps.state, { runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, results: details.results });
|
|
3779
|
+
rememberForegroundRun(deps.state, { runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, results: details.results, extensionBindings: params.extensionBindings });
|
|
3743
3780
|
|
|
3744
3781
|
const suppressRoutineResultIntercom = shouldSuppressRoutineResultIntercom({ suppressRoutineResultIntercom: params.suppressRoutineResultIntercom, results: [r] });
|
|
3745
3782
|
if (!r.detached && !r.interrupted && !suppressRoutineResultIntercom) {
|
|
@@ -3894,8 +3931,9 @@ function workflowChildResult(
|
|
|
3894
3931
|
? result.details.results[0].finalOutput
|
|
3895
3932
|
: receiptOutput;
|
|
3896
3933
|
const detached = result.details.results.some((child) => child.detached);
|
|
3934
|
+
const interrupted = result.details.results.some((child) => child.interrupted);
|
|
3897
3935
|
const stopped = result.details.results.some((child) => child.stopped);
|
|
3898
|
-
const ok = result.isError !== true && !detached && !stopped;
|
|
3936
|
+
const ok = result.isError !== true && !detached && !interrupted && !stopped;
|
|
3899
3937
|
const artifactPaths = new Set<string>();
|
|
3900
3938
|
if (result.details.asyncDir) artifactPaths.add(result.details.asyncDir);
|
|
3901
3939
|
if (result.details.parallelHandoff?.path) artifactPaths.add(result.details.parallelHandoff.path);
|
|
@@ -3935,6 +3973,7 @@ function workflowChildResult(
|
|
|
3935
3973
|
output,
|
|
3936
3974
|
...(!ok ? { error: receiptOutput || output || "Child run failed." } : {}),
|
|
3937
3975
|
...(detached ? { detached: true } : {}),
|
|
3976
|
+
...(interrupted ? { interrupted: true } : {}),
|
|
3938
3977
|
...(stopped ? { stopped: true } : {}),
|
|
3939
3978
|
...(structured.length === 1 ? { structuredOutput: structured[0] } : structured.length > 1 ? { structuredOutput: structured } : {}),
|
|
3940
3979
|
...(requestedContext ? { requestedContext } : {}),
|
|
@@ -4071,6 +4110,9 @@ export function prepareWorkflowLaunchParams(
|
|
|
4071
4110
|
? undefined
|
|
4072
4111
|
: Math.max(1, options.parentDeadlineAt - Date.now());
|
|
4073
4112
|
if (typeof childParams.resume === "string") {
|
|
4113
|
+
if (childParams.extensionBindings !== undefined || workflowDefaults.extensionBindings !== undefined) {
|
|
4114
|
+
throw new Error("extensionBindings is not supported with retained resume; resume uses the original retained child binding.");
|
|
4115
|
+
}
|
|
4074
4116
|
if (childParams.gate !== undefined || workflowDefaults.gate !== undefined) {
|
|
4075
4117
|
throw new Error("gate is not supported with retained resume; resume uses the retained child contract.");
|
|
4076
4118
|
}
|
|
@@ -4109,6 +4151,7 @@ export function prepareWorkflowLaunchParams(
|
|
|
4109
4151
|
...(options.suppressRoutineResultIntercom ? { suppressRoutineResultIntercom: true } : {}),
|
|
4110
4152
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
4111
4153
|
} as SubagentParamsLike;
|
|
4154
|
+
if (launchParams.extensionBindings !== undefined) launchParams.extensionBindings = normalizeExtensionBindings(launchParams.extensionBindings)!.value;
|
|
4112
4155
|
const normalizedGate = normalizeGateParams(launchParams);
|
|
4113
4156
|
if (!normalizedGate.ok) throw new Error(normalizedGate.error);
|
|
4114
4157
|
return normalizedGate.params;
|
|
@@ -4244,7 +4287,15 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4244
4287
|
if (!normalizedGate.ok) return buildRequestedModeError(params, normalizedGate.error);
|
|
4245
4288
|
const requestParams = normalizedGate.params;
|
|
4246
4289
|
const normalizedAction = typeof requestParams.action === "string" ? requestParams.action.trim() : requestParams.action;
|
|
4290
|
+
if (normalizedAction === "resume" && requestParams.extensionBindings !== undefined) return buildRequestedModeError(requestParams, "extensionBindings is not supported with action='resume'; resume uses the original retained child binding.");
|
|
4247
4291
|
if (requestParams.workflowScript !== undefined && normalizedAction === undefined) {
|
|
4292
|
+
if (requestParams.extensionBindings !== undefined) {
|
|
4293
|
+
try {
|
|
4294
|
+
requestParams.extensionBindings = normalizeExtensionBindings(requestParams.extensionBindings)!.value;
|
|
4295
|
+
} catch (error) {
|
|
4296
|
+
return buildRequestedModeError(requestParams, error instanceof Error ? error.message : String(error));
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4248
4299
|
const parentCwd = ctx.cwd;
|
|
4249
4300
|
const timeout = requestParams.timeoutMs ?? requestParams.maxRuntimeMs ?? (requestParams.async === false ? resolveConfigDefaultTimeoutMs(deps.config.timeoutMs) ?? DEFAULT_FOREGROUND_TIMEOUT_MS : undefined);
|
|
4250
4301
|
const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
|
|
@@ -4740,7 +4791,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4740
4791
|
} catch (receiptError) {
|
|
4741
4792
|
appendWorkflowEvent({ type: "subagent.workflow.receipt_write_failed", error: `Failed to persist async workflow receipt: ${receiptError instanceof Error ? receiptError.message : String(receiptError)}` });
|
|
4742
4793
|
}
|
|
4743
|
-
if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.stopped ? { stopped: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
|
|
4794
|
+
if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.stopped ? { stopped: true } : {}), ...(child.interrupted ? { interrupted: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
|
|
4744
4795
|
persist();
|
|
4745
4796
|
persistClosed = true;
|
|
4746
4797
|
appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}) });
|
|
@@ -4780,7 +4831,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4780
4831
|
} catch (receiptError) {
|
|
4781
4832
|
appendWorkflowEvent({ type: "subagent.workflow.receipt_write_failed", error: `Failed to persist async workflow receipt: ${receiptError instanceof Error ? receiptError.message : String(receiptError)}` });
|
|
4782
4833
|
}
|
|
4783
|
-
if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: status.state === "complete", state: status.state, summary: resultSummary, error: status.state === "complete" ? undefined : status.error, stopped: status.stopped, activityState: status.activityState, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.stopped ? { stopped: true } : {}), ...(child.detached && status.state !== "complete" ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
|
|
4834
|
+
if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: status.state === "complete", state: status.state, summary: resultSummary, error: status.state === "complete" ? undefined : status.error, stopped: status.stopped, activityState: status.activityState, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.stopped ? { stopped: true } : {}), ...(child.interrupted ? { interrupted: true } : {}), ...(child.detached && status.state !== "complete" ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
|
|
4784
4835
|
persist();
|
|
4785
4836
|
persistClosed = true;
|
|
4786
4837
|
appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}), ...(status.activityState ? { activityState: status.activityState } : {}) });
|
|
@@ -23,13 +23,16 @@ function cloneWorkflowStatus(status: AsyncStatus): AsyncStatus {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function childSucceeded(result: Pick<SingleResult, "exitCode" | "error">): boolean {
|
|
27
|
-
return result.exitCode === 0 && !result.error;
|
|
26
|
+
function childSucceeded(result: Pick<SingleResult, "exitCode" | "error" | "interrupted">): boolean {
|
|
27
|
+
return result.exitCode === 0 && !result.error && !result.interrupted;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const UNSUPPORTED_DETACHED_WORKFLOW_CONTINUATION = "unsupported-continuation: detached workflow child settled, but JavaScript workflow continuation was not persisted. Resume the workflow explicitly instead of treating the completed child as top-level workflow completion.";
|
|
31
|
+
const INTERRUPTED_DETACHED_CHILD = "Interrupted. Waiting for explicit next action.";
|
|
32
|
+
|
|
30
33
|
export function applyDetachedChildToPausedWorkflow(
|
|
31
34
|
status: AsyncStatus,
|
|
32
|
-
input: { childRunId: string; result: Pick<SingleResult, "exitCode" | "error" | "sessionFile">; workflowKey?: string },
|
|
35
|
+
input: { childRunId: string; result: Pick<SingleResult, "exitCode" | "error" | "interrupted" | "sessionFile">; workflowKey?: string },
|
|
33
36
|
): AsyncStatus | undefined {
|
|
34
37
|
if (status.mode !== "workflow" || status.state !== "paused") return undefined;
|
|
35
38
|
const next = cloneWorkflowStatus(status);
|
|
@@ -37,6 +40,7 @@ export function applyDetachedChildToPausedWorkflow(
|
|
|
37
40
|
?? (input.workflowKey ? next.steps?.find((candidate) => candidate.workflowKey === input.workflowKey) : undefined);
|
|
38
41
|
if (!step) return undefined;
|
|
39
42
|
const succeeded = childSucceeded(input.result);
|
|
43
|
+
const failedSiblingError = next.steps?.find((candidate) => candidate !== step && candidate.status === "failed" && candidate.error)?.error;
|
|
40
44
|
const updatedAt = Date.now();
|
|
41
45
|
step.status = succeeded ? "completed" : "failed";
|
|
42
46
|
step.endedAt = updatedAt;
|
|
@@ -45,10 +49,12 @@ export function applyDetachedChildToPausedWorkflow(
|
|
|
45
49
|
delete step.currentToolStartedAt;
|
|
46
50
|
if (input.result.sessionFile) step.sessionFile = input.result.sessionFile;
|
|
47
51
|
if (succeeded) delete step.error;
|
|
52
|
+
else if (input.result.interrupted) step.error = input.result.error ?? INTERRUPTED_DETACHED_CHILD;
|
|
48
53
|
else if (input.result.error) step.error = input.result.error;
|
|
49
54
|
next.lastUpdate = updatedAt;
|
|
50
55
|
const promoted = promotePausedWorkflowIfSettled(next);
|
|
51
|
-
if (promoted?.state === "failed" && input.result.
|
|
56
|
+
if (promoted?.state === "failed" && input.result.interrupted && !failedSiblingError) promoted.error = input.result.error ?? INTERRUPTED_DETACHED_CHILD;
|
|
57
|
+
else if (promoted?.state === "failed" && input.result.error) promoted.error = input.result.error;
|
|
52
58
|
return promoted ?? next;
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -63,10 +69,11 @@ export function promotePausedWorkflowIfSettled(status: AsyncStatus): AsyncStatus
|
|
|
63
69
|
const failed = next.steps.some((candidate) => candidate.status === "failed");
|
|
64
70
|
const updatedAt = Date.now();
|
|
65
71
|
next.lastUpdate = updatedAt;
|
|
66
|
-
next.state =
|
|
72
|
+
next.state = "failed";
|
|
67
73
|
next.endedAt = updatedAt;
|
|
68
74
|
delete next.activityState;
|
|
69
|
-
if (
|
|
75
|
+
if (failed) return next;
|
|
76
|
+
next.error = UNSUPPORTED_DETACHED_WORKFLOW_CONTINUATION;
|
|
70
77
|
return next;
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -83,6 +90,7 @@ function workflowResultChildren(status: AsyncStatus, childRunId: string, result:
|
|
|
83
90
|
output,
|
|
84
91
|
outputState: output.trim() ? "present" : "absent",
|
|
85
92
|
detached: undefined,
|
|
93
|
+
...(result.interrupted ? { interrupted: true } : {}),
|
|
86
94
|
...(result.error ? { error: result.error } : {}),
|
|
87
95
|
};
|
|
88
96
|
});
|
|
@@ -94,6 +102,7 @@ function workflowResultChildren(status: AsyncStatus, childRunId: string, result:
|
|
|
94
102
|
success: step.status === "completed" || step.status === "complete",
|
|
95
103
|
output: step.runId === childRunId ? output : "",
|
|
96
104
|
outputState: step.runId === childRunId && output.trim() ? "present" : "absent",
|
|
105
|
+
...(step.runId === childRunId && result.interrupted ? { interrupted: true } : {}),
|
|
97
106
|
...(step.error ? { error: step.error } : {}),
|
|
98
107
|
}));
|
|
99
108
|
}
|
|
@@ -404,7 +404,7 @@ function acceptanceRequiresChildReport(acceptance: ResolvedAcceptanceConfig): bo
|
|
|
404
404
|
return acceptance.criteria.length > 0 || acceptance.evidence.length > 0;
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
-
export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig, options: { reportOptional?: boolean } = {}): string {
|
|
407
|
+
export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig, options: { reportOptional?: boolean; structuredOutput?: boolean } = {}): string {
|
|
408
408
|
if (acceptance.level === "none") return "";
|
|
409
409
|
if (options.reportOptional && !acceptanceRequiresChildReport(acceptance)) return "";
|
|
410
410
|
const lines = [
|
|
@@ -431,13 +431,15 @@ export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig, opt
|
|
|
431
431
|
}
|
|
432
432
|
lines.push(
|
|
433
433
|
"",
|
|
434
|
-
|
|
434
|
+
options.structuredOutput
|
|
435
|
+
? "Include an `acceptanceReport` object in your final `structured_output` tool call in this shape:"
|
|
436
|
+
: "Finish with a fenced JSON block tagged `acceptance-report` in this shape:",
|
|
435
437
|
"Use empty arrays when no items apply; array fields contain strings unless object entries are shown.",
|
|
436
438
|
"Empty-string entries (`[\"\"]`) are ignored; use `[]` when nothing applies.",
|
|
437
439
|
"`criteriaSatisfied[].status` must be exactly one of: satisfied, not-satisfied, not-applicable.",
|
|
438
440
|
"`commandsRun[].result` must be exactly one of: passed, failed, not-run.",
|
|
439
441
|
"`manualNotes` and `notes` are optional strings; an empty string means no note and does not satisfy `manual-notes` evidence.",
|
|
440
|
-
"```acceptance-report",
|
|
442
|
+
...(options.structuredOutput ? [] : ["```acceptance-report"]),
|
|
441
443
|
JSON.stringify({
|
|
442
444
|
criteriaSatisfied: acceptance.criteria
|
|
443
445
|
.filter((criterion) => criterion.severity !== "recommended")
|
|
@@ -452,7 +454,7 @@ export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig, opt
|
|
|
452
454
|
reviewFindings: ["blocker: file.ts:12 - issue found, or no blockers"],
|
|
453
455
|
manualNotes: "anything else the parent should know",
|
|
454
456
|
}, null, 2),
|
|
455
|
-
"```",
|
|
457
|
+
...(options.structuredOutput ? [] : ["```"]),
|
|
456
458
|
);
|
|
457
459
|
return lines.join("\n");
|
|
458
460
|
}
|
|
@@ -1272,6 +1274,7 @@ export async function evaluateAcceptance(input: {
|
|
|
1272
1274
|
*/
|
|
1273
1275
|
fileOutput?: { content: string; path: string; authoritative?: boolean };
|
|
1274
1276
|
report?: AcceptanceReport;
|
|
1277
|
+
reportError?: string;
|
|
1275
1278
|
reviewResult?: AcceptanceReviewResult;
|
|
1276
1279
|
signal?: AbortSignal;
|
|
1277
1280
|
abortMessage?: string;
|
|
@@ -1293,7 +1296,9 @@ export async function evaluateAcceptance(input: {
|
|
|
1293
1296
|
};
|
|
1294
1297
|
if (acceptance.level === "none") return ledger;
|
|
1295
1298
|
|
|
1296
|
-
const parsed = input.
|
|
1299
|
+
const parsed = input.reportError
|
|
1300
|
+
? { error: input.reportError }
|
|
1301
|
+
: input.report
|
|
1297
1302
|
? (() => {
|
|
1298
1303
|
const validation = validateAcceptanceReport(input.report);
|
|
1299
1304
|
return validation.report
|
|
@@ -12,7 +12,7 @@ export function buildExecutionProjection(result: Pick<SingleResult, "exitCode" |
|
|
|
12
12
|
return { status: "stopped", success: false, exitCode: result.exitCode, stopped: true, ...(result.error ? { error: result.error } : {}) };
|
|
13
13
|
}
|
|
14
14
|
if (result.interrupted) {
|
|
15
|
-
return { status: "paused", success:
|
|
15
|
+
return { status: "paused", success: false, exitCode: result.exitCode, interrupted: true, ...(result.error ? { error: result.error } : {}) };
|
|
16
16
|
}
|
|
17
17
|
const success = result.exitCode === 0 && !result.error && !result.timedOut;
|
|
18
18
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
-
import type { AcceptanceRole } from "../../shared/types.ts";
|
|
2
|
+
import type { AcceptanceRole, TrackedMutationEvidence } from "../../shared/types.ts";
|
|
3
3
|
import { isMutatingTool } from "./long-running-guard.ts";
|
|
4
4
|
import { classifyTaskMutationIntent, expectsImplementationMutation, taskMayMutate } from "./task-intent.ts";
|
|
5
5
|
|
|
@@ -48,6 +48,7 @@ interface CompletionMutationGuardInput {
|
|
|
48
48
|
tools?: string[];
|
|
49
49
|
mcpDirectTools?: string[];
|
|
50
50
|
toolAvailabilityError?: string;
|
|
51
|
+
mutationEvidence?: TrackedMutationEvidence;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
interface CompletionMutationGuardResult {
|
|
@@ -238,7 +239,7 @@ export function evaluateCompletionMutationGuard(input: CompletionMutationGuardIn
|
|
|
238
239
|
const expectedMutation = hasMutationToolCapability(input.tools, input.mcpDirectTools)
|
|
239
240
|
? expectsImplementationMutation(input.agent, input.task)
|
|
240
241
|
: false;
|
|
241
|
-
const attemptedMutation = hasMutationToolCall(input.messages);
|
|
242
|
+
const attemptedMutation = hasMutationToolCall(input.messages) || input.mutationEvidence?.attemptedMutation === true;
|
|
242
243
|
const noEditChallengeComplete = isImplementationChallengeTask(input.task)
|
|
243
244
|
&& reportsNoBetterChallengeChange(input.messages);
|
|
244
245
|
return {
|
|
@@ -46,7 +46,7 @@ const DYNAMIC_STEP_KEYS = new Set(["expand", "parallel", "collect", "concurrency
|
|
|
46
46
|
const RUNNER_DYNAMIC_STEP_KEYS = new Set([...DYNAMIC_STEP_KEYS, "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "sessionFiles", "thinkingOverrides"]);
|
|
47
47
|
const DYNAMIC_EXPAND_KEYS = new Set(["from", "item", "key", "maxItems", "onEmpty"]);
|
|
48
48
|
const DYNAMIC_EXPAND_FROM_KEYS = new Set(["output", "path"]);
|
|
49
|
-
const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "output", "outputMode", "reads", "progress", "skill", "model", "toolBudget", "acceptance", "agentContract", "gateOn"]);
|
|
49
|
+
const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "output", "outputMode", "reads", "progress", "skill", "model", "fast", "toolBudget", "acceptance", "agentContract", "gateOn"]);
|
|
50
50
|
const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
|
|
51
51
|
...DYNAMIC_PARALLEL_KEYS,
|
|
52
52
|
"outputName", "structured", "inheritProjectContext", "inheritSkills", "skills", "outputPath", "namespaceOutputPath", "maxSubagentDepth", "timeoutMs", "waitToolEnabled",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export const PI_SUBAGENT_EXTENSION_BINDINGS_ENV = "PI_SUBAGENT_EXTENSION_BINDINGS";
|
|
2
|
+
export const MAX_EXTENSION_BINDING_NAMESPACES = 16;
|
|
3
|
+
export const MAX_EXTENSION_BINDINGS_BYTES = 16 * 1024;
|
|
4
|
+
export const MAX_EXTENSION_BINDINGS_DEPTH = 16;
|
|
5
|
+
export const MAX_EXTENSION_BINDINGS_PROPERTIES = 256;
|
|
6
|
+
|
|
7
|
+
const EXTENSION_BINDING_NAMESPACE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,62})\/[1-9][0-9]{0,8}$/;
|
|
8
|
+
|
|
9
|
+
export type ExtensionBindingJson = null | boolean | number | string | ReadonlyArray<ExtensionBindingJson> | { readonly [key: string]: ExtensionBindingJson };
|
|
10
|
+
export type ExtensionBindings = Readonly<Record<string, ExtensionBindingJson>>;
|
|
11
|
+
|
|
12
|
+
export interface NormalizedExtensionBindings {
|
|
13
|
+
value: ExtensionBindings;
|
|
14
|
+
json: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function canonicalizeJson(value: unknown, path: string, depth: number, seen: Set<object>, propertyCount: { value: number }): ExtensionBindingJson {
|
|
18
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
19
|
+
if (typeof value === "number") {
|
|
20
|
+
if (!Number.isFinite(value)) throw new Error(`${path} must contain only finite JSON numbers.`);
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
if (typeof value !== "object") throw new Error(`${path} must contain only plain JSON values.`);
|
|
24
|
+
if (depth > MAX_EXTENSION_BINDINGS_DEPTH) throw new Error(`${path} exceeds the maximum nesting depth of ${MAX_EXTENSION_BINDINGS_DEPTH}.`);
|
|
25
|
+
if (seen.has(value)) throw new Error(`${path} must not contain cycles.`);
|
|
26
|
+
seen.add(value);
|
|
27
|
+
try {
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
const output: ExtensionBindingJson[] = [];
|
|
30
|
+
for (let index = 0; index < value.length; index++) {
|
|
31
|
+
if (!Object.prototype.hasOwnProperty.call(value, index)) throw new Error(`${path} must not contain sparse arrays.`);
|
|
32
|
+
output.push(canonicalizeJson(value[index], `${path}[${index}]`, depth + 1, seen, propertyCount));
|
|
33
|
+
}
|
|
34
|
+
return Object.freeze(output);
|
|
35
|
+
}
|
|
36
|
+
const prototype = Object.getPrototypeOf(value);
|
|
37
|
+
if (prototype !== Object.prototype && prototype !== null) throw new Error(`${path} must contain only plain JSON objects.`);
|
|
38
|
+
if (Object.getOwnPropertySymbols(value).length > 0) throw new Error(`${path} must not contain symbol keys.`);
|
|
39
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
40
|
+
const output: Record<string, ExtensionBindingJson> = {};
|
|
41
|
+
for (const key of Object.keys(descriptors).sort()) {
|
|
42
|
+
const descriptor = descriptors[key]!;
|
|
43
|
+
if (!descriptor.enumerable || !("value" in descriptor)) throw new Error(`${path}.${key} must be an enumerable data property.`);
|
|
44
|
+
propertyCount.value += 1;
|
|
45
|
+
if (propertyCount.value > MAX_EXTENSION_BINDINGS_PROPERTIES) throw new Error(`extensionBindings exceeds ${MAX_EXTENSION_BINDINGS_PROPERTIES} total properties.`);
|
|
46
|
+
Object.defineProperty(output, key, { value: canonicalizeJson(descriptor.value, `${path}.${key}`, depth + 1, seen, propertyCount), enumerable: true, writable: false, configurable: false });
|
|
47
|
+
}
|
|
48
|
+
return Object.freeze(output);
|
|
49
|
+
} finally {
|
|
50
|
+
seen.delete(value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizeExtensionBindings(input: unknown): NormalizedExtensionBindings | undefined {
|
|
55
|
+
if (input === undefined) return undefined;
|
|
56
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("extensionBindings must be a plain JSON object.");
|
|
57
|
+
const prototype = Object.getPrototypeOf(input);
|
|
58
|
+
if (prototype !== Object.prototype && prototype !== null) throw new Error("extensionBindings must be a plain JSON object.");
|
|
59
|
+
const keys = Object.keys(input);
|
|
60
|
+
if (keys.length > MAX_EXTENSION_BINDING_NAMESPACES) throw new Error(`extensionBindings supports at most ${MAX_EXTENSION_BINDING_NAMESPACES} namespaces.`);
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
if (!EXTENSION_BINDING_NAMESPACE.test(key)) throw new Error(`extensionBindings namespace '${key}' must use a package-like name followed by '/<positive-version>'.`);
|
|
63
|
+
}
|
|
64
|
+
const value = canonicalizeJson(input, "extensionBindings", 0, new Set(), { value: 0 }) as ExtensionBindings;
|
|
65
|
+
const json = JSON.stringify(value);
|
|
66
|
+
const bytes = Buffer.byteLength(json, "utf8");
|
|
67
|
+
if (bytes > MAX_EXTENSION_BINDINGS_BYTES) throw new Error(`extensionBindings canonical JSON is ${bytes} bytes; maximum is ${MAX_EXTENSION_BINDINGS_BYTES}.`);
|
|
68
|
+
return { value, json };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function encodeExtensionBindings(input: ExtensionBindings | undefined): string | undefined {
|
|
72
|
+
return input === undefined ? undefined : normalizeExtensionBindings(input)!.json;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function omitExtensionBindingsEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
76
|
+
const { [PI_SUBAGENT_EXTENSION_BINDINGS_ENV]: _extensionBindings, ...sanitized } = env;
|
|
77
|
+
return sanitized;
|
|
78
|
+
}
|
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { finished } from "node:stream/promises";
|
|
5
5
|
import { trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
6
6
|
import type { ExternalProcessStatus } from "../../shared/types.ts";
|
|
7
|
+
import { omitExtensionBindingsEnv } from "./extension-bindings.ts";
|
|
7
8
|
|
|
8
9
|
const HARD_KILL_DELAY_MS = 2_000;
|
|
9
10
|
const MAX_OUTPUT_TAIL_BYTES = 64 * 1024;
|
|
@@ -54,6 +55,7 @@ export function runExternalCli(input: {
|
|
|
54
55
|
let hardKillTimer: NodeJS.Timeout | undefined;
|
|
55
56
|
const child = spawn(input.command, input.args ?? [], {
|
|
56
57
|
cwd: input.cwd,
|
|
58
|
+
env: omitExtensionBindingsEnv(process.env),
|
|
57
59
|
stdio: ["pipe", "pipe", "pipe"],
|
|
58
60
|
shell: false,
|
|
59
61
|
windowsHide: true,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
function withPriorityServiceTier(payload: unknown): unknown {
|
|
4
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return payload;
|
|
5
|
+
return { ...payload, service_tier: "priority" };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function registerSubagentFastModeExtension(pi: ExtensionAPI): void {
|
|
9
|
+
pi.on("before_provider_request", (event) => ({ payload: withPriorityServiceTier(event.payload) }));
|
|
10
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { splitKnownThinkingSuffix } from "../../shared/model-info.ts";
|
|
3
4
|
import { TEMP_ROOT_DIR } from "../../shared/types.ts";
|
|
4
5
|
|
|
5
6
|
export const EXCLUSIONS_PATH_ENV = "PI_MODEL_EXCLUSIONS_PATH";
|
|
@@ -194,7 +195,7 @@ export function getExcludedCount(): number {
|
|
|
194
195
|
* {@link recordModelFailure} is later recognised by the candidate filter.
|
|
195
196
|
*/
|
|
196
197
|
export function parseModelKey(fullId: string): { provider?: string; modelId: string } {
|
|
197
|
-
const base = fullId
|
|
198
|
+
const base = splitKnownThinkingSuffix(fullId).baseModel;
|
|
198
199
|
if (!base.includes("/")) return { modelId: base };
|
|
199
200
|
const slash = base.indexOf("/");
|
|
200
201
|
return { provider: base.slice(0, slash), modelId: base.slice(slash + 1) };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { splitKnownThinkingSuffix, type ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
|
|
2
2
|
import type { Usage } from "../../shared/types.ts";
|
|
3
3
|
import { filterFallbackCandidates, parseModelKey, recordModelFailure } from "./model-exclusions.ts";
|
|
4
4
|
import { checkModelScope, type ModelScopeCheckRule, type ModelScopeViolation, type ModelSource } from "./model-scope.ts";
|
|
@@ -14,12 +14,7 @@ interface ModelAttemptSummary {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
|
|
17
|
-
|
|
18
|
-
if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
|
|
19
|
-
return {
|
|
20
|
-
baseModel: model.substring(0, colonIdx),
|
|
21
|
-
thinkingSuffix: model.substring(colonIdx),
|
|
22
|
-
};
|
|
17
|
+
return splitKnownThinkingSuffix(model);
|
|
23
18
|
}
|
|
24
19
|
|
|
25
20
|
export function formatSubagentModelVerificationError(expectedModel: string, observedModel: string, availableModels: AvailableModelInfo[] | undefined): string | undefined {
|