pi-subagents 0.36.0 → 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 +17 -0
- package/README.md +93 -8
- package/agents/delegate.md +2 -0
- package/agents/worker.md +2 -0
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +20 -4
- package/src/agents/agent-management.ts +7 -1
- package/src/agents/agents.ts +99 -12
- package/src/api/capability-ceiling.ts +17 -0
- package/src/api/delegation.ts +3 -1
- package/src/api/preflight.ts +399 -0
- package/src/extension/index.ts +2 -0
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +8 -2
- package/src/extension/tool-description.ts +2 -0
- package/src/runs/background/async-execution.ts +125 -14
- package/src/runs/background/async-resume.ts +24 -7
- package/src/runs/background/async-status.ts +18 -0
- package/src/runs/background/process-terminal.ts +280 -0
- package/src/runs/background/run-status.ts +7 -1
- 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 +182 -12
- package/src/runs/foreground/chain-execution.ts +5 -0
- package/src/runs/foreground/execution.ts +59 -13
- package/src/runs/foreground/subagent-executor.ts +32 -2
- package/src/runs/shared/acceptance.ts +41 -30
- package/src/runs/shared/capability-ceiling.ts +177 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
- package/src/runs/shared/nested-events.ts +8 -1
- package/src/runs/shared/parallel-utils.ts +5 -0
- package/src/runs/shared/pi-args.ts +141 -58
- package/src/runs/shared/session-lease.ts +25 -5
- package/src/runs/shared/subagent-prompt-runtime.ts +14 -1
- package/src/runs/shared/tool-availability.ts +18 -2
- package/src/shared/launch-contract.ts +123 -0
- package/src/shared/types.ts +104 -7
- package/src/shared/utils.ts +17 -42
- package/src/slash/delegation-adapters.ts +1 -1
- package/src/slash/slash-commands.ts +1 -1
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
getFinalOutput,
|
|
43
43
|
findLatestSessionFile,
|
|
44
44
|
detectSubagentError,
|
|
45
|
+
hasEmptyTerminalAssistantResponse,
|
|
45
46
|
extractToolArgsPreview,
|
|
46
47
|
extractTextFromContent,
|
|
47
48
|
} from "../../shared/utils.ts";
|
|
@@ -51,7 +52,8 @@ import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
|
|
|
51
52
|
import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
|
|
52
53
|
import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
|
|
53
54
|
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
54
|
-
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
|
|
55
|
+
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
|
|
56
|
+
import { decodeSubagentCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
|
|
55
57
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
56
58
|
import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
|
|
57
59
|
import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
|
|
@@ -77,6 +79,7 @@ import { attachContractProjections, isAgentContractV1 } from "../shared/agent-co
|
|
|
77
79
|
import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
|
|
78
80
|
import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
|
|
79
81
|
import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
|
|
82
|
+
import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
|
|
80
83
|
import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
|
|
81
84
|
import {
|
|
82
85
|
acceptChildWatchdogEvent,
|
|
@@ -125,6 +128,7 @@ function resolveAttemptTimeout(options: RunSyncOptions): { timeoutMs: number; re
|
|
|
125
128
|
function buildPendingAcceptanceLedger(acceptance: ResolvedAcceptanceConfig): AcceptanceLedger {
|
|
126
129
|
return {
|
|
127
130
|
status: "pending",
|
|
131
|
+
evidenceStatus: "pending",
|
|
128
132
|
explicit: acceptance.explicit,
|
|
129
133
|
effectiveAcceptance: acceptance,
|
|
130
134
|
inferredReason: acceptance.inferredReason,
|
|
@@ -194,6 +198,7 @@ async function runSingleAttempt(
|
|
|
194
198
|
sessionEnabled: boolean;
|
|
195
199
|
systemPrompt: string;
|
|
196
200
|
resolvedSkillNames?: string[];
|
|
201
|
+
modelCandidates?: string[];
|
|
197
202
|
skillsWarning?: string;
|
|
198
203
|
jsonlPath?: string;
|
|
199
204
|
artifactPaths?: ArtifactPaths;
|
|
@@ -215,7 +220,7 @@ async function runSingleAttempt(
|
|
|
215
220
|
childIndex: options.index ?? 0,
|
|
216
221
|
})
|
|
217
222
|
: undefined;
|
|
218
|
-
const { args, env: sharedEnv, tempDir, toolDiagnosticPath } = buildPiArgs({
|
|
223
|
+
const { args, env: sharedEnv, tempDir, toolDiagnosticPath, capabilityAudit } = buildPiArgs({
|
|
219
224
|
baseArgs: ["--mode", "json", "-p"],
|
|
220
225
|
task,
|
|
221
226
|
sessionEnabled: shared.sessionEnabled,
|
|
@@ -249,12 +254,44 @@ async function runSingleAttempt(
|
|
|
249
254
|
allowZeroToolBudget: options.allowZeroToolBudget,
|
|
250
255
|
childWatchdog,
|
|
251
256
|
waitToolEnabled: options.waitToolEnabled,
|
|
257
|
+
capabilityCeiling: options.capabilityCeiling,
|
|
252
258
|
});
|
|
253
259
|
|
|
260
|
+
const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(shared.systemPrompt, options.turnBudget);
|
|
261
|
+
const toolPlan = resolvePiLaunchToolPlan({
|
|
262
|
+
tools: agent.tools,
|
|
263
|
+
extensions: agent.extensions,
|
|
264
|
+
subagentOnlyExtensions: agent.subagentOnlyExtensions,
|
|
265
|
+
mcpDirectTools: agent.mcpDirectTools,
|
|
266
|
+
cwd: options.cwd ?? runtimeCwd,
|
|
267
|
+
requireReadTool: Boolean(shared.resolvedSkillNames?.length),
|
|
268
|
+
structuredOutput: Boolean(options.structuredOutput),
|
|
269
|
+
capabilityCeiling: options.capabilityCeiling,
|
|
270
|
+
inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
|
|
271
|
+
});
|
|
272
|
+
const launchContractDigest = launchBindingDigest({
|
|
273
|
+
definitionDigest: agentDefinitionDigest(agent),
|
|
274
|
+
task: shared.originalTask ?? task,
|
|
275
|
+
...(modelArg ? { model: modelArg } : {}),
|
|
276
|
+
modelCandidates: shared.modelCandidates,
|
|
277
|
+
...(resolvedThinking ? { thinking: resolvedThinking } : {}),
|
|
278
|
+
systemPrompt: effectiveSystemPrompt,
|
|
279
|
+
systemPromptMode: agent.systemPromptMode,
|
|
280
|
+
inheritProjectContext: agent.inheritProjectContext,
|
|
281
|
+
inheritSkills: agent.inheritSkills,
|
|
282
|
+
skills: shared.resolvedSkillNames ?? [],
|
|
283
|
+
tools: toolPlan.effectiveToolAllowlist,
|
|
284
|
+
extensions: toolPlan.extensionArgs,
|
|
285
|
+
mcpDirectTools: toolPlan.effectiveMcpTools,
|
|
286
|
+
...(options.outputPath ? { outputPath: options.outputPath } : {}),
|
|
287
|
+
outputMode: options.outputMode ?? "inline",
|
|
288
|
+
...(options.structuredOutput ? { structuredOutputSchema: options.structuredOutput.schema } : {}),
|
|
289
|
+
});
|
|
254
290
|
const result: SingleResult = withRunContext({
|
|
255
291
|
agent: agent.name,
|
|
256
292
|
task: shared.originalTask ?? task,
|
|
257
293
|
...(options.agentContract ? { agentContract: options.agentContract } : {}),
|
|
294
|
+
launchContractDigest,
|
|
258
295
|
exitCode: 0,
|
|
259
296
|
messages: [],
|
|
260
297
|
usage: emptyUsage(),
|
|
@@ -266,6 +303,8 @@ async function runSingleAttempt(
|
|
|
266
303
|
skillsWarning: shared.skillsWarning,
|
|
267
304
|
...(options.turnBudget ? { turnBudget: initialTurnBudgetState(options.turnBudget) } : {}),
|
|
268
305
|
...(options.toolBudget ? { toolBudget: initialToolBudgetState(options.toolBudget) } : {}),
|
|
306
|
+
...(options.capabilityCeiling ? { capabilityCeiling: options.capabilityCeiling } : {}),
|
|
307
|
+
...(capabilityAudit ? { capabilityAudit } : {}),
|
|
269
308
|
}, options.context);
|
|
270
309
|
const startTime = Date.now();
|
|
271
310
|
if (options.structuredOutput) {
|
|
@@ -1041,22 +1080,21 @@ async function runSingleAttempt(
|
|
|
1041
1080
|
result.exitCode = 1;
|
|
1042
1081
|
}
|
|
1043
1082
|
if (result.exitCode === 0 && !result.error) {
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
result.exitCode = errInfo.exitCode ?? 1;
|
|
1047
|
-
result.error = errInfo.details
|
|
1048
|
-
? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
|
|
1049
|
-
: `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
if (result.exitCode === 0 && !result.error) {
|
|
1053
|
-
const finalText = getFinalOutput(result.messages ?? []);
|
|
1083
|
+
const messages = result.messages ?? [];
|
|
1084
|
+
const finalText = getFinalOutput(messages);
|
|
1054
1085
|
const missingStructuredOutput = options.structuredOutput
|
|
1055
1086
|
? !existsSync(options.structuredOutput.outputPath)
|
|
1056
1087
|
: false;
|
|
1057
|
-
|
|
1088
|
+
const errInfo = detectSubagentError(messages);
|
|
1089
|
+
const missingOutput = !finalText?.trim() && (!options.structuredOutput || missingStructuredOutput);
|
|
1090
|
+
if (missingOutput && (!errInfo.hasError || hasEmptyTerminalAssistantResponse(messages))) {
|
|
1058
1091
|
result.exitCode = 1;
|
|
1059
1092
|
result.error = "Subagent produced no output (possible model cold-start or empty response).";
|
|
1093
|
+
} else if (errInfo.hasError) {
|
|
1094
|
+
result.exitCode = errInfo.exitCode ?? 1;
|
|
1095
|
+
result.error = errInfo.details
|
|
1096
|
+
? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
|
|
1097
|
+
: `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
|
|
1060
1098
|
}
|
|
1061
1099
|
}
|
|
1062
1100
|
if (options.structuredOutput && result.exitCode === 0 && !result.error) {
|
|
@@ -1194,6 +1232,10 @@ export async function runSync(
|
|
|
1194
1232
|
task: string,
|
|
1195
1233
|
options: RunSyncOptions,
|
|
1196
1234
|
): Promise<SingleResult> {
|
|
1235
|
+
options = {
|
|
1236
|
+
...options,
|
|
1237
|
+
capabilityCeiling: options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId),
|
|
1238
|
+
};
|
|
1197
1239
|
const agent = agents.find((a) => a.name === agentName);
|
|
1198
1240
|
if (!agent) {
|
|
1199
1241
|
return withRunContext({
|
|
@@ -1317,8 +1359,11 @@ export async function runSync(
|
|
|
1317
1359
|
toolCount: target.progressSummary?.toolCount,
|
|
1318
1360
|
error: target.error,
|
|
1319
1361
|
agentContract: target.agentContract,
|
|
1362
|
+
launchContractDigest: target.launchContractDigest,
|
|
1320
1363
|
execution: target.execution,
|
|
1321
1364
|
acceptance: target.acceptance,
|
|
1365
|
+
capabilityCeiling: target.capabilityCeiling,
|
|
1366
|
+
capabilityAudit: target.capabilityAudit,
|
|
1322
1367
|
review: target.review,
|
|
1323
1368
|
effects: target.effects,
|
|
1324
1369
|
...(transcriptWriter ? { transcriptPath: artifactPathsResult.transcriptPath } : {}),
|
|
@@ -1383,6 +1428,7 @@ export async function runSync(
|
|
|
1383
1428
|
artifactPaths: artifactPathsResult,
|
|
1384
1429
|
transcriptWriter,
|
|
1385
1430
|
attemptNotes,
|
|
1431
|
+
modelCandidates: candidates.map((candidate) => applyThinkingSuffix(candidate, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined)),
|
|
1386
1432
|
outputSnapshot,
|
|
1387
1433
|
originalTask: task,
|
|
1388
1434
|
});
|
|
@@ -47,6 +47,7 @@ import { formatControlIntercomMessage, formatControlNoticeMessage, resolveContro
|
|
|
47
47
|
import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
|
|
48
48
|
import { formatSpawnBudget, getSpawnBudgetSnapshot, grantSpawnBudget, preflightSpawnBudget, preflightSpawnBudgetGrant, reserveSpawnBudget } from "../shared/spawn-budget.ts";
|
|
49
49
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
50
|
+
import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
50
51
|
import { isAgentContractV1 } from "../shared/agent-contract.ts";
|
|
51
52
|
import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
52
53
|
import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
|
|
@@ -240,6 +241,7 @@ interface ExecutionContextData {
|
|
|
240
241
|
contextPolicy: AgentDefaultContextPolicy;
|
|
241
242
|
modelScope?: ModelScopeConfig;
|
|
242
243
|
parentSessionId: string | null;
|
|
244
|
+
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
243
245
|
}
|
|
244
246
|
|
|
245
247
|
function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
|
|
@@ -410,6 +412,8 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
|
|
|
410
412
|
...(result.transcriptError ? { transcriptError: result.transcriptError } : {}),
|
|
411
413
|
...(result.detachedReason ? { detachedReason: result.detachedReason } : {}),
|
|
412
414
|
...(result.acceptance ? { acceptance: result.acceptance } : {}),
|
|
415
|
+
...(result.capabilityCeiling ? { capabilityCeiling: result.capabilityCeiling } : {}),
|
|
416
|
+
...(result.capabilityAudit ? { capabilityAudit: result.capabilityAudit } : {}),
|
|
413
417
|
};
|
|
414
418
|
const recovered = previous?.children[index];
|
|
415
419
|
return child.status === "detached" && recovered && recovered.status !== "detached" ? recovered : child;
|
|
@@ -473,6 +477,8 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
|
|
|
473
477
|
...(input.result.transcriptError ? { transcriptError: input.result.transcriptError } : {}),
|
|
474
478
|
...(input.result.detachedReason ? { detachedReason: input.result.detachedReason } : {}),
|
|
475
479
|
...(input.result.acceptance ? { acceptance: input.result.acceptance } : {}),
|
|
480
|
+
...(input.result.capabilityCeiling ? { capabilityCeiling: input.result.capabilityCeiling } : {}),
|
|
481
|
+
...(input.result.capabilityAudit ? { capabilityAudit: input.result.capabilityAudit } : {}),
|
|
476
482
|
};
|
|
477
483
|
trimRememberedForegroundRuns(state);
|
|
478
484
|
const output = getSingleResultOutput(input.result).trim();
|
|
@@ -501,7 +507,7 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
|
|
|
501
507
|
});
|
|
502
508
|
}
|
|
503
509
|
|
|
504
|
-
function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string } | undefined {
|
|
510
|
+
function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } | undefined {
|
|
505
511
|
const requested = (params.id ?? params.runId)?.trim();
|
|
506
512
|
if (!requested || !state.foregroundRuns?.size || !state.currentSessionId) return undefined;
|
|
507
513
|
const sessionRuns = [...state.foregroundRuns.values()].filter((run) => run.sessionId === state.currentSessionId);
|
|
@@ -520,7 +526,16 @@ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: Subage
|
|
|
520
526
|
if (path.extname(child.sessionFile) !== ".jsonl") throw new Error(`Foreground run '${run.runId}' child ${index} session file must be a .jsonl file: ${child.sessionFile}`);
|
|
521
527
|
const sessionFile = path.resolve(child.sessionFile);
|
|
522
528
|
if (!fs.existsSync(sessionFile)) throw new Error(`Foreground run '${run.runId}' child ${index} session file does not exist: ${child.sessionFile}`);
|
|
523
|
-
return {
|
|
529
|
+
return {
|
|
530
|
+
runId: run.runId,
|
|
531
|
+
mode: run.mode,
|
|
532
|
+
state: "complete",
|
|
533
|
+
agent: child.agent,
|
|
534
|
+
index,
|
|
535
|
+
cwd: run.cwd,
|
|
536
|
+
sessionFile,
|
|
537
|
+
...(child.capabilityCeiling ? { capabilityCeiling: child.capabilityCeiling } : {}),
|
|
538
|
+
};
|
|
524
539
|
}
|
|
525
540
|
|
|
526
541
|
type AsyncResumeSourceTarget = ReturnType<typeof resolveAsyncResumeTarget> & { source: "async" };
|
|
@@ -534,6 +549,7 @@ type NestedResumeSourceTarget = {
|
|
|
534
549
|
index: number;
|
|
535
550
|
cwd?: string;
|
|
536
551
|
sessionFile: string;
|
|
552
|
+
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
537
553
|
};
|
|
538
554
|
type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget | NestedResumeSourceTarget;
|
|
539
555
|
|
|
@@ -886,6 +902,7 @@ function appendStepToAsyncChain(input: {
|
|
|
886
902
|
contextForAgent: contextPolicy.contextForAgent,
|
|
887
903
|
asyncDir: resolved.location.asyncDir,
|
|
888
904
|
validateOutputBindings: false,
|
|
905
|
+
capabilityCeiling: intersectSubagentCapabilityCeilings(status.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(asyncCtx.currentSessionId)),
|
|
889
906
|
});
|
|
890
907
|
if ("error" in built) {
|
|
891
908
|
return {
|
|
@@ -990,6 +1007,7 @@ function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "neste
|
|
|
990
1007
|
index: 0,
|
|
991
1008
|
cwd: asyncDir ? path.dirname(asyncDir) : undefined,
|
|
992
1009
|
sessionFile: validateNestedSessionFile(run, trustedSessionRoots),
|
|
1010
|
+
...(run.capabilityCeiling ? { capabilityCeiling: run.capabilityCeiling } : {}),
|
|
993
1011
|
};
|
|
994
1012
|
}
|
|
995
1013
|
|
|
@@ -1283,6 +1301,7 @@ async function resumeAsyncRun(input: {
|
|
|
1283
1301
|
controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
|
|
1284
1302
|
childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
|
|
1285
1303
|
globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit,
|
|
1304
|
+
capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
|
|
1286
1305
|
});
|
|
1287
1306
|
if (result.isError) return result;
|
|
1288
1307
|
const attachedId = result.details.asyncId ?? runId;
|
|
@@ -1352,6 +1371,7 @@ async function resumeAsyncRun(input: {
|
|
|
1352
1371
|
...(input.absoluteDeadlineAt !== undefined ? { absoluteDeadlineAt: input.absoluteDeadlineAt } : {}),
|
|
1353
1372
|
...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
|
|
1354
1373
|
...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
|
|
1374
|
+
capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, recoveryDescriptor?.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
|
|
1355
1375
|
});
|
|
1356
1376
|
if (result.isError) return result;
|
|
1357
1377
|
|
|
@@ -2092,6 +2112,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
2092
2112
|
turnBudget: data.turnBudget,
|
|
2093
2113
|
toolBudget: data.toolBudget,
|
|
2094
2114
|
configToolBudget: data.configToolBudget,
|
|
2115
|
+
capabilityCeiling: data.capabilityCeiling,
|
|
2095
2116
|
globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
|
|
2096
2117
|
});
|
|
2097
2118
|
}
|
|
@@ -2133,6 +2154,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
2133
2154
|
turnBudget: data.turnBudget,
|
|
2134
2155
|
toolBudget: data.toolBudget,
|
|
2135
2156
|
configToolBudget: data.configToolBudget,
|
|
2157
|
+
capabilityCeiling: data.capabilityCeiling,
|
|
2136
2158
|
globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
|
|
2137
2159
|
});
|
|
2138
2160
|
}
|
|
@@ -2190,6 +2212,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
2190
2212
|
turnBudget: data.turnBudget,
|
|
2191
2213
|
toolBudget: data.toolBudget,
|
|
2192
2214
|
configToolBudget: data.configToolBudget,
|
|
2215
|
+
capabilityCeiling: data.capabilityCeiling,
|
|
2193
2216
|
});
|
|
2194
2217
|
}
|
|
2195
2218
|
|
|
@@ -2265,6 +2288,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
2265
2288
|
toolBudget: data.toolBudget,
|
|
2266
2289
|
configToolBudget: data.configToolBudget,
|
|
2267
2290
|
globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
|
|
2291
|
+
capabilityCeiling: data.capabilityCeiling,
|
|
2268
2292
|
});
|
|
2269
2293
|
|
|
2270
2294
|
if (chainResult.requestedAsync) {
|
|
@@ -2321,6 +2345,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
2321
2345
|
turnBudget: data.turnBudget,
|
|
2322
2346
|
toolBudget: data.toolBudget,
|
|
2323
2347
|
configToolBudget: data.configToolBudget,
|
|
2348
|
+
capabilityCeiling: data.capabilityCeiling,
|
|
2324
2349
|
globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
|
|
2325
2350
|
});
|
|
2326
2351
|
}
|
|
@@ -2363,6 +2388,7 @@ interface ForegroundParallelRunInput {
|
|
|
2363
2388
|
state: SubagentState;
|
|
2364
2389
|
intercomEvents: IntercomEventBus;
|
|
2365
2390
|
parentSessionId: string | null;
|
|
2391
|
+
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
2366
2392
|
signal: AbortSignal;
|
|
2367
2393
|
runId: string;
|
|
2368
2394
|
sessionDirForIndex: (idx?: number) => string | undefined;
|
|
@@ -2605,6 +2631,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
2605
2631
|
outputMode: behavior?.outputMode,
|
|
2606
2632
|
maxSubagentDepth: input.maxSubagentDepths[index],
|
|
2607
2633
|
waitToolEnabled: input.waitToolEnabled,
|
|
2634
|
+
capabilityCeiling: input.capabilityCeiling,
|
|
2608
2635
|
controlConfig: input.controlConfig,
|
|
2609
2636
|
onControlEvent: input.onControlEvent,
|
|
2610
2637
|
onDetachedExit: (result) => updateRememberedForegroundChild(input.state, { runId: input.runId, mode: "parallel", cwd: taskCwd, sessionId: input.parentSessionId, index, result, events: input.intercomEvents }),
|
|
@@ -2915,6 +2942,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
2915
2942
|
state: deps.state,
|
|
2916
2943
|
intercomEvents: deps.pi.events,
|
|
2917
2944
|
parentSessionId: data.parentSessionId,
|
|
2945
|
+
capabilityCeiling: data.capabilityCeiling,
|
|
2918
2946
|
signal,
|
|
2919
2947
|
runId,
|
|
2920
2948
|
sessionDirForIndex,
|
|
@@ -3274,6 +3302,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3274
3302
|
turnBudget: data.turnBudget,
|
|
3275
3303
|
enforceHardTurnLimit: params.enforceHardTurnLimit,
|
|
3276
3304
|
toolBudget: effectiveToolBudget.toolBudget,
|
|
3305
|
+
capabilityCeiling: data.capabilityCeiling,
|
|
3277
3306
|
allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
|
|
3278
3307
|
});
|
|
3279
3308
|
} finally {
|
|
@@ -3991,6 +4020,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
3991
4020
|
contextPolicy,
|
|
3992
4021
|
modelScope,
|
|
3993
4022
|
parentSessionId: deps.state.currentSessionId,
|
|
4023
|
+
capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(deps.state.currentSessionId ?? undefined),
|
|
3994
4024
|
};
|
|
3995
4025
|
|
|
3996
4026
|
const foregroundDescription = effectiveParams.task?.trim()
|
|
@@ -29,10 +29,9 @@ const LEVEL_RANK: Record<Exclude<AcceptanceLevel, "auto">, number> = {
|
|
|
29
29
|
attested: 1,
|
|
30
30
|
checked: 2,
|
|
31
31
|
verified: 3,
|
|
32
|
-
reviewed: 4,
|
|
33
32
|
};
|
|
34
33
|
|
|
35
|
-
const VALID_LEVELS = new Set<AcceptanceLevel>(["auto", "none", "attested", "checked", "verified"
|
|
34
|
+
const VALID_LEVELS = new Set<AcceptanceLevel>(["auto", "none", "attested", "checked", "verified"]);
|
|
36
35
|
const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>([
|
|
37
36
|
"changed-files",
|
|
38
37
|
"tests-added",
|
|
@@ -48,7 +47,7 @@ const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify
|
|
|
48
47
|
const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
|
|
49
48
|
const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
|
|
50
49
|
const ACCEPTANCE_REVIEW_KEYS = new Set(["agent", "focus", "required"]);
|
|
51
|
-
const EXPLICIT_REVIEWED_UNAVAILABLE = "
|
|
50
|
+
const EXPLICIT_REVIEWED_UNAVAILABLE = "is an achieved status, not a requestable acceptance level. For a read-only reviewer call, omit acceptance. To require independent review of a writer result, use acceptance.review.required and orchestrate the reviewer separately.";
|
|
52
51
|
|
|
53
52
|
function normalizeLevel(level: AcceptanceLevel | undefined): Exclude<AcceptanceLevel, "auto"> | "auto" {
|
|
54
53
|
return level ?? "auto";
|
|
@@ -67,7 +66,6 @@ function requiredEvidenceForLevel(level: Exclude<AcceptanceLevel, "auto">): Acce
|
|
|
67
66
|
case "checked":
|
|
68
67
|
return ["changed-files", "tests-added", "commands-run", "residual-risks", "no-staged-files"];
|
|
69
68
|
case "verified":
|
|
70
|
-
case "reviewed":
|
|
71
69
|
return ["changed-files", "tests-added", "commands-run", "validation-output", "residual-risks", "no-staged-files"];
|
|
72
70
|
}
|
|
73
71
|
}
|
|
@@ -110,10 +108,10 @@ function inferLevel(input: {
|
|
|
110
108
|
reasons.push(input.async ? "async write-capable or risky run" : "risky write-capable run");
|
|
111
109
|
if (input.dynamic || input.dynamicGroup) reasons.push("dynamic fanout context");
|
|
112
110
|
return {
|
|
113
|
-
level: "
|
|
111
|
+
level: "checked",
|
|
114
112
|
reasons,
|
|
115
113
|
criteria: ["Implement the requested change without widening scope", "Return evidence sufficient for an independent acceptance review"],
|
|
116
|
-
evidence: requiredEvidenceForLevel("
|
|
114
|
+
evidence: requiredEvidenceForLevel("checked"),
|
|
117
115
|
review: { agent: "reviewer", required: true },
|
|
118
116
|
};
|
|
119
117
|
}
|
|
@@ -160,9 +158,9 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
160
158
|
if (input === undefined) return errors;
|
|
161
159
|
if (input === false) return errors;
|
|
162
160
|
if (typeof input === "string") {
|
|
163
|
-
if (
|
|
161
|
+
if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
162
|
+
else if (!VALID_LEVELS.has(input as AcceptanceLevel)) errors.push(`${pathLabel} has invalid level '${input}'.`);
|
|
164
163
|
else if (input === "none") errors.push(`${pathLabel} level "none" requires a reason; use { level: "none", reason: "..." }.`);
|
|
165
|
-
else if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
166
164
|
return errors;
|
|
167
165
|
}
|
|
168
166
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
@@ -173,13 +171,14 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
173
171
|
for (const key of Object.keys(value)) {
|
|
174
172
|
if (!ACCEPTANCE_CONFIG_KEYS.has(key)) errors.push(`${pathLabel}.${key} is not supported.`);
|
|
175
173
|
}
|
|
176
|
-
if (value.level
|
|
177
|
-
errors.push(`${pathLabel}.level
|
|
174
|
+
if (value.level === "reviewed") {
|
|
175
|
+
errors.push(`${pathLabel}.level ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
176
|
+
} else if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level as AcceptanceLevel))) {
|
|
177
|
+
errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified.`);
|
|
178
178
|
}
|
|
179
179
|
if (value.level === "none" && (typeof value.reason !== "string" || !value.reason.trim())) {
|
|
180
180
|
errors.push(`${pathLabel}.reason is required when level is none.`);
|
|
181
181
|
}
|
|
182
|
-
if (value.level === "reviewed") errors.push(`${pathLabel}.level ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
183
182
|
if (value.reason !== undefined && typeof value.reason !== "string") errors.push(`${pathLabel}.reason must be a string.`);
|
|
184
183
|
if (value.criteria !== undefined && !Array.isArray(value.criteria)) errors.push(`${pathLabel}.criteria must be an array.`);
|
|
185
184
|
if (Array.isArray(value.criteria)) {
|
|
@@ -362,10 +361,7 @@ export function resolveEffectiveAcceptance(input: {
|
|
|
362
361
|
(explicit.criteria?.length ? explicit.criteria : inferred.criteria) as Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }>,
|
|
363
362
|
evidence,
|
|
364
363
|
);
|
|
365
|
-
|
|
366
|
-
if (level === "reviewed" && input.explicit !== undefined && explicitLevel !== "reviewed" && explicit.review === undefined && review && review !== false) {
|
|
367
|
-
review = { ...review, required: false };
|
|
368
|
-
}
|
|
364
|
+
const review = explicit.review !== undefined ? explicit.review : inferred.review;
|
|
369
365
|
return {
|
|
370
366
|
level,
|
|
371
367
|
explicit: input.explicit !== undefined,
|
|
@@ -1061,8 +1057,10 @@ export async function evaluateAcceptance(input: {
|
|
|
1061
1057
|
reportOptional?: boolean;
|
|
1062
1058
|
}): Promise<AcceptanceLedger> {
|
|
1063
1059
|
const acceptance = input.acceptance;
|
|
1060
|
+
const initialStatus = acceptance.level === "none" ? "not-required" : "claimed";
|
|
1064
1061
|
const ledger: AcceptanceLedger = {
|
|
1065
|
-
status:
|
|
1062
|
+
status: initialStatus,
|
|
1063
|
+
evidenceStatus: initialStatus,
|
|
1066
1064
|
explicit: acceptance.explicit,
|
|
1067
1065
|
effectiveAcceptance: acceptance,
|
|
1068
1066
|
inferredReason: acceptance.inferredReason,
|
|
@@ -1084,11 +1082,13 @@ export async function evaluateAcceptance(input: {
|
|
|
1084
1082
|
if (parsed.report) {
|
|
1085
1083
|
ledger.childReport = parsed.report;
|
|
1086
1084
|
ledger.status = "attested";
|
|
1085
|
+
ledger.evidenceStatus = "attested";
|
|
1087
1086
|
} else if (!input.reportOptional || needsReport || parsed.error !== ACCEPTANCE_REPORT_NOT_FOUND) {
|
|
1088
1087
|
ledger.childReportParseError = parsed.error;
|
|
1089
1088
|
ledger.runtimeChecks.push({ id: "attestation", status: "failed", message: parsed.error ?? "Structured acceptance report missing." });
|
|
1090
1089
|
if (!input.reportOptional) {
|
|
1091
1090
|
ledger.status = "rejected";
|
|
1091
|
+
ledger.evidenceStatus = "rejected";
|
|
1092
1092
|
return ledger;
|
|
1093
1093
|
}
|
|
1094
1094
|
} else {
|
|
@@ -1103,6 +1103,7 @@ export async function evaluateAcceptance(input: {
|
|
|
1103
1103
|
];
|
|
1104
1104
|
if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1105
1105
|
ledger.status = "checked";
|
|
1106
|
+
ledger.evidenceStatus = "checked";
|
|
1106
1107
|
}
|
|
1107
1108
|
}
|
|
1108
1109
|
|
|
@@ -1110,6 +1111,7 @@ export async function evaluateAcceptance(input: {
|
|
|
1110
1111
|
if (acceptance.level === "verified" && acceptance.verify.length === 0) {
|
|
1111
1112
|
ledger.runtimeChecks.push({ id: "verification-config", status: "failed", message: "verified acceptance requires runtime verify commands." });
|
|
1112
1113
|
ledger.status = "rejected";
|
|
1114
|
+
ledger.evidenceStatus = "rejected";
|
|
1113
1115
|
return ledger;
|
|
1114
1116
|
}
|
|
1115
1117
|
ledger.verifyRuns = [];
|
|
@@ -1119,34 +1121,42 @@ export async function evaluateAcceptance(input: {
|
|
|
1119
1121
|
}
|
|
1120
1122
|
if (ledger.verifyRuns.some((run) => run.status === "failed" || run.status === "timed-out")) {
|
|
1121
1123
|
ledger.status = "rejected";
|
|
1124
|
+
ledger.evidenceStatus = "rejected";
|
|
1122
1125
|
return ledger;
|
|
1123
1126
|
}
|
|
1124
|
-
if (!ledger.runtimeChecks.some((check) => check.status === "failed"))
|
|
1127
|
+
if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1128
|
+
ledger.status = "verified";
|
|
1129
|
+
ledger.evidenceStatus = "verified";
|
|
1130
|
+
}
|
|
1125
1131
|
}
|
|
1126
1132
|
|
|
1127
1133
|
if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1128
1134
|
ledger.status = "rejected";
|
|
1135
|
+
ledger.evidenceStatus = "rejected";
|
|
1129
1136
|
return ledger;
|
|
1130
1137
|
}
|
|
1131
|
-
if (ledger.status === "claimed"
|
|
1138
|
+
if (ledger.status === "claimed") {
|
|
1132
1139
|
ledger.status = acceptance.level === "verified" ? "verified" : acceptance.level;
|
|
1140
|
+
ledger.evidenceStatus = ledger.status;
|
|
1133
1141
|
}
|
|
1134
1142
|
|
|
1135
|
-
if (acceptance.
|
|
1136
|
-
if (input.reviewResult) {
|
|
1143
|
+
if (acceptance.review && acceptance.review !== false) {
|
|
1144
|
+
if (input.reviewResult?.status === "reviewed") {
|
|
1137
1145
|
ledger.reviewResult = input.reviewResult;
|
|
1138
|
-
ledger.status =
|
|
1139
|
-
} else {
|
|
1140
|
-
|
|
1141
|
-
ledger.
|
|
1142
|
-
|
|
1146
|
+
ledger.status = "reviewed";
|
|
1147
|
+
} else if (input.reviewResult?.status === "blockers") {
|
|
1148
|
+
ledger.reviewResult = input.reviewResult;
|
|
1149
|
+
ledger.status = "rejected";
|
|
1150
|
+
} else if (acceptance.review.required !== false) {
|
|
1151
|
+
ledger.reviewResult = input.reviewResult ?? {
|
|
1152
|
+
status: "review-required",
|
|
1143
1153
|
findings: [{
|
|
1144
|
-
severity:
|
|
1145
|
-
issue: "
|
|
1154
|
+
severity: "non-blocking",
|
|
1155
|
+
issue: "Independent review has not been supplied.",
|
|
1146
1156
|
rationale: "The run cannot be marked reviewed from child evidence alone.",
|
|
1147
1157
|
}],
|
|
1148
1158
|
};
|
|
1149
|
-
|
|
1159
|
+
ledger.status = "review-required";
|
|
1150
1160
|
}
|
|
1151
1161
|
}
|
|
1152
1162
|
|
|
@@ -1154,8 +1164,10 @@ export async function evaluateAcceptance(input: {
|
|
|
1154
1164
|
}
|
|
1155
1165
|
|
|
1156
1166
|
export function buildSkippedAcceptanceLedger(acceptance: ResolvedAcceptanceConfig, input: { id: string; message: string }): AcceptanceLedger {
|
|
1167
|
+
const status = acceptance.level === "none" ? "not-required" : "rejected";
|
|
1157
1168
|
return {
|
|
1158
|
-
status
|
|
1169
|
+
status,
|
|
1170
|
+
evidenceStatus: status,
|
|
1159
1171
|
explicit: acceptance.explicit,
|
|
1160
1172
|
effectiveAcceptance: acceptance,
|
|
1161
1173
|
inferredReason: acceptance.inferredReason,
|
|
@@ -1173,7 +1185,6 @@ export function acceptanceFailureMessage(ledger: AcceptanceLedger): string | und
|
|
|
1173
1185
|
if (failedCheck) return `Acceptance rejected: ${failedCheck.message}`;
|
|
1174
1186
|
const failedVerify = ledger.verifyRuns.find((run) => run.status === "failed" || run.status === "timed-out");
|
|
1175
1187
|
if (failedVerify) return `Acceptance verification '${failedVerify.id}' ${failedVerify.status}.`;
|
|
1176
|
-
if (ledger.reviewResult?.status === "needs-parent-decision") return "Acceptance review required but no automatic reviewer result is available.";
|
|
1177
1188
|
if (ledger.reviewResult?.status === "blockers") return "Acceptance review found blockers.";
|
|
1178
1189
|
return "Acceptance rejected.";
|
|
1179
1190
|
}
|