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.
Files changed (36) hide show
  1. package/CHANGELOG.md +31 -4
  2. package/README.md +1 -1
  3. package/docs/configuration.md +3 -3
  4. package/docs/models.md +17 -3
  5. package/index.ts +10 -1
  6. package/package.json +1 -1
  7. package/prompts/council.md +10 -7
  8. package/skills/council-mode/SKILL.md +17 -16
  9. package/skills/pi-subagents/SKILL.md +5 -1
  10. package/skills/pi-subagents/references/execution-controls.md +2 -0
  11. package/skills/pi-subagents/references/prompting-and-roles.md +7 -0
  12. package/src/agents/agent-management.ts +85 -5
  13. package/src/agents/agents.ts +67 -24
  14. package/src/api/preflight.ts +7 -2
  15. package/src/extension/doctor.ts +10 -0
  16. package/src/extension/fanout-child.ts +3 -2
  17. package/src/extension/index.ts +4 -3
  18. package/src/extension/public-execution.ts +1 -1
  19. package/src/extension/tool-result.ts +19 -0
  20. package/src/inspectors/herdr/client.ts +3 -3
  21. package/src/runs/background/async-execution.ts +10 -5
  22. package/src/runs/background/async-retention.ts +1 -1
  23. package/src/runs/background/chain-root-attachment.ts +5 -0
  24. package/src/runs/background/result-watcher.ts +4 -3
  25. package/src/runs/background/stale-run-reconciler.ts +3 -0
  26. package/src/runs/background/subagent-runner.ts +26 -2
  27. package/src/runs/background/wait-completions.ts +2 -0
  28. package/src/runs/background/wait-tool.ts +4 -3
  29. package/src/runs/foreground/execution.ts +29 -1
  30. package/src/runs/foreground/subagent-executor.ts +14 -6
  31. package/src/runs/shared/model-fallback.ts +36 -15
  32. package/src/runs/shared/model-scope.ts +106 -39
  33. package/src/runs/shared/pi-args.ts +34 -1
  34. package/src/runs/shared/subagent-prompt-runtime.ts +36 -9
  35. package/src/shared/types.ts +13 -3
  36. 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 { ModelScopeConfig } from "../shared/model-scope.ts";
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
- input.modelScope === undefined ? {} : { scope: input.modelScope },
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, data.modelScope === undefined ? {} : { scope: data.modelScope });
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
- data.modelScope === undefined ? {} : { scope: data.modelScope },
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: data.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 ModelScopeConfig, type ModelScopeViolation, type ModelSource } from "./model-scope.ts";
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?: ModelScopeConfig;
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?.enforce) {
310
+ if (resolved && options?.scope) {
284
311
  const source: ModelSource = explicit === undefined ? "inherited" : (options.source ?? "inherited");
285
- const violation = checkModelScope(resolved, options.scope, source);
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?: ModelScopeConfig;
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
- if ((index > 0 || options?.scope?.strict === true) && options?.scope?.enforce) {
362
- const violation = checkModelScope(normalized, options.scope, "inherited");
363
- if (violation) {
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);