pi-subagents 0.53.0 → 0.54.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 +31 -4
- package/README.md +1 -1
- package/docs/configuration.md +3 -3
- package/docs/models.md +17 -3
- package/index.ts +10 -1
- package/package.json +1 -1
- package/prompts/council.md +10 -7
- package/skills/council-mode/SKILL.md +17 -16
- package/skills/pi-subagents/SKILL.md +5 -1
- package/skills/pi-subagents/references/execution-controls.md +2 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +7 -0
- package/src/agents/agent-management.ts +85 -5
- package/src/agents/agents.ts +67 -24
- package/src/api/preflight.ts +7 -2
- package/src/extension/doctor.ts +10 -0
- package/src/extension/fanout-child.ts +3 -2
- package/src/extension/index.ts +4 -3
- package/src/extension/public-execution.ts +1 -1
- package/src/extension/tool-result.ts +19 -0
- package/src/inspectors/herdr/client.ts +3 -3
- package/src/runs/background/async-execution.ts +10 -5
- package/src/runs/background/async-retention.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +5 -0
- package/src/runs/background/result-watcher.ts +4 -3
- package/src/runs/background/stale-run-reconciler.ts +3 -0
- package/src/runs/background/subagent-runner.ts +26 -2
- package/src/runs/background/wait-completions.ts +2 -0
- package/src/runs/background/wait-tool.ts +4 -3
- package/src/runs/foreground/execution.ts +29 -1
- package/src/runs/foreground/subagent-executor.ts +14 -6
- package/src/runs/shared/model-fallback.ts +36 -15
- package/src/runs/shared/model-scope.ts +106 -39
- package/src/runs/shared/pi-args.ts +34 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +36 -9
- package/src/shared/types.ts +13 -3
- package/src/watchdog/permission-arbiter.ts +59 -51
|
@@ -85,7 +85,7 @@ import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
|
|
|
85
85
|
import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
|
|
86
86
|
import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-fanout-budget.ts";
|
|
87
87
|
import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
|
|
88
|
-
import { formatModelAttemptNote, isRetryableModelFailure, recordRetryableModelFailure } from "../shared/model-fallback.ts";
|
|
88
|
+
import { formatModelAttemptNote, isContextOverflow, isRetryableModelFailure, recordRetryableModelFailure } from "../shared/model-fallback.ts";
|
|
89
89
|
import {
|
|
90
90
|
SUBAGENT_STARTUP_RETRY_DELAYS_MS,
|
|
91
91
|
formatSubagentExtensionConflictError,
|
|
@@ -236,6 +236,8 @@ interface StepResult {
|
|
|
236
236
|
model?: string;
|
|
237
237
|
attemptedModels?: string[];
|
|
238
238
|
modelAttempts?: ModelAttempt[];
|
|
239
|
+
/** True when the dispatch failed because the input exceeded the model's context window. */
|
|
240
|
+
contextOverflow?: boolean;
|
|
239
241
|
totalCost?: CostSummary;
|
|
240
242
|
artifactPaths?: ArtifactPaths;
|
|
241
243
|
outputSaveError?: string;
|
|
@@ -1257,6 +1259,7 @@ async function runSingleStepInner(
|
|
|
1257
1259
|
model: imported.model,
|
|
1258
1260
|
attemptedModels: imported.attemptedModels,
|
|
1259
1261
|
modelAttempts: imported.modelAttempts,
|
|
1262
|
+
contextOverflow: imported.contextOverflow,
|
|
1260
1263
|
totalCost: imported.totalCost,
|
|
1261
1264
|
structuredOutput: timedOut || stopped ? undefined : imported.structuredOutput,
|
|
1262
1265
|
structuredOutputPath: timedOut || stopped ? undefined : imported.structuredOutputPath,
|
|
@@ -1458,6 +1461,8 @@ async function runSingleStepInner(
|
|
|
1458
1461
|
// Escalated to "file" after an unexplained zero-activity startup failure so
|
|
1459
1462
|
// retries keep the task text out of argv (endpoint pre-exec scans may deny it).
|
|
1460
1463
|
let taskDeliveryOverride: SubagentTaskDelivery | undefined;
|
|
1464
|
+
let contextOverflow = false;
|
|
1465
|
+
let launchWarningsEmitted = false;
|
|
1461
1466
|
modelAttemptsLoop: while (modelIndex < candidates.length) {
|
|
1462
1467
|
if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
1463
1468
|
const candidate = candidates[modelIndex];
|
|
@@ -1479,7 +1484,7 @@ async function runSingleStepInner(
|
|
|
1479
1484
|
childIndex: ctx.flatIndex,
|
|
1480
1485
|
})
|
|
1481
1486
|
: undefined;
|
|
1482
|
-
const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit } = buildPiArgs(omitUndefinedProperties({
|
|
1487
|
+
const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit, warnings } = buildPiArgs(omitUndefinedProperties({
|
|
1483
1488
|
parentSessionId: step.parentSessionId,
|
|
1484
1489
|
baseArgs: ["--mode", "json", "-p"],
|
|
1485
1490
|
task,
|
|
@@ -1525,6 +1530,10 @@ async function runSingleStepInner(
|
|
|
1525
1530
|
childWatchdog,
|
|
1526
1531
|
waitToolEnabled: step.waitToolEnabled,
|
|
1527
1532
|
}));
|
|
1533
|
+
if (!launchWarningsEmitted && warnings.length > 0) {
|
|
1534
|
+
for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
|
|
1535
|
+
launchWarningsEmitted = true;
|
|
1536
|
+
}
|
|
1528
1537
|
if (step.definitionDigest) {
|
|
1529
1538
|
const toolPlan = resolvePiLaunchToolPlan(omitUndefinedProperties({
|
|
1530
1539
|
tools: step.tools,
|
|
@@ -1536,6 +1545,7 @@ async function runSingleStepInner(
|
|
|
1536
1545
|
structuredOutput: Boolean(effectiveStructuredOutput),
|
|
1537
1546
|
capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
|
|
1538
1547
|
inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
|
|
1548
|
+
permissionRules: step.permissionRules,
|
|
1539
1549
|
}));
|
|
1540
1550
|
launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
|
|
1541
1551
|
actualLaunchContractDigest = launchBindingDigest(omitUndefinedProperties({
|
|
@@ -1771,6 +1781,11 @@ async function runSingleStepInner(
|
|
|
1771
1781
|
}
|
|
1772
1782
|
const retryableModelFailure = isRetryableModelFailure(error);
|
|
1773
1783
|
if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
|
|
1784
|
+
if (isContextOverflow(error)) {
|
|
1785
|
+
contextOverflow = true;
|
|
1786
|
+
attemptNotes.push(`[fallback] ${attempt.model} failed: context overflow — the input exceeds this model's context window. Reduce the task input or use a model with a larger context window.`);
|
|
1787
|
+
break modelAttemptsLoop;
|
|
1788
|
+
}
|
|
1774
1789
|
if (!retryableModelFailure || modelIndex === candidates.length - 1) break modelAttemptsLoop;
|
|
1775
1790
|
attemptNotes.push(formatModelAttemptNote(attempt, candidates[modelIndex + 1]));
|
|
1776
1791
|
modelIndex += 1;
|
|
@@ -1908,6 +1923,7 @@ async function runSingleStepInner(
|
|
|
1908
1923
|
model: finalResult?.model,
|
|
1909
1924
|
attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
|
|
1910
1925
|
modelAttempts,
|
|
1926
|
+
contextOverflow: contextOverflow || undefined,
|
|
1911
1927
|
totalCost: costSummaryFromAttempts(modelAttempts),
|
|
1912
1928
|
artifactPaths,
|
|
1913
1929
|
outputSaveError: artifactErrors.outputSaveError,
|
|
@@ -2520,6 +2536,7 @@ async function runSubagent(
|
|
|
2520
2536
|
model: step.model,
|
|
2521
2537
|
attemptedModels: step.attemptedModels,
|
|
2522
2538
|
modelAttempts: step.modelAttempts,
|
|
2539
|
+
contextOverflow: step.contextOverflow,
|
|
2523
2540
|
})),
|
|
2524
2541
|
exitCode: state === "complete" || state === "paused" ? 0 : 1,
|
|
2525
2542
|
timestamp: now,
|
|
@@ -3869,6 +3886,7 @@ async function runSubagent(
|
|
|
3869
3886
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
|
|
3870
3887
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
|
|
3871
3888
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "modelAttempts", singleResult.modelAttempts);
|
|
3889
|
+
setOptionalProperty(requiredStatusStep(statusPayload, fi), "contextOverflow", singleResult.contextOverflow);
|
|
3872
3890
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
|
|
3873
3891
|
if (singleResult.totalCost) {
|
|
3874
3892
|
pendingParallelUsageCost = {
|
|
@@ -3938,6 +3956,7 @@ async function runSubagent(
|
|
|
3938
3956
|
model: pr.model,
|
|
3939
3957
|
attemptedModels: pr.attemptedModels,
|
|
3940
3958
|
modelAttempts: pr.modelAttempts,
|
|
3959
|
+
contextOverflow: pr.contextOverflow,
|
|
3941
3960
|
totalCost: pr.totalCost,
|
|
3942
3961
|
artifactPaths: pr.artifactPaths,
|
|
3943
3962
|
transcriptPath: pr.transcriptPath,
|
|
@@ -4263,6 +4282,7 @@ async function runSubagent(
|
|
|
4263
4282
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
|
|
4264
4283
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
|
|
4265
4284
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "modelAttempts", singleResult.modelAttempts);
|
|
4285
|
+
setOptionalProperty(requiredStatusStep(statusPayload, fi), "contextOverflow", singleResult.contextOverflow);
|
|
4266
4286
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
|
|
4267
4287
|
if (singleResult.totalCost) {
|
|
4268
4288
|
pendingParallelUsageCost = {
|
|
@@ -4366,6 +4386,7 @@ async function runSubagent(
|
|
|
4366
4386
|
model: pr.model,
|
|
4367
4387
|
attemptedModels: pr.attemptedModels,
|
|
4368
4388
|
modelAttempts: pr.modelAttempts,
|
|
4389
|
+
contextOverflow: pr.contextOverflow,
|
|
4369
4390
|
totalCost: pr.totalCost,
|
|
4370
4391
|
artifactPaths: pr.artifactPaths,
|
|
4371
4392
|
transcriptPath: pr.transcriptPath,
|
|
@@ -4583,6 +4604,7 @@ async function runSubagent(
|
|
|
4583
4604
|
model: singleResult.model,
|
|
4584
4605
|
attemptedModels: singleResult.attemptedModels,
|
|
4585
4606
|
modelAttempts: singleResult.modelAttempts,
|
|
4607
|
+
contextOverflow: singleResult.contextOverflow,
|
|
4586
4608
|
totalCost: singleResult.totalCost,
|
|
4587
4609
|
artifactPaths: singleResult.artifactPaths,
|
|
4588
4610
|
transcriptPath: singleResult.transcriptPath,
|
|
@@ -4661,6 +4683,7 @@ async function runSubagent(
|
|
|
4661
4683
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, flatIndex).thinking));
|
|
4662
4684
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "attemptedModels", singleResult.attemptedModels);
|
|
4663
4685
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "modelAttempts", singleResult.modelAttempts);
|
|
4686
|
+
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "contextOverflow", singleResult.contextOverflow);
|
|
4664
4687
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "totalCost", singleResult.totalCost);
|
|
4665
4688
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "error", stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error);
|
|
4666
4689
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "transcriptPath", singleResult.transcriptPath ?? requiredStatusStep(statusPayload, flatIndex).transcriptPath);
|
|
@@ -4935,6 +4958,7 @@ async function runSubagent(
|
|
|
4935
4958
|
model: r.model,
|
|
4936
4959
|
attemptedModels: r.attemptedModels,
|
|
4937
4960
|
modelAttempts: r.modelAttempts,
|
|
4961
|
+
contextOverflow: r.contextOverflow,
|
|
4938
4962
|
totalCost: r.totalCost,
|
|
4939
4963
|
artifactPaths: r.artifactPaths,
|
|
4940
4964
|
outputSaveError: r.outputSaveError,
|
|
@@ -44,6 +44,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
|
|
|
44
44
|
const childRunId = asNonEmptyString(child.runId);
|
|
45
45
|
const error = asNonEmptyString(child.error);
|
|
46
46
|
const model = asNonEmptyString(child.model);
|
|
47
|
+
const contextOverflow = child.contextOverflow === true;
|
|
47
48
|
return [{
|
|
48
49
|
...(agent ? { agent } : {}),
|
|
49
50
|
...(childRunId ? { runId: childRunId } : {}),
|
|
@@ -51,6 +52,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
|
|
|
51
52
|
...(outputState ? { outputState } : {}),
|
|
52
53
|
...(error ? { error } : {}),
|
|
53
54
|
...(model ? { model } : {}),
|
|
55
|
+
...(contextOverflow ? { contextOverflow: true } : {}),
|
|
54
56
|
...(artifactPaths ? { artifactPaths } : {}),
|
|
55
57
|
}];
|
|
56
58
|
})
|
|
@@ -3,6 +3,7 @@ import { SubagentWaitParams } from "../../extension/schemas.ts";
|
|
|
3
3
|
import type { Details, SubagentState } from "../../shared/types.ts";
|
|
4
4
|
import { resolveWaitToolConfig, waitForSubagents } from "./subagent-wait.ts";
|
|
5
5
|
import type { WaitSubscriptionManager } from "./wait-subscriptions.ts";
|
|
6
|
+
import { finalizeToolResult } from "../../extension/tool-result.ts";
|
|
6
7
|
|
|
7
8
|
export function registerWaitTool(pi: ExtensionAPI, state: SubagentState, enabled = resolveWaitToolConfig().enabled, subscriptions?: Pick<WaitSubscriptionManager, "arm">): void {
|
|
8
9
|
const tool: ToolDefinition<typeof SubagentWaitParams, Details> = {
|
|
@@ -21,14 +22,14 @@ In an interactive chat, do not call this merely to wait: return control to the u
|
|
|
21
22
|
|
|
22
23
|
Non-blocking subscriptions are visible in subagent status and differ from disabling waitTool: waitTool.enabled=false returns immediately without registering any future wake. Provider jobs are session-scoped and identified exactly, so replacing one job with another cannot hide a completion. Provider extensions must be explicitly loaded in this process. In a child agent, keep \`subagent_wait\` in the child tool allowlist and load each provider through the agent's extensions or subagentOnlyExtensions; this tool never loads providers or grants tools itself.${enabled ? "" : "\n\nConfigured behavior: subagent_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and returns immediately without blocking."}`,
|
|
23
24
|
parameters: SubagentWaitParams,
|
|
24
|
-
execute(_id, params, signal, onUpdate, ctx) {
|
|
25
|
-
return waitForSubagents(params, signal, {
|
|
25
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
26
|
+
return finalizeToolResult(await waitForSubagents(params, signal, {
|
|
26
27
|
state,
|
|
27
28
|
events: pi.events,
|
|
28
29
|
enabled,
|
|
29
30
|
onUpdate,
|
|
30
31
|
...(subscriptions && ctx?.hasUI ? { subscribe: (input) => subscriptions.arm(input) } : {}),
|
|
31
|
-
});
|
|
32
|
+
}));
|
|
32
33
|
},
|
|
33
34
|
};
|
|
34
35
|
pi.registerTool(tool);
|
|
@@ -73,6 +73,7 @@ import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleO
|
|
|
73
73
|
import {
|
|
74
74
|
buildModelCandidates,
|
|
75
75
|
formatModelAttemptNote,
|
|
76
|
+
isContextOverflow,
|
|
76
77
|
isRetryableModelFailure,
|
|
77
78
|
recordRetryableModelFailure,
|
|
78
79
|
} from "../shared/model-fallback.ts";
|
|
@@ -308,6 +309,7 @@ async function runSingleAttempt(
|
|
|
308
309
|
originalTask?: string;
|
|
309
310
|
taskDelivery?: SubagentTaskDelivery;
|
|
310
311
|
orcaProgressTab?: OrcaProgressTab;
|
|
312
|
+
launchWarnings: { emitted: boolean };
|
|
311
313
|
},
|
|
312
314
|
): Promise<SingleResult> {
|
|
313
315
|
const effectiveThinking = options.thinkingOverride ?? agent.thinking;
|
|
@@ -326,7 +328,7 @@ async function runSingleAttempt(
|
|
|
326
328
|
const permissionAuditPath = permissionRules && options.artifactsDir
|
|
327
329
|
? path.join(options.artifactsDir, "permission-audit", `${options.runId}-${options.index ?? 0}.jsonl`)
|
|
328
330
|
: undefined;
|
|
329
|
-
const { args, env: sharedEnv, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit } = buildPiArgs({
|
|
331
|
+
const { args, env: sharedEnv, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit, warnings } = buildPiArgs({
|
|
330
332
|
baseArgs: ["--mode", "json", "-p"],
|
|
331
333
|
task,
|
|
332
334
|
taskDelivery: shared.taskDelivery,
|
|
@@ -369,6 +371,10 @@ async function runSingleAttempt(
|
|
|
369
371
|
waitToolEnabled: options.waitToolEnabled,
|
|
370
372
|
capabilityCeiling: options.capabilityCeiling,
|
|
371
373
|
});
|
|
374
|
+
if (!shared.launchWarnings.emitted && warnings.length > 0) {
|
|
375
|
+
for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
|
|
376
|
+
shared.launchWarnings.emitted = true;
|
|
377
|
+
}
|
|
372
378
|
|
|
373
379
|
const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(shared.systemPrompt, options.turnBudget);
|
|
374
380
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
@@ -382,6 +388,7 @@ async function runSingleAttempt(
|
|
|
382
388
|
capabilityCeiling: options.capabilityCeiling,
|
|
383
389
|
inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
|
|
384
390
|
agentName: agent.name,
|
|
391
|
+
permissionRules,
|
|
385
392
|
});
|
|
386
393
|
const launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
|
|
387
394
|
const launchContractDigest = launchBindingDigest({
|
|
@@ -1051,6 +1058,20 @@ async function runSingleAttempt(
|
|
|
1051
1058
|
}
|
|
1052
1059
|
|
|
1053
1060
|
if (evt.type === "tool_result_end" && evt.message) {
|
|
1061
|
+
const toolResultCompletion = {
|
|
1062
|
+
toolCallId: (evt.message as { toolCallId?: unknown }).toolCallId ?? (evt as { toolCallId?: unknown }).toolCallId,
|
|
1063
|
+
toolName: (evt.message as { toolName?: unknown }).toolName ?? (evt as { toolName?: unknown }).toolName,
|
|
1064
|
+
};
|
|
1065
|
+
clearActiveToolTimeout(toolResultCompletion);
|
|
1066
|
+
const endedTool = removeActiveToolCall(toolResultCompletion);
|
|
1067
|
+
if (endedTool) {
|
|
1068
|
+
progress.recentTools.push({
|
|
1069
|
+
tool: endedTool.tool,
|
|
1070
|
+
args: endedTool.args,
|
|
1071
|
+
endMs: now,
|
|
1072
|
+
});
|
|
1073
|
+
refreshCurrentTool();
|
|
1074
|
+
}
|
|
1054
1075
|
result.messages!.push(evt.message);
|
|
1055
1076
|
const resultText = extractTextFromContent(evt.message.content);
|
|
1056
1077
|
if (options.toolBudget && pendingToolResult && resultText.includes("Tool budget hard limit reached")) {
|
|
@@ -1681,6 +1702,7 @@ async function runSyncCompletionInner(
|
|
|
1681
1702
|
const modelAttempts: ModelAttempt[] = [];
|
|
1682
1703
|
const aggregateUsage = emptyUsage();
|
|
1683
1704
|
const attemptNotes: string[] = [];
|
|
1705
|
+
const launchWarnings = { emitted: false };
|
|
1684
1706
|
let totalToolCount = 0;
|
|
1685
1707
|
let totalDurationMs = 0;
|
|
1686
1708
|
|
|
@@ -1772,6 +1794,7 @@ async function runSyncCompletionInner(
|
|
|
1772
1794
|
originalTask: task,
|
|
1773
1795
|
taskDelivery: taskDeliveryOverride,
|
|
1774
1796
|
orcaProgressTab,
|
|
1797
|
+
launchWarnings,
|
|
1775
1798
|
});
|
|
1776
1799
|
lastResult = result;
|
|
1777
1800
|
if (startupAttemptIndex === 0) {
|
|
@@ -1864,6 +1887,11 @@ async function runSyncCompletionInner(
|
|
|
1864
1887
|
}
|
|
1865
1888
|
const retryableModelFailure = isRetryableModelFailure(result.error);
|
|
1866
1889
|
if (retryableModelFailure) recordRetryableModelFailure(result.model ?? candidate, result.error);
|
|
1890
|
+
if (isContextOverflow(result.error)) {
|
|
1891
|
+
result.contextOverflow = true;
|
|
1892
|
+
attemptNotes.push(`[fallback] ${attempt.model} failed: context overflow — the input exceeds this model's context window. Reduce the task input or use a model with a larger context window.`);
|
|
1893
|
+
break modelAttemptsLoop;
|
|
1894
|
+
}
|
|
1867
1895
|
if (!retryableModelFailure || modelIndex === modelsToTry.length - 1) break modelAttemptsLoop;
|
|
1868
1896
|
attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[modelIndex + 1]));
|
|
1869
1897
|
break;
|
|
@@ -30,7 +30,7 @@ import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/
|
|
|
30
30
|
import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts";
|
|
31
31
|
import { buildModelCandidates, inheritsParentModel, normalizeParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, type ParentModel } from "../shared/model-fallback.ts";
|
|
32
32
|
import { formatRetainedChildren, listRetainedChildren } from "../background/retained-children.ts";
|
|
33
|
-
import type
|
|
33
|
+
import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
|
|
34
34
|
import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
|
|
35
35
|
import { recordRun } from "../shared/run-history.ts";
|
|
36
36
|
import {
|
|
@@ -853,6 +853,10 @@ function isAsyncRunNotFound(error: unknown): boolean {
|
|
|
853
853
|
return error instanceof Error && error.message.startsWith("Async run not found.");
|
|
854
854
|
}
|
|
855
855
|
|
|
856
|
+
function isMissingExactAsyncStatusFile(error: unknown, requested: string): boolean {
|
|
857
|
+
return error instanceof Error && error.message === `Status file not found for async run '${requested}'.`;
|
|
858
|
+
}
|
|
859
|
+
|
|
856
860
|
function isResumeAmbiguity(error: unknown): boolean {
|
|
857
861
|
return error instanceof Error && /Ambiguous .*run id prefix/.test(error.message);
|
|
858
862
|
}
|
|
@@ -906,7 +910,7 @@ function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState, o
|
|
|
906
910
|
throw new Error(`Resume id '${requested}' is ambiguous between foreground run '${foregroundTarget.runId}' and async run '${asyncTarget.runId}'. Provide a full run id.`);
|
|
907
911
|
}
|
|
908
912
|
if (foregroundTarget) {
|
|
909
|
-
if (isExactResumeError(asyncError, "async", requested)) throw asyncError;
|
|
913
|
+
if (isExactResumeError(asyncError, "async", requested) && !isMissingExactAsyncStatusFile(asyncError, requested)) throw asyncError;
|
|
910
914
|
if (isResumeAmbiguity(asyncError) && !resumeTargetExact(foregroundTarget, requested)) throw asyncError;
|
|
911
915
|
return foregroundTarget;
|
|
912
916
|
}
|
|
@@ -1866,6 +1870,7 @@ async function resumeAsyncRun(input: {
|
|
|
1866
1870
|
...(completed.model ? { model: completed.model } : {}),
|
|
1867
1871
|
...(completed.attemptedModels ? { attemptedModels: completed.attemptedModels } : {}),
|
|
1868
1872
|
...(completed.modelAttempts ? { modelAttempts: completed.modelAttempts } : {}),
|
|
1873
|
+
...(completed.contextOverflow ? { contextOverflow: true } : {}),
|
|
1869
1874
|
...(completed.structuredOutput !== undefined ? { structuredOutput: completed.structuredOutput } : {}),
|
|
1870
1875
|
...(completed.structuredOutputPath ? { structuredOutputPath: completed.structuredOutputPath } : {}),
|
|
1871
1876
|
...(completed.structuredOutputSchemaPath ? { structuredOutputSchemaPath: completed.structuredOutputSchemaPath } : {}),
|
|
@@ -2652,6 +2657,7 @@ function resolveStaticLaunchSummary(input: {
|
|
|
2652
2657
|
}): StaticLaunchSummary {
|
|
2653
2658
|
const agentConfig = input.agents.find((agent) => agent.name === input.agent);
|
|
2654
2659
|
const externalRunner = agentConfig?.runner?.type === "external-cli" || agentConfig?.runner?.type === "external-job";
|
|
2660
|
+
const modelScopes = resolveModelScopesForAgent(input.modelScope, input.agent, input.parentModel);
|
|
2655
2661
|
const model = externalRunner
|
|
2656
2662
|
? undefined
|
|
2657
2663
|
: resolveEffectiveSubagentModel(
|
|
@@ -2660,7 +2666,7 @@ function resolveStaticLaunchSummary(input: {
|
|
|
2660
2666
|
input.parentModel,
|
|
2661
2667
|
input.availableModels,
|
|
2662
2668
|
input.currentProvider,
|
|
2663
|
-
|
|
2669
|
+
modelScopes.length === 0 ? {} : { scope: modelScopes },
|
|
2664
2670
|
);
|
|
2665
2671
|
const thinkingOverride = externalRunner ? undefined : input.thinkingOverrideForTask(input.agent, input.index, model);
|
|
2666
2672
|
const thinking = externalRunner ? undefined : resolveEffectiveThinking(model, thinkingOverride ?? agentConfig?.thinking);
|
|
@@ -2890,9 +2896,10 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
2890
2896
|
const externalRunnerWithoutExplicitModel = (a.runner?.type === "external-cli" || a.runner?.type === "external-job")
|
|
2891
2897
|
&& params.model === undefined
|
|
2892
2898
|
&& (a.model === undefined || (a.modelSource?.type === "subagents.defaultModel" && a.model === a.modelSource.model));
|
|
2899
|
+
const modelScopes = resolveModelScopesForAgent(data.modelScope, a.name, parentModel);
|
|
2893
2900
|
const modelOverride = a.runner?.type === "external-cli" || a.runner?.type === "external-job"
|
|
2894
2901
|
? params.model ?? (externalRunnerWithoutExplicitModel ? undefined : a.model)
|
|
2895
|
-
: resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider,
|
|
2902
|
+
: resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, modelScopes.length === 0 ? {} : { scope: modelScopes });
|
|
2896
2903
|
const modelOverrideFromParent = inheritsParentModel(params.model as string | undefined, a.model, parentModel);
|
|
2897
2904
|
return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
|
|
2898
2905
|
agent: params.agent!,
|
|
@@ -3234,6 +3241,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3234
3241
|
const parentModel = data.parentModel;
|
|
3235
3242
|
const currentProvider = parentModel?.provider;
|
|
3236
3243
|
const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
3244
|
+
const modelScopes = resolveModelScopesForAgent(data.modelScope, agentConfig.name, parentModel);
|
|
3237
3245
|
let task = params.task ?? "";
|
|
3238
3246
|
let modelOverride: string | undefined = resolveEffectiveSubagentModel(
|
|
3239
3247
|
params.model as string | undefined,
|
|
@@ -3241,7 +3249,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3241
3249
|
parentModel,
|
|
3242
3250
|
availableModels,
|
|
3243
3251
|
currentProvider,
|
|
3244
|
-
|
|
3252
|
+
modelScopes.length === 0 ? {} : { scope: modelScopes },
|
|
3245
3253
|
);
|
|
3246
3254
|
const modelOverrideFromParent = inheritsParentModel(params.model as string | undefined, agentConfig.model, parentModel);
|
|
3247
3255
|
let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
|
|
@@ -3384,7 +3392,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3384
3392
|
thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride, modelOverrideFromParent),
|
|
3385
3393
|
availableModels,
|
|
3386
3394
|
preferredModelProvider: currentProvider,
|
|
3387
|
-
modelScope:
|
|
3395
|
+
modelScope: modelScopes,
|
|
3388
3396
|
skills: effectiveSkills,
|
|
3389
3397
|
structuredOutput: structuredRuntime,
|
|
3390
3398
|
agentContract: params.agentContract,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 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
|
-
import { checkModelScope, type
|
|
4
|
+
import { checkModelScope, type ModelScopeCheckRule, type ModelScopeViolation, type ModelSource } from "./model-scope.ts";
|
|
5
5
|
|
|
6
6
|
export type { AvailableModelInfo };
|
|
7
7
|
|
|
@@ -236,7 +236,7 @@ function resolveRequiredSubagentModelCandidate(
|
|
|
236
236
|
|
|
237
237
|
export interface ResolveSubagentModelOverrideOptions {
|
|
238
238
|
/** When set with `enforce: true`, out-of-scope models are rejected. */
|
|
239
|
-
scope?:
|
|
239
|
+
scope?: ModelScopeCheckRule | ModelScopeCheckRule[];
|
|
240
240
|
/** Origin of the requested model: explicit caller-supplied (hard error) vs inherited (warn). Defaults to `"inherited"`. */
|
|
241
241
|
source?: ModelSource;
|
|
242
242
|
/** Called for warn-severity violations instead of `console.warn`. */
|
|
@@ -247,6 +247,32 @@ function defaultScopeWarn(violation: ModelScopeViolation): void {
|
|
|
247
247
|
console.warn(`[pi-subagents] ${violation.message}`);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
function configuredScopes(scope: ModelScopeCheckRule | ModelScopeCheckRule[] | undefined): ModelScopeCheckRule[] {
|
|
251
|
+
return scope ? (Array.isArray(scope) ? scope : [scope]) : [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function throwForUnresolvedEnforcedInheritScope(scope: ModelScopeCheckRule | ModelScopeCheckRule[] | undefined, includeMixed = false): void {
|
|
255
|
+
const unresolvedInheritScope = configuredScopes(scope)
|
|
256
|
+
.find((entry) => entry.enforce === true && (includeMixed ? entry.allow?.includes(INHERIT_MODEL) : entry.allow?.length === 1 && entry.allow[0] === INHERIT_MODEL));
|
|
257
|
+
if (!unresolvedInheritScope) return;
|
|
258
|
+
const origin = unresolvedInheritScope.origin ?? "modelScope";
|
|
259
|
+
throw new Error(`Cannot enforce subagent model scope (${origin}): 'inherit' requires a current parent session model.`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function enforceModelScopes(
|
|
263
|
+
model: string,
|
|
264
|
+
scope: ModelScopeCheckRule | ModelScopeCheckRule[] | undefined,
|
|
265
|
+
source: ModelSource,
|
|
266
|
+
onWarn: ((violation: ModelScopeViolation) => void) | undefined,
|
|
267
|
+
): void {
|
|
268
|
+
const violations = configuredScopes(scope)
|
|
269
|
+
.map((entry) => checkModelScope(model, entry, source))
|
|
270
|
+
.filter((violation): violation is ModelScopeViolation => violation !== undefined);
|
|
271
|
+
const error = violations.find((violation) => violation.severity === "error");
|
|
272
|
+
if (error) throw new Error(error.message);
|
|
273
|
+
for (const violation of violations) (onWarn ?? defaultScopeWarn)(violation);
|
|
274
|
+
}
|
|
275
|
+
|
|
250
276
|
/**
|
|
251
277
|
* Resolve the `--model` override passed to a spawned subagent.
|
|
252
278
|
*
|
|
@@ -274,19 +300,16 @@ export function resolveSubagentModelOverride(
|
|
|
274
300
|
): string | undefined {
|
|
275
301
|
const trimmed = typeof requestedModel === "string" ? requestedModel.trim() : "";
|
|
276
302
|
const explicit = trimmed && trimmed !== INHERIT_MODEL ? trimmed : undefined;
|
|
303
|
+
if (!parentModel) throwForUnresolvedEnforcedInheritScope(options?.scope, explicit === undefined || options?.source === "inherited");
|
|
277
304
|
let resolved: string | undefined;
|
|
278
305
|
if (explicit === undefined) {
|
|
279
306
|
resolved = parentModel ? `${parentModel.provider}/${parentModel.id}` : undefined;
|
|
280
307
|
} else {
|
|
281
308
|
resolved = resolveRequiredSubagentModelCandidate(explicit, availableModels, preferredProvider);
|
|
282
309
|
}
|
|
283
|
-
if (resolved && options?.scope
|
|
310
|
+
if (resolved && options?.scope) {
|
|
284
311
|
const source: ModelSource = explicit === undefined ? "inherited" : (options.source ?? "inherited");
|
|
285
|
-
|
|
286
|
-
if (violation) {
|
|
287
|
-
if (violation.severity === "error") throw new Error(violation.message);
|
|
288
|
-
(options.onWarn ?? defaultScopeWarn)(violation);
|
|
289
|
-
}
|
|
312
|
+
enforceModelScopes(resolved, options.scope, source, options.onWarn);
|
|
290
313
|
}
|
|
291
314
|
return resolved;
|
|
292
315
|
}
|
|
@@ -318,7 +341,7 @@ export function resolveEffectiveSubagentModel(
|
|
|
318
341
|
|
|
319
342
|
export interface BuildModelCandidatesOptions {
|
|
320
343
|
/** Fallback models warn by default and throw when strict scope enforcement is enabled. */
|
|
321
|
-
scope?:
|
|
344
|
+
scope?: ModelScopeCheckRule | ModelScopeCheckRule[];
|
|
322
345
|
onWarn?: (violation: ModelScopeViolation) => void;
|
|
323
346
|
/** The primary model came from the running parent session, not configuration. */
|
|
324
347
|
primaryModelFromParent?: boolean;
|
|
@@ -341,6 +364,7 @@ export function buildModelCandidates(
|
|
|
341
364
|
preferredProvider?: string,
|
|
342
365
|
options?: BuildModelCandidatesOptions,
|
|
343
366
|
): string[] {
|
|
367
|
+
if (!primaryModel) throwForUnresolvedEnforcedInheritScope(options?.scope, true);
|
|
344
368
|
const seen = new Set<string>();
|
|
345
369
|
const candidates: string[] = [];
|
|
346
370
|
const rawCandidates = [primaryModel, ...(fallbackModels ?? [])];
|
|
@@ -358,12 +382,9 @@ export function buildModelCandidates(
|
|
|
358
382
|
continue;
|
|
359
383
|
}
|
|
360
384
|
if (seen.has(normalized)) continue;
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (violation.severity === "error") throw new Error(violation.message);
|
|
365
|
-
(options.onWarn ?? defaultScopeWarn)(violation);
|
|
366
|
-
}
|
|
385
|
+
const scopes = configuredScopes(options?.scope);
|
|
386
|
+
if (index > 0 || scopes.some((scope) => scope.enforce === true && scope.strict === true)) {
|
|
387
|
+
enforceModelScopes(normalized, scopes, "inherited", options?.onWarn);
|
|
367
388
|
}
|
|
368
389
|
seen.add(normalized);
|
|
369
390
|
candidates.push(normalized);
|