pi-subagents 0.58.0 → 0.59.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 (105) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +89 -4
  8. package/docs/workflows.md +93 -2
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +1 -1
  13. package/skills/pi-subagents/references/execution-controls.md +5 -1
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +24 -3
  16. package/src/agents/agent-management.ts +5 -17
  17. package/src/agents/agent-serializer.ts +4 -2
  18. package/src/agents/agents.ts +67 -26
  19. package/src/agents/runtime-agent-registry.ts +9 -16
  20. package/src/api/background-work.ts +5 -1
  21. package/src/api/delegation.ts +0 -7
  22. package/src/api/preflight.ts +6 -9
  23. package/src/extension/fanout-child.ts +5 -3
  24. package/src/extension/index.ts +51 -23
  25. package/src/extension/public-execution.ts +15 -2
  26. package/src/extension/schemas.ts +35 -10
  27. package/src/extension/tool-description.ts +18 -3
  28. package/src/intercom/result-intercom.ts +2 -0
  29. package/src/profiles/profiles.ts +5 -6
  30. package/src/runs/background/active-async-capacity.ts +2 -2
  31. package/src/runs/background/async-execution.ts +50 -35
  32. package/src/runs/background/async-job-tracker.ts +14 -12
  33. package/src/runs/background/async-resume.ts +23 -25
  34. package/src/runs/background/async-status-snapshot.ts +23 -261
  35. package/src/runs/background/async-status.ts +66 -7
  36. package/src/runs/background/chain-append.ts +6 -3
  37. package/src/runs/background/chain-root-attachment.ts +60 -8
  38. package/src/runs/background/fleet-view.ts +21 -11
  39. package/src/runs/background/notify.ts +158 -6
  40. package/src/runs/background/result-files.ts +2 -1
  41. package/src/runs/background/result-watcher.ts +2 -0
  42. package/src/runs/background/resume-guidance.ts +1 -1
  43. package/src/runs/background/retained-children.ts +1 -1
  44. package/src/runs/background/run-status.ts +18 -8
  45. package/src/runs/background/scheduled-runs.ts +86 -7
  46. package/src/runs/background/stale-run-reconciler.ts +10 -4
  47. package/src/runs/background/steering.ts +4 -14
  48. package/src/runs/background/subagent-runner.ts +346 -357
  49. package/src/runs/background/subagent-wait.ts +58 -10
  50. package/src/runs/background/terminal-run-index.ts +1 -1
  51. package/src/runs/background/wait-completions.ts +22 -1
  52. package/src/runs/background/wait-config.ts +23 -9
  53. package/src/runs/background/wait-tool.ts +9 -2
  54. package/src/runs/foreground/async-steering-action.ts +2 -2
  55. package/src/runs/foreground/execution.ts +114 -120
  56. package/src/runs/foreground/foreground-control.ts +3 -0
  57. package/src/runs/foreground/foreground-history.ts +1 -0
  58. package/src/runs/foreground/subagent-executor.ts +478 -239
  59. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  60. package/src/runs/shared/abort-recovery.ts +119 -0
  61. package/src/runs/shared/async-status-projection.ts +463 -0
  62. package/src/runs/shared/child-identity.ts +19 -4
  63. package/src/runs/shared/child-launch-plan.ts +151 -0
  64. package/src/runs/shared/completion-evidence.ts +89 -0
  65. package/src/runs/shared/completion-guard.ts +1 -1
  66. package/src/runs/shared/dynamic-fanout.ts +2 -2
  67. package/src/runs/shared/host-step-status.ts +230 -0
  68. package/src/runs/shared/lane-metadata.ts +105 -0
  69. package/src/runs/shared/mcp-config-sources.ts +42 -6
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  71. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  72. package/src/runs/shared/model-fallback.ts +9 -2
  73. package/src/runs/shared/nested-events.ts +6 -2
  74. package/src/runs/shared/nested-render.ts +7 -3
  75. package/src/runs/shared/parallel-handoff.ts +419 -7
  76. package/src/runs/shared/parallel-utils.ts +7 -0
  77. package/src/runs/shared/pi-args.ts +20 -2
  78. package/src/runs/shared/single-output.ts +27 -4
  79. package/src/runs/shared/subagent-prompt-runtime.ts +13 -4
  80. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  81. package/src/runs/shared/worktree.ts +18 -0
  82. package/src/shared/child-session-name.ts +46 -0
  83. package/src/shared/extension-context.ts +24 -0
  84. package/src/shared/formatters.ts +5 -2
  85. package/src/shared/launch-contract.ts +1 -1
  86. package/src/shared/settings.ts +9 -103
  87. package/src/shared/types.ts +198 -29
  88. package/src/shared/utils.ts +35 -55
  89. package/src/slash/delegation-adapters.ts +1 -8
  90. package/src/slash/delegation-request.ts +0 -4
  91. package/src/slash/slash-bridge.ts +1 -2
  92. package/src/slash/slash-commands.ts +369 -90
  93. package/src/slash/slash-live-state.ts +22 -11
  94. package/src/tui/fleet-status.ts +125 -74
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +316 -27
  97. package/src/watchdog/turn-delta.ts +1 -1
  98. package/src/workflows/chat-progress.ts +6 -3
  99. package/src/workflows/host-command.ts +230 -0
  100. package/src/workflows/scripted-workflow.ts +452 -38
  101. package/src/workflows/workflow-child-summary.ts +9 -5
  102. package/src/workflows/workflow-preflight.ts +270 -0
  103. package/src/workflows/workflow-receipt.ts +43 -4
  104. package/src/workflows/workflow-settlement.ts +246 -0
  105. package/src/runs/shared/turn-budget.ts +0 -98
@@ -32,7 +32,7 @@ An agent may set `runner.type: external-job` with a non-empty `provider` and opt
32
32
 
33
33
  External job profiles are async-only. The provider owns the remote job and Pi owns the async run record. Status persists provider name, provider job id, prompt digest, provider options, handle/conversation URLs when supplied, result artifact path, last known state, and provider failure code/message. Recovery uses existing provider job metadata to call `reattach` and `result`; it refuses to redispatch a prompt when the persisted provider job does not match the prompt digest.
34
34
 
35
- External job profiles do not support foreground/clarify, steer/resume, Pi models/tools/extensions/skills, tool or turn budgets, structured output, native child permissions, fallbacks, or Pi child sessions. Capacity conflicts fail closed and include the blocking provider job id when the provider supplies it.
35
+ External job profiles do not support foreground/clarify, steer/resume, Pi models/tools/extensions/skills, tool budgets, structured output, native child permissions, fallbacks, or Pi child sessions. Capacity conflicts fail closed and include the blocking provider job id when the provider supplies it.
36
36
 
37
37
  ### Single agent
38
38
 
@@ -84,6 +84,8 @@ For one host-run verification command, pass `gate: "npm test"` on a `runs.run`/`
84
84
 
85
85
  Completed workflow children from this parent session stay addressable as retained children. `subagent({ action: "children.list" })` lists up to the last 10 with run ids and reports each row as `resumable` or `not resumable` with a reason. Resume only rows reported `resumable`. For a retained-child challenge, use `resume` instead of `steer` when the child is complete. If no retained child is resumable, launch a same-role fallback challenge and label it as fallback. A later workflow continues a resumable child with `runs.run(key, { resume: "<run-id>", task: "follow-up" })`. Inside `workflowScript`, awaiting that call waits for the revived child to finish and returns its completed output and new `runId`; top-level `{ action: "resume" }` remains detached. Pass explicit follow-up task text. Assign each returned child result back to the loop variable because every resume can return a new retained `runId`; always resume the latest returned id. `resume` and `agent` are mutually exclusive, the revived child keeps its stored agent/model/tool contract, and `gate` is rejected on retained resume items.
86
86
 
87
+ Each workflow key identifies one result lane: use a new stable workflow key for every distinct retained resume pass; same-key calls are reused only when launch parameters are identical, and incompatible parameters are rejected.
88
+
87
89
  Terminal async workflows also persist `workflow-receipt.json` beside `status.json`. It maps each stable child key to its agent, requested and resolved context when known, latest run id, resumability, output reference, and continuation lineage. A later workflow can resume the latest retained child without copying its run id:
88
90
 
89
91
  ```js
@@ -118,6 +120,8 @@ subagent({
118
120
 
119
121
  File-only output mode works for workflowScript child launches. Use relative child output paths for scratch reports so the runtime stores them under the run artifact directory and age-based cleanup can remove them. Use absolute paths only for user-approved durable destinations, such as session memory, a docs folder outside the repo, or a known handoff path. For cross-codebase waves, include the repo slug or lane key in each output path so reports from different repositories cannot collide.
120
122
 
123
+ The `output` field is the API binding; a filename mentioned in task text is only instruction and does not override runtime routing. When a later workflow step or parent needs a durable file, set `output` on `runs.run`/`runs.all` and return the child’s `outputReference`, `outputPathMapping`, or `artifactPaths`; arbitrary literal strings returned by workflow JavaScript are not rewritten. Omitted child output may use a managed aggregate-derived sibling path.
124
+
121
125
  For review fanout where the parent continues a local audit:
122
126
 
123
127
  ```typescript
@@ -18,7 +18,7 @@ subagent({ action: "list" })
18
18
  subagent({ action: "children.list" })
19
19
  ```
20
20
 
21
- Lists up to the last 10 retained workflow children from this parent session with explicit `resumable` or `not resumable` rows. Resume only rows reported `resumable`. Send a simple follow-up or implementation challenge with `subagent({ action: "resume", id: "<run-id>", message: "..." })`. Continue one inside a workflow with `runs.run(key, { resume: "<run-id>", task: "follow-up" })`; the revived child keeps its stored agent, model, and tool contract. If no resumable child is listed, start a same-role fallback challenge and label it as fallback. `steer` with `mode: "follow_up"` only queues text for the next `resume` when the child has already completed.
21
+ Lists up to the last 10 retained workflow children from this parent session with explicit `resumable` or `not resumable` rows. Resume only rows reported `resumable`. Send a simple follow-up or implementation challenge with `subagent({ action: "resume", id: "<run-id>", message: "..." })`. Continue one inside a workflow with `runs.run(key, { resume: "<run-id>", task: "follow-up" })`; each workflow key identifies one result lane, so use a new stable workflow key for every distinct retained resume pass. Same-key calls are reused only when launch parameters are identical, and incompatible parameters are rejected. The revived child keeps its stored agent, model, and tool contract. If no resumable child is listed, start a same-role fallback challenge and label it as fallback. `steer` with `mode: "follow_up"` only queues text for the next `resume` when the child has already completed.
22
22
 
23
23
  ### Refinement overlays
24
24
 
@@ -126,7 +126,6 @@ That is only a starting point. Omit `package` for the traditional unqualified ru
126
126
  - `acceptanceRole`
127
127
  - `async` — single-agent default for background launch (`true`/`false`); explicit tool-call `async` wins
128
128
  - `timeoutMs` — single-agent default run-level max runtime in ms; foreground calls use a 30-minute package default only when neither the call nor agent provides one (tool alias `maxRuntimeMs` is also accepted)
129
- - `turnBudget` — single-agent default `{ maxTurns, graceTurns? }` JSON object
130
129
 
131
130
  `aliases` is an optional comma-separated or block-list set of alternate names for selecting an agent. Aliases resolve to the canonical `name` for execution, status, persistence, and config. Exact canonical names take precedence over aliases, and alias collisions between distinct canonical agents fail as ambiguous. Management create/update accepts a comma-separated string, string array, or `false`/empty string to clear aliases.
132
131
 
@@ -14,7 +14,7 @@ Parent extensions may register a session-scoped, out-of-band ceiling through `pi
14
14
  - **Recon and planning**: use `scout`, then write a plan when needed
15
15
  - **Parallel exploration**: run multiple non-conflicting tasks concurrently
16
16
  - **Regular skill specialists**: when discovery shows proactive skill subagent suggestions and the current work is broad enough, launch a small fresh-context fanout that asks one subagent per relevant regularly used skill to apply that skill's perspective to the task
17
- - **Long-running work**: launch async/background runs and inspect them later. For mutation-capable work, bound the delivery slice and elapsed runtime, then request checkpoints after active tool work returns. Reserve hard turn and tool-call caps for explicitly read-only children.
17
+ - **Long-running work**: launch async/background runs and inspect them later. For mutation-capable work, bound the delivery slice and elapsed runtime, then request checkpoints after active tool work returns. Reserve hard tool-call caps for explicitly read-only children.
18
18
  - **Subagent control**: watch needs-attention signals and soft-interrupt only when a delegated run is genuinely blocked
19
19
  - **Agent authoring**: create, update, or override project agents. Treat saved chain records as legacy inspection or migration inputs, not as a current authoring target.
20
20
 
@@ -27,6 +27,7 @@ Agents use the `subagent(...)` tool with `workflowScript` for execution, and `ac
27
27
  - `/subagents` — interactive admin for inspecting agents and editing model, thinking, or system prompt
28
28
  - `/subagents-stop [run-id]` — stop a current-session top-level async run; opens a selector when no id is given
29
29
  - `/subagents-detach [run-id]` — detach an active foreground single-subagent run without terminating its child
30
+ - `/subagents-steer <run-id> [--child <child-id>] <message>` — steer a live async run (or one child of it) from non-TUI sessions and RPC hosts
30
31
  - `/subagent-cost` — show parent plus child token usage and cost for the session
31
32
  - `/subagents-fleet` — open the live fleet inspector with per-child controls; `Ctrl+Alt+F` opens it during an active foreground turn, `↑↓`/`jk` selects children, `PgUp`/`PgDn` scrolls transcript detail, `s` steers the selected live async child, and `D` stops its top-level async run after confirmation
32
33
  - `/subagents-watchdog` — inspect or configure the opt-in adversarial change watchdog (model, on/off, recommend-model, check)
@@ -90,7 +91,7 @@ subagent({
90
91
 
91
92
  Use this when the user wants implementation or current diff review to continue until reviewers stop finding fixes worth doing now. Keep the loop in the parent session: one async `worker` implements or fixes, fresh-context `reviewer` agents inspect the actual repo and diff, the parent synthesizes accepted fixes, and one async forked `worker` applies them. The parent can express the sequence up front as an async/background `workflowScript` when the workflow is known, or continue with explicit follow-up workflowScript runs after each async completion. For an initial workflow, pass `async: true` so the main chat is unblocked. Treat an async implementation worker handoff as an intermediate state, not final completion, unless the user explicitly asked for worker-only work, review-only output, or to stop after implementation. Stop when reviewers find no P0 blockers or P1 fixes worth doing now, remaining P2 feedback is optional or deferred, an unapproved product/scope/architecture decision appears, or the max review-round cap is reached. Default to 3 review rounds unless the user sets a different cap. Do not loop for optional polish, and do not let children launch subagents or decide the loop outcome.
92
93
 
93
- As a conservative orchestration policy, do not pass `turnBudget` or a hard `toolBudget` to an implementation worker, fix worker, reviewer with edit authority, or other mutation-capable child. The default tool budget blocks read/search tools rather than mutation tools, but count limits still do not measure delivery safety. Use a narrow task plus an outer elapsed deadline with enough margin, then request a checkpoint after the current tool returns. The checkpoint should report changed files, build/test state, remaining work, and commit or PR state. An elapsed timeout is not a mutation-safe boundary and must not be used as the checkpoint trigger.
94
+ As a conservative orchestration policy, do not pass a hard `toolBudget` to an implementation worker, fix worker, reviewer with edit authority, or other mutation-capable child. The default tool budget blocks read/search tools rather than mutation tools, but count limits still do not measure delivery safety. Use a narrow task plus an outer elapsed deadline with enough margin, then request a checkpoint after the current tool returns. The checkpoint should report changed files, build/test state, remaining work, and commit or PR state. An elapsed timeout is not a mutation-safe boundary and must not be used as the checkpoint trigger.
94
95
 
95
96
  ### Parallel research technique
96
97
 
@@ -192,6 +193,26 @@ For one run, use inline config:
192
193
 
193
194
  For persistent tweaks, edit `subagents.agentOverrides` in user or project settings. User overrides apply everywhere. Project overrides apply only in that repo and win over user overrides. Use `/subagents-models` or `subagent({ action: "models" })` to inspect the live mapping after settings and overrides load.
194
195
 
196
+ Provider-scoped entries can layer on top of the default override for the active parent session provider. The provider is selected once from the parent model before child model fallback starts, so fallback attempts cannot switch configuration. Within each settings file, the provider entry wins per field; project settings still win over user settings.
197
+
198
+ ```json
199
+ {
200
+ "subagents": {
201
+ "agentOverrides": {
202
+ "worker": { "thinking": "medium" }
203
+ },
204
+ "agentOverridesByProvider": {
205
+ "github-copilot": {
206
+ "worker": { "model": "github-copilot/gpt-5-mini" }
207
+ },
208
+ "openrouter": {
209
+ "worker": { "model": "openrouter/openai/gpt-5-mini" }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ ```
215
+
195
216
  Model ids do not have to be exact. Separator variations (`claude-haiku-4.5` vs `claude-haiku-4-5`), case (`Claude-Sonnet-4`), and optional trailing date stamps (`claude-haiku-4-5-20251001`) all resolve to the same registry model. Exact `provider/id` wins; a qualified `provider/model` never switches providers. To constrain subagents to a budget or compliance profile, set `subagents.modelScope: { enforce: true, allow: ["anthropic/*", "openai/gpt-5-*"] }` in user or project settings. Out-of-scope models you pass explicitly error and abort; models inherited from frontmatter, `subagents.defaultModel`, agent frontmatter, or the parent session only warn.
196
217
 
197
218
  For model fleets, use the profile commands instead of hand-editing repeated overrides: `/subagents-refresh-provider-models <provider>`, `/subagents-generate-profiles <provider>`, `/subagents-load-profile <name>`, and `/subagents-check-profile <name>`. Profiles live under `~/.pi/agent/profiles/pi-subagents/` and replace only `settings.subagents` when loaded.
@@ -206,7 +227,7 @@ A strong subagent prompt usually includes:
206
227
  - **Authority boundary**: whether the child may read, edit, commit, push, comment, close, merge, publish, or release. Omit or forbid actions that are not approved.
207
228
  - **Context/evidence**: relevant plan paths, files, diffs, decisions, or user constraints already approved.
208
229
  - **Success criteria**: what must be true before the child can finish.
209
- - **Hard constraints**: true invariants only, such as no edits for review-only tasks, one writer thread, child must not run subagents unless it is an explicitly assigned `tools: subagent` fanout child, or escalation for unapproved decisions.
230
+ - **Hard constraints**: true invariants only, such as no edits for review-only tasks, one writer thread, child must not run subagents unless it is explicitly authorized through `tools: subagent` or `allowNestedSubagents: true`, or escalation for unapproved decisions.
210
231
  - **Validation**: targeted checks to run, or the next-best check when validation is impossible.
211
232
  - **Output**: the expected summary shape, artifact path, or finding format. Use managed artifact paths for scratch reports; reserve repo-qualified absolute paths for durable handoffs that the user approved.
212
233
  - **Stop rules**: when to ask via `intercom` or `contact_supervisor`, when to stop after enough evidence, and when not to keep searching.
@@ -31,7 +31,6 @@ import { parseFrontmatter, parseFrontmatterList } from "./frontmatter.ts";
31
31
  import { toModelInfo } from "../shared/model-info.ts";
32
32
  import { resolveSubagentModelOverride, type ParentModel } from "../runs/shared/model-fallback.ts";
33
33
  import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
34
- import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts";
35
34
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
36
35
  import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
37
36
  import type { AcceptanceInput, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
@@ -331,7 +330,6 @@ export function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<
331
330
  if (hasKey(cfg, "defaultContext")) changed("defaultContext");
332
331
  if (hasKey(cfg, "async")) changed("async");
333
332
  if (hasKey(cfg, "timeoutMs")) changed("timeoutMs");
334
- if (hasKey(cfg, "turnBudget")) changed("turnBudget");
335
333
  if (hasKey(cfg, "acceptance")) changed("acceptance");
336
334
  if (hasKey(cfg, "acceptanceRole")) changed("acceptanceRole");
337
335
  if (hasKey(cfg, "output")) changed("output");
@@ -511,15 +509,6 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
511
509
  else if (typeof cfg.timeoutMs === "number" && Number.isInteger(cfg.timeoutMs) && cfg.timeoutMs > 0) target.defaultTimeoutMs = cfg.timeoutMs;
512
510
  else return "config.timeoutMs must be a positive integer or false when provided.";
513
511
  }
514
- if (hasKey(cfg, "turnBudget")) {
515
- if (cfg.turnBudget === false || cfg.turnBudget === "") delete target.defaultTurnBudget;
516
- else {
517
- const resolved = resolveTurnBudgetConfig(cfg.turnBudget, "config.turnBudget");
518
- if (resolved.error) return resolved.error;
519
- if (resolved.turnBudget !== undefined) target.defaultTurnBudget = resolved.turnBudget;
520
- else delete target.defaultTurnBudget;
521
- }
522
- }
523
512
  if (hasKey(cfg, "acceptance")) {
524
513
  if (cfg.acceptance === "") delete target.defaultAcceptance;
525
514
  else {
@@ -724,7 +713,6 @@ function formatAgentDetail(agent: AgentConfig): string {
724
713
  if (agent.defaultContext) lines.push(`Default context: ${agent.defaultContext}`);
725
714
  if (agent.defaultAsync !== undefined) lines.push(`Async: ${agent.defaultAsync ? "true" : "false"}`);
726
715
  if (agent.defaultTimeoutMs !== undefined) lines.push(`Timeout: ${agent.defaultTimeoutMs}ms`);
727
- if (agent.defaultTurnBudget) lines.push(`Turn budget: ${JSON.stringify(agent.defaultTurnBudget)}`);
728
716
  if (agent.defaultAcceptance !== undefined) lines.push(`Acceptance: ${typeof agent.defaultAcceptance === "object" ? JSON.stringify(agent.defaultAcceptance) : String(agent.defaultAcceptance)}`);
729
717
  if (agent.acceptanceRole) lines.push(`Acceptance role: ${agent.acceptanceRole}`);
730
718
  if (agent.source === "builtin") lines.push(`Disabled: ${agent.disabled ? "true" : "false"}`);
@@ -746,7 +734,7 @@ function formatAgentDetail(agent: AgentConfig): string {
746
734
 
747
735
  export function handleList(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
748
736
  const scope = normalizeListScope(params.agentScope) ?? "both";
749
- const d = discoverAgentsAll(ctx.cwd);
737
+ const d = discoverAgentsAll(ctx.cwd, ctx.model?.provider);
750
738
  let scopedAgents = mergeAgentsForScope(scope, d.user, d.project, d.builtin, d.package);
751
739
  if (ctx.runtimeAgentOwner && listRuntimeAgentConfigs(ctx.runtimeAgentOwner).length > 0) {
752
740
  const configuredAgents: AgentConfig[] = [
@@ -807,7 +795,7 @@ function handleModels(params: ManagementParams, ctx: ManagementContext): AgentTo
807
795
  return result(`Builtin agent '${requestedAgent}' not found. Available: ${BUILTIN_AGENT_NAMES.join(", ")}.`, true);
808
796
  }
809
797
 
810
- const discovered = discoverAgentsAll(ctx.cwd);
798
+ const discovered = discoverAgentsAll(ctx.cwd, ctx.model?.provider);
811
799
  const builtinByName = new Map(discovered.builtin.map((agent) => [agent.name, agent]));
812
800
  const resolveBuiltinModelAgent = (name: string): AgentConfig | undefined => builtinByName.get(name) ?? resolveAgentName(name, discovered.builtin).agent;
813
801
  const availableModels = ctx.modelRegistry.getAvailable().map(toModelInfo);
@@ -886,7 +874,7 @@ function handleGet(params: ManagementParams, ctx: ManagementContext): AgentToolR
886
874
  if (!params.agent) return result("Specify 'agent' for get.", true);
887
875
  const scope = normalizeListScope(params.agentScope);
888
876
  if (!scope) return result("agentScope must be 'user', 'project', or 'both' for get.", true);
889
- const discovered = discoverAgentsAll(ctx.cwd);
877
+ const discovered = discoverAgentsAll(ctx.cwd, ctx.model?.provider);
890
878
  const matches = findAgentsInDiscovery(params.agent, discovered, scope);
891
879
  const diagnostics = diagnosticsForScope(discovered.agentDiagnostics, scope);
892
880
  const rawName = params.agent.trim();
@@ -916,7 +904,7 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
916
904
  const runtimeName = buildRuntimeName(name, parsedPackage.packageName);
917
905
  const scopeRaw = cfg.scope ?? "user";
918
906
  if (scopeRaw !== "user" && scopeRaw !== "project") return result("config.scope must be 'user' or 'project'.", true);
919
- const scope = scopeRaw as ManagementScope;
907
+ const scope = scopeRaw;
920
908
  if (hasKey(cfg, "steps")) return result("Durable chain definitions were removed; use workflowScript or /prompt-workflow for repeatable workflows.", true);
921
909
  const d = discoverAgentsAll(ctx.cwd);
922
910
  const projectConfigDir = getProjectConfigDir(ctx.cwd);
@@ -1164,7 +1152,7 @@ function handleReset(params: ManagementParams, ctx: ManagementContext): AgentToo
1164
1152
  }
1165
1153
 
1166
1154
  export function handleManagementAction(action: string, params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
1167
- switch (action as ManagementAction) {
1155
+ switch (action) {
1168
1156
  case "list": return handleList(params, ctx);
1169
1157
  case "get": return handleGet(params, ctx);
1170
1158
  case "models": return handleModels(params, ctx);
@@ -9,6 +9,7 @@ export const KNOWN_FIELDS = new Set([
9
9
  "alias",
10
10
  "aliases",
11
11
  "tools",
12
+ "allowNestedSubagents",
12
13
  "model",
13
14
  "fallbackModels",
14
15
  "fast",
@@ -21,7 +22,6 @@ export const KNOWN_FIELDS = new Set([
21
22
  "async",
22
23
  "timeoutMs",
23
24
  "toolTimeoutMs",
24
- "turnBudget",
25
25
  "acceptance",
26
26
  "acceptanceRole",
27
27
  "skill",
@@ -70,6 +70,9 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
70
70
  ];
71
71
  const toolsValue = joinComma(tools);
72
72
  if (toolsValue || preserve("tools")) lines.push(`tools: ${toolsValue ?? ""}`);
73
+ if (config.allowNestedSubagents === true || preserve("allowNestedSubagents")) {
74
+ lines.push(`allowNestedSubagents: ${config.allowNestedSubagents === undefined ? "" : config.allowNestedSubagents ? "true" : "false"}`);
75
+ }
73
76
 
74
77
  if (config.model || preserve("model")) lines.push(`model: ${config.model ?? ""}`);
75
78
  const fallbackModelsValue = joinComma(config.fallbackModels);
@@ -94,7 +97,6 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
94
97
  if (config.defaultAsync !== undefined || preserve("async")) lines.push(`async: ${config.defaultAsync === undefined ? "" : config.defaultAsync ? "true" : "false"}`);
95
98
  if (config.defaultTimeoutMs !== undefined || preserve("timeoutMs")) lines.push(`timeoutMs: ${config.defaultTimeoutMs ?? ""}`);
96
99
  if (config.defaultToolTimeoutMs !== undefined || preserve("toolTimeoutMs")) lines.push(`toolTimeoutMs: ${config.defaultToolTimeoutMs ?? ""}`);
97
- if (config.defaultTurnBudget || preserve("turnBudget")) lines.push(`turnBudget: ${config.defaultTurnBudget ? JSON.stringify(config.defaultTurnBudget) : ""}`);
98
100
  if (config.defaultAcceptance !== undefined || preserve("acceptance")) {
99
101
  lines.push(`acceptance: ${config.defaultAcceptance === undefined
100
102
  ? ""
@@ -8,7 +8,7 @@ import { parse as parseYaml } from "yaml";
8
8
  import * as os from "node:os";
9
9
  import * as path from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
- import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, OutputMode, ToolBudgetConfig, TurnBudgetConfig } from "../shared/types.ts";
11
+ import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, OutputMode, ToolBudgetConfig } from "../shared/types.ts";
12
12
  import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, parseExternalCliCapabilityNarrowing, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
13
13
  import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
14
14
  import { KNOWN_FIELDS } from "./agent-serializer.ts";
@@ -20,7 +20,6 @@ import { parseModelScopeConfig, type ModelScopeConfig } from "../runs/shared/mod
20
20
  export { BUILTIN_AGENT_NAMES } from "./builtin-names.ts";
21
21
  export { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./identity.ts";
22
22
  import { parseMemoryFrontmatter } from "./agent-memory.ts";
23
- import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts";
24
23
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
25
24
  import { validatePermissionRules, type PermissionRules } from "../runs/shared/permissions.ts";
26
25
  import { parseThinkingLevel, type ThinkingLevel } from "../shared/thinking-ceiling.ts";
@@ -71,6 +70,7 @@ export interface BuiltinAgentOverrideBase {
71
70
  skills?: string[];
72
71
  skillPath?: string[];
73
72
  tools?: string[];
73
+ allowNestedSubagents?: boolean;
74
74
  mcpDirectTools?: string[];
75
75
  extensions?: string[];
76
76
  subagentOnlyExtensions?: string[];
@@ -99,6 +99,7 @@ interface BuiltinAgentOverrideConfig {
99
99
  systemPrompt?: string;
100
100
  skills?: string[] | false;
101
101
  tools?: string[] | false | "inherit";
102
+ allowNestedSubagents?: boolean;
102
103
  extensions?: string[] | false;
103
104
  subagentOnlyExtensions?: string[] | false;
104
105
  mutationTools?: string[] | false;
@@ -131,6 +132,7 @@ export interface AgentConfig {
131
132
  description: string;
132
133
  aliases?: string[];
133
134
  tools?: string[];
135
+ allowNestedSubagents?: boolean;
134
136
  mcpDirectTools?: string[];
135
137
  model?: string;
136
138
  modelProvider?: string;
@@ -145,7 +147,6 @@ export interface AgentConfig {
145
147
  defaultAsync?: boolean;
146
148
  defaultTimeoutMs?: number;
147
149
  defaultToolTimeoutMs?: number;
148
- defaultTurnBudget?: TurnBudgetConfig;
149
150
  defaultAcceptance?: AcceptanceInput;
150
151
  acceptanceRole?: AcceptanceRole;
151
152
  systemPrompt: string;
@@ -179,6 +180,7 @@ type ProjectRootResolution = "nearest" | "git-root";
179
180
 
180
181
  interface SubagentSettings {
181
182
  overrides: Record<string, BuiltinAgentOverrideConfig>;
183
+ providerOverrides: Record<string, Record<string, BuiltinAgentOverrideConfig>>;
182
184
  defaultModel?: string;
183
185
  defaultProvider?: string;
184
186
  defaultThinking?: string;
@@ -189,7 +191,7 @@ interface SubagentSettings {
189
191
  modelScope?: ModelScopeConfig;
190
192
  }
191
193
 
192
- const EMPTY_SUBAGENT_SETTINGS: SubagentSettings = { overrides: {} };
194
+ const EMPTY_SUBAGENT_SETTINGS: SubagentSettings = { overrides: {}, providerOverrides: {} };
193
195
  const agentFrontmatterFields = new WeakMap<AgentConfig, Set<string>>();
194
196
 
195
197
  export interface ChainStepConfig {
@@ -725,6 +727,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
725
727
  ...(agent.skills ? { skills: [...agent.skills] } : {}),
726
728
  ...(agent.skillPath ? { skillPath: [...agent.skillPath] } : {}),
727
729
  ...(agent.tools ? { tools: [...agent.tools] } : {}),
730
+ ...(agent.allowNestedSubagents !== undefined ? { allowNestedSubagents: agent.allowNestedSubagents } : {}),
728
731
  ...(agent.mcpDirectTools ? { mcpDirectTools: [...agent.mcpDirectTools] } : {}),
729
732
  ...(!agent.extensionsFromDefault && agent.extensions ? { extensions: [...agent.extensions] } : {}),
730
733
  ...(agent.subagentOnlyExtensions ? { subagentOnlyExtensions: [...agent.subagentOnlyExtensions] } : {}),
@@ -757,6 +760,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
757
760
  ...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
758
761
  ...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
759
762
  ...(override.tools !== undefined ? { tools: Array.isArray(override.tools) ? [...override.tools] : override.tools } : {}),
763
+ ...(override.allowNestedSubagents !== undefined ? { allowNestedSubagents: override.allowNestedSubagents } : {}),
760
764
  ...(override.extensions !== undefined ? { extensions: override.extensions === false ? false : [...override.extensions] } : {}),
761
765
  ...(override.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: override.subagentOnlyExtensions === false ? false : [...override.subagentOnlyExtensions] } : {}),
762
766
  ...(override.mutationTools !== undefined ? { mutationTools: override.mutationTools === false ? false : [...override.mutationTools] } : {}),
@@ -1048,6 +1052,10 @@ function parseBuiltinOverrideEntry(
1048
1052
 
1049
1053
  const tools = parseToolsOverride(input.tools, { filePath, name });
1050
1054
  if (tools !== undefined) override.tools = tools;
1055
+ if ("allowNestedSubagents" in input) {
1056
+ if (typeof input.allowNestedSubagents === "boolean") override.allowNestedSubagents = input.allowNestedSubagents;
1057
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'allowNestedSubagents'; expected a boolean.`);
1058
+ }
1051
1059
 
1052
1060
  const extensions = parseOverrideStringArrayOrFalse(input.extensions, { filePath, name, field: "extensions" });
1053
1061
  if (extensions !== undefined) override.extensions = extensions;
@@ -1127,9 +1135,12 @@ function readSubagentSettings(filePath: string | null): SubagentSettings {
1127
1135
  const modelScope = parseModelScopeConfig(subagentsObject.modelScope, { filePath });
1128
1136
 
1129
1137
  const parsed: Record<string, BuiltinAgentOverrideConfig> = {};
1138
+ const providerOverrides: Record<string, Record<string, BuiltinAgentOverrideConfig>> = {};
1130
1139
  const agentOverrides = subagentsObject.agentOverrides;
1140
+ const agentOverridesByProvider = subagentsObject.agentOverridesByProvider;
1131
1141
  const parsedSettings: SubagentSettings = {
1132
1142
  overrides: parsed,
1143
+ providerOverrides,
1133
1144
  ...(defaultModel !== undefined ? { defaultModel } : {}),
1134
1145
  ...(defaultProvider !== undefined ? { defaultProvider } : {}),
1135
1146
  ...(defaultThinking !== undefined ? { defaultThinking } : {}),
@@ -1139,16 +1150,42 @@ function readSubagentSettings(filePath: string | null): SubagentSettings {
1139
1150
  ...(disableThinking !== undefined ? { disableThinking } : {}),
1140
1151
  ...(modelScope !== undefined ? { modelScope } : {}),
1141
1152
  };
1142
- if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) {
1143
- return parsedSettings;
1153
+ if (agentOverrides && typeof agentOverrides === "object" && !Array.isArray(agentOverrides)) {
1154
+ for (const [name, value] of Object.entries(agentOverrides)) {
1155
+ const override = parseBuiltinOverrideEntry(name, value, filePath);
1156
+ if (override) parsed[name] = override;
1157
+ }
1144
1158
  }
1145
- for (const [name, value] of Object.entries(agentOverrides)) {
1146
- const override = parseBuiltinOverrideEntry(name, value, filePath);
1147
- if (override) parsed[name] = override;
1159
+ if (agentOverridesByProvider !== undefined) {
1160
+ if (!agentOverridesByProvider || typeof agentOverridesByProvider !== "object" || Array.isArray(agentOverridesByProvider)) {
1161
+ throw new Error(`Subagent settings in '${filePath}' have invalid 'agentOverridesByProvider'; expected an object keyed by provider.`);
1162
+ }
1163
+ for (const [provider, value] of Object.entries(agentOverridesByProvider)) {
1164
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1165
+ throw new Error(`Subagent settings in '${filePath}' have invalid 'agentOverridesByProvider.${provider}'; expected an object keyed by agent.`);
1166
+ }
1167
+ const entries: Record<string, BuiltinAgentOverrideConfig> = {};
1168
+ for (const [agentName, agentValue] of Object.entries(value)) {
1169
+ const override = parseBuiltinOverrideEntry(`agentOverridesByProvider.${provider}.${agentName}`, agentValue, filePath);
1170
+ if (override) entries[agentName] = override;
1171
+ }
1172
+ providerOverrides[provider] = entries;
1173
+ }
1148
1174
  }
1149
1175
  return parsedSettings;
1150
1176
  }
1151
1177
 
1178
+ function selectProviderOverrides(settings: SubagentSettings, provider: string | undefined): SubagentSettings {
1179
+ if (!provider) return settings;
1180
+ const selected = settings.providerOverrides[provider];
1181
+ if (!selected) return settings;
1182
+ const overrides = { ...settings.overrides };
1183
+ for (const [name, override] of Object.entries(selected)) {
1184
+ overrides[name] = { ...overrides[name], ...override };
1185
+ }
1186
+ return { ...settings, overrides };
1187
+ }
1188
+
1152
1189
  function resolveSubagentDefaultProvider(
1153
1190
  userSettings: SubagentSettings,
1154
1191
  projectSettings: SubagentSettings,
@@ -1301,6 +1338,7 @@ function applyBuiltinOverride(
1301
1338
  if (override.systemPrompt !== undefined) next.systemPrompt = override.systemPrompt;
1302
1339
  if (override.skills !== undefined) { if (override.skills === false) delete next.skills; else next.skills = [...override.skills]; }
1303
1340
  if (override.tools !== undefined) applyToolsOverride(next, override.tools);
1341
+ if (override.allowNestedSubagents !== undefined) next.allowNestedSubagents = override.allowNestedSubagents;
1304
1342
  if (override.extensions !== undefined) { if (override.extensions === false) delete next.extensions; else next.extensions = [...override.extensions]; }
1305
1343
  if (override.subagentOnlyExtensions !== undefined) { if (override.subagentOnlyExtensions === false) delete next.subagentOnlyExtensions; else next.subagentOnlyExtensions = [...override.subagentOnlyExtensions]; }
1306
1344
  if (override.mutationTools !== undefined) { if (override.mutationTools === false) delete next.mutationTools; else next.mutationTools = [...override.mutationTools]; }
@@ -1465,6 +1503,9 @@ function applyCustomAgentOverride(
1465
1503
  applyToolsOverride(mutable(), override.tools);
1466
1504
  anyFilled = true;
1467
1505
  }
1506
+ if (override.allowNestedSubagents !== undefined) {
1507
+ fill("allowNestedSubagents", ["allowNestedSubagents"], override.allowNestedSubagents);
1508
+ }
1468
1509
  if (override.extensions !== undefined) {
1469
1510
  fill("extensions", ["extensions"], override.extensions === false ? undefined : [...override.extensions]);
1470
1511
  }
@@ -1517,7 +1558,7 @@ function applyCustomAgentOverrides(
1517
1558
 
1518
1559
  export function buildBuiltinOverrideConfig(
1519
1560
  base: BuiltinAgentOverrideBase,
1520
- draft: Pick<AgentConfig, "model" | "modelProvider" | "fallbackModels" | "fast" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritGlobalContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "mutationTools" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description" | "output" | "outputMode" | "defaultReads">>,
1561
+ draft: Pick<AgentConfig, "model" | "modelProvider" | "fallbackModels" | "fast" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritGlobalContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "allowNestedSubagents" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "mutationTools" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description" | "output" | "outputMode" | "defaultReads">>,
1521
1562
  ): BuiltinAgentOverrideConfig | undefined {
1522
1563
  const override: BuiltinAgentOverrideConfig = {};
1523
1564
 
@@ -1546,6 +1587,7 @@ export function buildBuiltinOverrideConfig(
1546
1587
  const baseTools = joinToolList(base);
1547
1588
  const draftTools = joinToolList(draft);
1548
1589
  if (!arraysEqual(draftTools, baseTools)) override.tools = draftTools ? [...draftTools] : false;
1590
+ if (draft.allowNestedSubagents !== base.allowNestedSubagents) override.allowNestedSubagents = draft.allowNestedSubagents === true;
1549
1591
  if (!arraysEqual(draft.extensions, base.extensions)) override.extensions = draft.extensions ? [...draft.extensions] : false;
1550
1592
  if (!arraysEqual(draft.subagentOnlyExtensions, base.subagentOnlyExtensions)) {
1551
1593
  override.subagentOnlyExtensions = draft.subagentOnlyExtensions ? [...draft.subagentOnlyExtensions] : false;
@@ -1680,7 +1722,7 @@ export function removeBuiltinAgentOverrideFields(
1680
1722
  return { path: filePath, removed: true };
1681
1723
  }
1682
1724
 
1683
- const DISCOVERY_PRUNED_DIR_NAMES = new Set([".git", "node_modules"]);
1725
+ const DISCOVERY_PRUNED_DIR_NAMES = new Set([".git", "node_modules", ".pi", "sync-backups"]);
1684
1726
 
1685
1727
  function isDiscoveryNestedProjectRoot(dir: string): boolean {
1686
1728
  return isDirectory(getProjectConfigDir(dir)) || isDirectory(path.join(dir, ".agents"));
@@ -1894,7 +1936,7 @@ function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string)
1894
1936
 
1895
1937
  function validateExternalRunnerProfile(frontmatter: Record<string, string>, agentName: string, runner: AgentRunnerConfig | undefined): void {
1896
1938
  if (runner?.type !== "external-cli" && runner?.type !== "external-job") return;
1897
- const unsupported = ["tools", "model", "fallbackModels", "thinking", "extensions", "subagentOnlyExtensions", "mutationTools", "maxSubagentDepth", "completionGuard", "skills", "skill", "skillPath", "toolBudget", "permission", "permissions"]
1939
+ const unsupported = ["tools", "allowNestedSubagents", "model", "fallbackModels", "thinking", "extensions", "subagentOnlyExtensions", "mutationTools", "maxSubagentDepth", "completionGuard", "skills", "skill", "skillPath", "toolBudget", "permission", "permissions"]
1898
1940
  .filter((field) => frontmatter[field] !== undefined);
1899
1941
  if (unsupported.length > 0) {
1900
1942
  throw new Error(`Agent '${agentName}' uses runner.type='${runner.type}' and declares unsupported Pi-only fields: ${unsupported.join(", ")}.`);
@@ -2027,13 +2069,6 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
2027
2069
  }
2028
2070
  defaultToolTimeoutMs = parsed;
2029
2071
  }
2030
- let defaultTurnBudget: TurnBudgetConfig | undefined;
2031
- if (frontmatter.turnBudget !== undefined && frontmatter.turnBudget.trim()) {
2032
- const parsed = JSON.parse(frontmatter.turnBudget) as unknown;
2033
- const resolved = resolveTurnBudgetConfig(parsed, `Agent '${localName}' turnBudget frontmatter`);
2034
- if (resolved.error) throw new Error(resolved.error);
2035
- defaultTurnBudget = resolved.turnBudget;
2036
- }
2037
2072
  const defaultAcceptance = parseAgentAcceptanceFrontmatter(frontmatter.acceptance, localName);
2038
2073
  let outputMode: OutputMode | undefined;
2039
2074
  if (frontmatter.outputMode !== undefined) {
@@ -2055,6 +2090,12 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
2055
2090
  else if (frontmatter.fast === "false") fast = false;
2056
2091
  else throw new Error(`Agent '${localName}' has invalid fast frontmatter; expected true or false.`);
2057
2092
  }
2093
+ let allowNestedSubagents: boolean | undefined;
2094
+ if (frontmatter.allowNestedSubagents !== undefined) {
2095
+ if (frontmatter.allowNestedSubagents === "true") allowNestedSubagents = true;
2096
+ else if (frontmatter.allowNestedSubagents === "false") allowNestedSubagents = false;
2097
+ else throw new Error(`Agent '${localName}' has invalid allowNestedSubagents frontmatter; expected true or false.`);
2098
+ }
2058
2099
 
2059
2100
  const extraFields: Record<string, string> = {};
2060
2101
  for (const [key, value] of Object.entries(frontmatter)) {
@@ -2098,6 +2139,7 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
2098
2139
  description: frontmatter.description,
2099
2140
  ...(aliases !== undefined ? { aliases } : {}),
2100
2141
  ...(rawTools !== undefined ? { tools } : {}),
2142
+ ...(allowNestedSubagents !== undefined ? { allowNestedSubagents } : {}),
2101
2143
  ...(mcpDirectTools.length > 0 ? { mcpDirectTools } : {}),
2102
2144
  ...(frontmatter.model !== undefined ? { model: frontmatter.model } : {}),
2103
2145
  ...(fallbackModels?.length ? { fallbackModels } : {}),
@@ -2111,7 +2153,6 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
2111
2153
  ...(defaultAsync !== undefined ? { defaultAsync } : {}),
2112
2154
  ...(defaultTimeoutMs !== undefined ? { defaultTimeoutMs } : {}),
2113
2155
  ...(defaultToolTimeoutMs !== undefined ? { defaultToolTimeoutMs } : {}),
2114
- ...(defaultTurnBudget !== undefined ? { defaultTurnBudget } : {}),
2115
2156
  ...(defaultAcceptance !== undefined ? { defaultAcceptance } : {}),
2116
2157
  ...(acceptanceRole !== undefined ? { acceptanceRole } : {}),
2117
2158
  systemPrompt: body,
@@ -2237,15 +2278,15 @@ function extraUserAgentDirs(): string[] {
2237
2278
  .filter((dir) => dir.length > 0);
2238
2279
  }
2239
2280
 
2240
- export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
2281
+ export function discoverAgents(cwd: string, scope: AgentScope, preferredModelProvider?: string): AgentDiscoveryResult {
2241
2282
  const effectiveCwd = path.resolve(cwd);
2242
2283
  const userDirOld = path.join(getAgentDir(), "agents");
2243
2284
  const userDirNew = path.join(os.homedir(), ".agents");
2244
2285
  const { readDirs: projectAgentDirs, candidateDirs: projectCandidateDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(effectiveCwd);
2245
2286
  const userSettingsPath = getUserAgentSettingsPath();
2246
2287
  const projectSettingsPath = getProjectAgentSettingsPath(effectiveCwd);
2247
- const userSettings = scope === "project" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(userSettingsPath);
2248
- const projectSettings = scope === "user" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(projectSettingsPath);
2288
+ const userSettings = selectProviderOverrides(scope === "project" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(userSettingsPath), preferredModelProvider);
2289
+ const projectSettings = selectProviderOverrides(scope === "user" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(projectSettingsPath), preferredModelProvider);
2249
2290
  const defaultProvider = resolveSubagentDefaultProvider(userSettings, projectSettings, projectSettingsPath);
2250
2291
  const defaultModel = resolveSubagentDefaultModel(userSettings, projectSettings, userSettingsPath, projectSettingsPath, defaultProvider);
2251
2292
  const defaultThinking = resolveSubagentDefaultThinking(userSettings, projectSettings, projectSettingsPath);
@@ -2308,7 +2349,7 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
2308
2349
  return { agents, agentDiagnostics, projectAgentsDir, cwd: effectiveCwd, scope, directories, ...(modelScope !== undefined ? { modelScope } : {}), ...(maxThinking !== undefined ? { maxThinking } : {}) };
2309
2350
  }
2310
2351
 
2311
- export function discoverAgentsAll(cwd: string): {
2352
+ export function discoverAgentsAll(cwd: string, preferredModelProvider?: string): {
2312
2353
  builtin: AgentConfig[];
2313
2354
  package: AgentConfig[];
2314
2355
  user: AgentConfig[];
@@ -2331,8 +2372,8 @@ export function discoverAgentsAll(cwd: string): {
2331
2372
  const { readDirs: projectChainDirs, preferredDir: projectChainDir } = resolveNearestProjectChainDirs(cwd);
2332
2373
  const userSettingsPath = getUserAgentSettingsPath();
2333
2374
  const projectSettingsPath = getProjectAgentSettingsPath(cwd);
2334
- const userSettings = readSubagentSettings(userSettingsPath);
2335
- const projectSettings = readSubagentSettings(projectSettingsPath);
2375
+ const userSettings = selectProviderOverrides(readSubagentSettings(userSettingsPath), preferredModelProvider);
2376
+ const projectSettings = selectProviderOverrides(readSubagentSettings(projectSettingsPath), preferredModelProvider);
2336
2377
  const defaultProvider = resolveSubagentDefaultProvider(userSettings, projectSettings, projectSettingsPath);
2337
2378
  const defaultModel = resolveSubagentDefaultModel(userSettings, projectSettings, userSettingsPath, projectSettingsPath, defaultProvider);
2338
2379
  const defaultThinking = resolveSubagentDefaultThinking(userSettings, projectSettings, projectSettingsPath);
@@ -1,10 +1,9 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, OutputMode, ToolBudgetConfig, TurnBudgetConfig } from "../shared/types.ts";
2
+ import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, OutputMode, ToolBudgetConfig } from "../shared/types.ts";
3
3
  import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, parseExternalCliCapabilityNarrowing, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
4
4
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
5
5
  import { validatePermissionRules, type PermissionRules } from "../runs/shared/permissions.ts";
6
6
  import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
7
- import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts";
8
7
  import { BUILTIN_AGENT_NAMES } from "./builtin-names.ts";
9
8
  import type { AgentConfig, AgentDefaultContext, AgentDiscoveryDiagnostic } from "./agents.ts";
10
9
 
@@ -21,6 +20,7 @@ export interface RuntimeAgentDefinition {
21
20
  systemPrompt: string;
22
21
  aliases?: readonly string[];
23
22
  tools?: readonly string[];
23
+ allowNestedSubagents?: boolean;
24
24
  mcpDirectTools?: readonly string[];
25
25
  model?: string;
26
26
  fallbackModels?: readonly string[];
@@ -33,7 +33,6 @@ export interface RuntimeAgentDefinition {
33
33
  defaultAsync?: boolean;
34
34
  defaultTimeoutMs?: number;
35
35
  defaultToolTimeoutMs?: number;
36
- defaultTurnBudget?: TurnBudgetConfig;
37
36
  defaultAcceptance?: AcceptanceInput;
38
37
  acceptanceRole?: AcceptanceRole;
39
38
  runner?: AgentRunnerConfig;
@@ -135,8 +134,8 @@ function validateStringList(value: unknown, field: string): string[] | undefined
135
134
 
136
135
  function validatePositiveInteger(value: unknown, field: string): number | undefined {
137
136
  if (value === undefined) return undefined;
138
- if (!Number.isInteger(value) || (value as number) <= 0) throw new Error(`${field} must be a positive integer when provided.`);
139
- return value as number;
137
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) throw new Error(`${field} must be a positive integer when provided.`);
138
+ return value;
140
139
  }
141
140
 
142
141
  function validateBoolean(value: unknown, field: string): boolean | undefined {
@@ -183,12 +182,6 @@ function validateRunner(value: unknown): AgentRunnerConfig | undefined {
183
182
  return { type: "external-cli", ...(isCodeOwnedExternalCliAdapterId(runner.adapter) ? { adapter: runner.adapter } : {}), command: runner.command.trim(), ...(args?.length ? { args } : {}), ...(runner.promptDelivery ? { promptDelivery: "stdin" as const } : {}), ...(capabilities ? { capabilities } : {}) };
184
183
  }
185
184
 
186
- function validateTurnBudget(value: unknown): TurnBudgetConfig | undefined {
187
- const result = resolveTurnBudgetConfig(value, "Runtime agent definition defaultTurnBudget");
188
- if (result.error) throw new Error(result.error);
189
- return result.turnBudget;
190
- }
191
-
192
185
  function validateAcceptance(value: unknown): AcceptanceInput | undefined {
193
186
  const errors = validateAcceptanceInput(value, "Runtime agent definition defaultAcceptance");
194
187
  if (errors.length > 0) throw new Error(errors.join(" "));
@@ -205,9 +198,9 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
205
198
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Runtime agent definition must be an object.");
206
199
  const definition = value as Record<string, unknown>;
207
200
  const supported = new Set([
208
- "description", "systemPrompt", "aliases", "tools", "mcpDirectTools", "model", "fallbackModels", "thinking",
201
+ "description", "systemPrompt", "aliases", "tools", "allowNestedSubagents", "mcpDirectTools", "model", "fallbackModels", "thinking",
209
202
  "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "defaultContext", "defaultAsync", "defaultTimeoutMs",
210
- "defaultToolTimeoutMs", "defaultTurnBudget", "defaultAcceptance", "acceptanceRole", "runner", "skills", "skillPath",
203
+ "defaultToolTimeoutMs", "defaultAcceptance", "acceptanceRole", "runner", "skills", "skillPath",
211
204
  "extensions", "subagentOnlyExtensions", "mutationTools", "output", "outputMode", "defaultReads", "defaultProgress", "interactive",
212
205
  "maxSubagentDepth", "completionGuard", "toolBudget", "permissions",
213
206
  ]);
@@ -225,6 +218,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
225
218
  if (outputMode !== undefined && outputMode !== "inline" && outputMode !== "file-only") throw new Error("Runtime agent definition outputMode must be 'inline' or 'file-only'.");
226
219
  const aliases = validateStringList(definition.aliases, "Runtime agent definition aliases");
227
220
  const tools = validateStringList(definition.tools, "Runtime agent definition tools");
221
+ const allowNestedSubagents = validateBoolean(definition.allowNestedSubagents, "Runtime agent definition allowNestedSubagents");
228
222
  const mcpDirectTools = validateStringList(definition.mcpDirectTools, "Runtime agent definition mcpDirectTools");
229
223
  const model = validateOptionalString(definition.model, "Runtime agent definition model");
230
224
  const fallbackModels = validateStringList(definition.fallbackModels, "Runtime agent definition fallbackModels");
@@ -234,7 +228,6 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
234
228
  const defaultAsync = validateBoolean(definition.defaultAsync, "Runtime agent definition defaultAsync");
235
229
  const defaultTimeoutMs = validatePositiveInteger(definition.defaultTimeoutMs, "Runtime agent definition defaultTimeoutMs");
236
230
  const defaultToolTimeoutMs = validatePositiveInteger(definition.defaultToolTimeoutMs, "Runtime agent definition defaultToolTimeoutMs");
237
- const defaultTurnBudget = validateTurnBudget(definition.defaultTurnBudget);
238
231
  const defaultAcceptance = validateAcceptance(definition.defaultAcceptance);
239
232
  const runner = validateRunner(definition.runner);
240
233
  const skills = validateStringList(definition.skills, "Runtime agent definition skills");
@@ -255,6 +248,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
255
248
  systemPrompt: validateString(definition.systemPrompt, "Runtime agent definition systemPrompt", MAX_SYSTEM_PROMPT_LENGTH),
256
249
  ...(aliases ? { aliases } : {}),
257
250
  ...(tools ? { tools } : {}),
251
+ ...(allowNestedSubagents !== undefined ? { allowNestedSubagents } : {}),
258
252
  ...(mcpDirectTools ? { mcpDirectTools } : {}),
259
253
  ...(model ? { model } : {}),
260
254
  ...(fallbackModels ? { fallbackModels } : {}),
@@ -267,7 +261,6 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
267
261
  ...(defaultAsync !== undefined ? { defaultAsync } : {}),
268
262
  ...(defaultTimeoutMs !== undefined ? { defaultTimeoutMs } : {}),
269
263
  ...(defaultToolTimeoutMs !== undefined ? { defaultToolTimeoutMs } : {}),
270
- ...(defaultTurnBudget !== undefined ? { defaultTurnBudget } : {}),
271
264
  ...(defaultAcceptance !== undefined ? { defaultAcceptance } : {}),
272
265
  ...(acceptanceRole !== undefined ? { acceptanceRole: acceptanceRole as AcceptanceRole } : {}),
273
266
  ...(runner !== undefined ? { runner } : {}),
@@ -334,6 +327,7 @@ function toAgentConfig(name: string, definition: RuntimeAgentDefinition): AgentC
334
327
  ...(aliases ? { aliases } : {}),
335
328
  ...(definition.runner !== undefined ? { runner: definition.runner } : {}),
336
329
  ...(definition.tools !== undefined ? { tools: [...definition.tools] } : {}),
330
+ ...(definition.allowNestedSubagents !== undefined ? { allowNestedSubagents: definition.allowNestedSubagents } : {}),
337
331
  ...(definition.mcpDirectTools !== undefined ? { mcpDirectTools: [...definition.mcpDirectTools] } : {}),
338
332
  ...(definition.model !== undefined ? { model: definition.model } : {}),
339
333
  ...(definition.fallbackModels !== undefined ? { fallbackModels: [...definition.fallbackModels] } : {}),
@@ -346,7 +340,6 @@ function toAgentConfig(name: string, definition: RuntimeAgentDefinition): AgentC
346
340
  ...(definition.defaultAsync !== undefined ? { defaultAsync: definition.defaultAsync } : {}),
347
341
  ...(definition.defaultTimeoutMs !== undefined ? { defaultTimeoutMs: definition.defaultTimeoutMs } : {}),
348
342
  ...(definition.defaultToolTimeoutMs !== undefined ? { defaultToolTimeoutMs: definition.defaultToolTimeoutMs } : {}),
349
- ...(definition.defaultTurnBudget !== undefined ? { defaultTurnBudget: definition.defaultTurnBudget } : {}),
350
343
  ...(definition.defaultAcceptance !== undefined ? { defaultAcceptance: definition.defaultAcceptance } : {}),
351
344
  ...(definition.acceptanceRole !== undefined ? { acceptanceRole: definition.acceptanceRole } : {}),
352
345
  systemPrompt: definition.systemPrompt,