@signalridge/pi-subagents 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,93 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.0
4
+ ### Minor Changes
5
+
6
+ - 72be09a: An agent no longer picks its own model.
7
+
8
+ `model:` and `thinking:` in agent frontmatter are no longer read. Which model a
9
+ subagent runs is the tier catalogue's decision, and a per-file pin was a way
10
+ around it — silently, since nothing warned unless the file also named a tier. A
11
+ file that still carries them loads and runs exactly as before; the two lines
12
+ have no effect, and a warning names the file so the migration can be finished
13
+ one agent at a time.
14
+
15
+ The built-in agents drop their own pin for the same reason: `Explore` named
16
+ `anthropic/claude-haiku-4-5`, which is both an end-run around the catalogue and
17
+ a vendor the machine may not have.
18
+
19
+ The resulting fallback is simple and worth stating: with no tier passed, none in
20
+ the agent, and no `agentTiers.defaultTier`, a subagent runs on the parent
21
+ session's model.
22
+
23
+ A `defaultTier` — or an agent's `tier:` — that names no defined profile is now
24
+ reported at startup with the available keys, rather than waiting for the first
25
+ spawn that needs it. `/agents` no longer advertises a pinned model in the type
26
+ list and writes `tier:` instead of `model:` when it regenerates an agent file.
27
+
28
+ Programmatic callers and the legacy RPC still accept `model`/`thinking`. That is
29
+ an escape hatch for code, not a way to configure an agent.
30
+
31
+ ## 1.3.0
32
+ ### Minor Changes
33
+
34
+ - 1860def: Name your model tiers, and stop the orchestrator choosing models by hand.
35
+
36
+ A tier is one name for a (model, thinking) pair, defined in `subagents.json`:
37
+
38
+ ```json
39
+ {
40
+ "agentTiers": {
41
+ "defaultTier": "medium",
42
+ "profiles": {
43
+ "small": { "description": "Fast, cheap exploration", "model": "deepseek/deepseek-v4-flash", "thinking": "max" },
44
+ "research": { "description": "Long-context research", "model": "kimi/k3", "thinking": "max" }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ Keys are arbitrary — `small`/`medium`/`large` are an example, not a vocabulary.
51
+ A profile requires both `model` and `thinking` (either may be `"inherit"`);
52
+ `description` is optional and is what the host reads when choosing.
53
+
54
+ Resolution order: the `tier` on the call, then `tier:` in the agent's
55
+ frontmatter, then `agentTiers.defaultTier`, then the agent's legacy
56
+ `model:`/`thinking:`, then the parent session. A tier that applies decides both
57
+ fields outright, and an agent carrying both a tier and legacy pins logs a warning
58
+ naming the file. Unknown keys, malformed profiles and unavailable models refuse
59
+ the spawn before it starts, naming the tier and where it came from — none of them
60
+ quietly substitutes another model.
61
+
62
+ The catalogue is rendered into the `Agent` tool description at registration, so
63
+ the host knows the vocabulary before its first call rather than having to
64
+ remember a lookup tool. Custom descriptions gain `{{tierList}}`,
65
+ `{{compactTierList}}` and `{{defaultTier}}`.
66
+
67
+ Resolution happens once, in `agent-runner.ts`, so the top-level `Agent` tool,
68
+ nested delegation, the scheduler and cross-extension RPC share one precedence and
69
+ one set of refusals.
70
+
71
+ `pi-workflows` is untouched: its tiers stay the fixed `small | medium | large` of
72
+ the cross-package protocol, which is what keeps a workflow definition validatable
73
+ at parse time and portable between machines. The two systems share no fields.
74
+
75
+ The agent conversation overlay now marks its own edges with a rule at the top
76
+ and the bottom. It floats over the transcript and previously drew no border at
77
+ all — two variables named `hrTop` and `hrMid` were a bold title row and a blank
78
+ line — so there was no telling where the agent's conversation ended and the
79
+ parent's resumed. Two rules, none in between: an inner rule competes with the
80
+ pair that marks the boundary, and a four-sided box would cost two columns on
81
+ every row for the same job.
82
+
83
+ The LLM-facing `Agent` and nested-`Agent` schemas no longer accept `model` or
84
+ `thinking` — a caller picks a tier instead. This is a minor, not a major: a tool
85
+ schema is read by a model at runtime rather than compiled against, so no import,
86
+ settings key or RPC payload changes shape. Programmatic callers and the legacy
87
+ RPC still accept both fields, and agents with no tier configured behave exactly
88
+ as before. The migration is: define the profiles once, then replace each agent's
89
+ `model:`/`thinking:` with `tier: <name>`.
90
+
3
91
  ## 1.0.0
4
92
  ### Major Changes
5
93
 
package/README.md CHANGED
@@ -230,8 +230,9 @@ All fields are optional — sensible defaults for everything.
230
230
  | `memory` | — | Persistent agent memory scope: `project`, `local`, or `user`. Auto-detects read-only agents |
231
231
  | `disallowed_tools` | — | Comma-separated tools to deny even if extensions provide them |
232
232
  | `isolation` | — | Set to `worktree` to run in an isolated git worktree |
233
- | `model` | inherit parent | Model `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`). Resolved tolerantly (`.`/`-` and a trailing date stamp are interchangeable) and falls back to the same model under another provider if the named one doesn't have it |
234
- | `thinking` | inherit | off, minimal, low, medium, high, xhigh, max actual availability depends on your pi version and model; pi clamps unsupported levels down |
233
+ | `tier` | none | This agent's default model tier, by name, from `agentTiers.profiles`. A tier passed at the call site overrides it. When set, it wins over `model`/`thinking` below see [Model tiers](#model-tiers) |
234
+ | ~~`model`~~ | | **Removed.** An agent no longer chooses its own model; use `tier`. A file that still has it loads normally, with a warning naming it — the line simply has no effect |
235
+ | ~~`thinking`~~ | — | **Removed**, same as `model` above |
235
236
  | `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited |
236
237
  | `persist_session` | `false` | Persist this subagent as a normal pi session instead of keeping the session in memory only. The subagent's `.output` transcript is still written either way unless `output_transcript: false` |
237
238
  | `output_transcript` | `true` (or `subagents.json` `outputTranscript`) | Write this subagent's `.output` transcript; when set, overrides the `subagents.json` `outputTranscript` default. Set `false` to write no transcript file or path. Governs only the transcript — independent of `persist_session`, `isolation: worktree`, and `memory:` |
@@ -317,8 +318,7 @@ Launch a sub-agent.
317
318
  | `prompt` | string | yes | The task for the agent |
318
319
  | `description` | string | yes | Short 3-5 word summary (shown in UI) |
319
320
  | `subagent_type` | string | yes | Agent type (built-in or custom) |
320
- | `model` | string | no | Model `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`). Resolved tolerantly (`.`/`-` and a trailing date stamp interchangeable) with provider fallback |
321
- | `thinking` | string | no | Thinking level: off, minimal, low, medium, high, xhigh, max (availability depends on pi version and model) |
321
+ | `tier` | string | no | Model tier for this spawn, by name. Overrides the agent's own default tier. Unknown tiers are rejected, not substituted see [Model tiers](#model-tiers) |
322
322
  | `max_turns` | number | no | Max agentic turns. Omit for unlimited (default) |
323
323
  | `run_in_background` | boolean | no | Run without blocking |
324
324
  | `resume` | string | no | Agent ID to resume a previous session |
@@ -409,6 +409,113 @@ When background agents complete, they notify the main agent. The **join mode** c
409
409
  **Configuration:**
410
410
  - Configure join mode in `/agents` → Settings → Join mode
411
411
 
412
+ ## Model tiers
413
+
414
+ A **tier** is one name for a (model, thinking) pair. The host agent picks a tier
415
+ by name and nothing else: the `Agent` tool exposes `tier` and does **not** expose
416
+ `model` or `thinking`, so which model runs is decided by whoever writes
417
+ `subagents.json`, not by the orchestrator improvising per call.
418
+
419
+ Names are yours. `small`/`medium`/`large` below are only an example — `research`,
420
+ `cheap`, `nightly` are equally valid keys.
421
+
422
+ ```json
423
+ {
424
+ "agentTiers": {
425
+ "defaultTier": "medium",
426
+ "profiles": {
427
+ "small": { "description": "Fast, cheap exploration", "model": "deepseek/deepseek-v4-flash", "thinking": "max" },
428
+ "medium": { "description": "Ordinary planning and review", "model": "openai-codex/gpt-5.6-luna", "thinking": "max" },
429
+ "large": { "description": "Architecture and risky review", "model": "openai-codex/gpt-5.6-sol", "thinking": "xhigh" },
430
+ "research": { "description": "Long-context research", "model": "kimi/k3", "thinking": "max" }
431
+ }
432
+ }
433
+ }
434
+ ```
435
+
436
+ A profile is all-or-nothing: both `model` and `thinking` are required, and either
437
+ may be the literal `"inherit"` to keep the parent's. `description` is optional and
438
+ defaults to the key; it is what the host reads when choosing between tiers.
439
+
440
+ ### How the host discovers tiers
441
+
442
+ The catalogue is rendered into the `Agent` tool description at registration, so
443
+ the host knows the vocabulary before its first call — there is no lookup tool to
444
+ remember. It sees:
445
+
446
+ ```
447
+ Available agent tiers:
448
+
449
+ - small: Fast, cheap exploration
450
+ model: deepseek/deepseek-v4-flash
451
+ thinking: max
452
+ ...
453
+ Default tier: medium
454
+
455
+ The caller may pass only a tier key. Do not pass model or thinking directly.
456
+ ```
457
+
458
+ A [custom tool description](#persistent-settings) can place it with
459
+ `{{tierList}}`, `{{compactTierList}}` or `{{defaultTier}}`. Tier changes apply on
460
+ the next pi session, since the description is built once at registration.
461
+
462
+ ### Precedence
463
+
464
+ 1. `tier` passed to the `Agent` call
465
+ 2. `tier:` in the agent's frontmatter
466
+ 3. `agentTiers.defaultTier`
467
+ 4. the parent session's model and thinking
468
+
469
+ There is no fifth step: an agent cannot pin its own model. `model:`/`thinking:`
470
+ in frontmatter are read only to warn that they are stale, and the built-in
471
+ agents pin nothing either. With no tier anywhere — none passed, none in the
472
+ agent, no `defaultTier` — a subagent runs on the parent session's model, which
473
+ is what a workspace that has configured no tiers gets.
474
+
475
+ ### Refusals
476
+
477
+ A `defaultTier`, or an agent's `tier:`, that names no defined profile is
478
+ reported at **startup**, listing the available keys — a typo there would
479
+ otherwise sit quiet until the first spawn that needed it, possibly minutes into
480
+ a session.
481
+
482
+ These fail **before** the spawn, with the tier key and where it came from named.
483
+ None of them silently substitutes another model:
484
+
485
+ - a tier key nobody defined (from the call, the agent file, or `defaultTier`)
486
+ - a profile dropped as malformed during settings load
487
+ - a profile whose model is not available on this machine
488
+ - a syntactically invalid key (blank, whitespace, over 64 characters)
489
+
490
+ ### Merging global and project settings
491
+
492
+ `~/.pi/agent/subagents.json` supplies the catalogue; `<cwd>/.pi/subagents.json`
493
+ edits it. A project profile replaces its global namesake **whole** — never field
494
+ by field, which would let a project change a model while inheriting a thinking
495
+ level nobody chose for that pair. A project profile that fails validation blocks
496
+ its global namesake rather than reviving it, and `defaultTier` is a simple
497
+ project-over-global override.
498
+
499
+ ### Not the same as `workflow.tiers`
500
+
501
+ `pi-workflows` has its own tiers, and they stay fixed at `small | medium | large`.
502
+ That vocabulary is part of the cross-package protocol: it lets a workflow
503
+ definition be validated at parse time and stay portable between machines, neither
504
+ of which survives arbitrary names. The two systems share no fields — a spawn
505
+ records `agentTier`/`agentTierSnapshot` or `tier`/`tierSnapshot`, never one
506
+ standing in for the other.
507
+
508
+ ### Migrating from `model:`/`thinking:`
509
+
510
+ Define the profiles once, then replace each agent's `model:`/`thinking:` with
511
+ `tier: <name>`. Files that still carry the old fields load and run — the fields
512
+ are ignored, with a warning naming the file — so the migration can be done one
513
+ agent at a time. Until an agent names a tier it uses `defaultTier`, or the
514
+ parent's model when none is set.
515
+
516
+ Programmatic callers and the legacy RPC may still pass `model`/`thinking`
517
+ directly. That is the escape hatch for code, not a way to configure an agent.
518
+
412
519
  ## Model Scope
413
520
 
414
521
  **Opt-in:** off by default. Enable via `/agents → Settings → Scope models`.
@@ -3,7 +3,7 @@ Launch a new agent to handle complex, multi-step tasks autonomously. Each agent
3
3
  Available agent types and the tools they have access to:
4
4
  {{typeList}}
5
5
 
6
- Custom agents can be defined in .pi/agents/<name>.md (project) or {{agentDir}}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.
6
+ Custom agents can be defined in .pi/agents/<name>.md (project) or {{agentDir}}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.{{tierList}}
7
7
 
8
8
  When using the Agent tool, specify a subagent_type parameter to select which agent type to use.
9
9
 
@@ -23,8 +23,7 @@ If the target is already known, use a direct tool — `read` for a known path, `
23
23
  - Use steer_subagent to send mid-run messages to a running background agent.
24
24
  - Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent.
25
25
  - If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
26
- - Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
27
- - Use thinking to control extended thinking level.
26
+ - Use tier to pick the model profile for this spawn, by name. A tier overrides the agent's own default tier. Model and thinking are not callable parameters — they are what a tier resolves to.
28
27
  - Use inherit_context if the agent needs the parent conversation history.
29
28
  - Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications). The worktree is automatically cleaned up if the agent makes no changes; otherwise the path and branch are returned in the result.{{scheduleGuideline}}
30
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalridge/pi-subagents",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Signalridge's managed subagent runtime with workflow-owned orchestration RPC.",
5
5
  "author": "tintinweb and signalridge contributors",
6
6
  "license": "MIT",
@@ -691,8 +691,9 @@ export function serializeAgentFile(cfg: AgentConfig): string {
691
691
  fmFields.push(`description: ${JSON.stringify(cfg.description)}`);
692
692
  if (cfg.displayName) fmFields.push(`display_name: ${JSON.stringify(cfg.displayName)}`);
693
693
  fmFields.push(`tools: ${formatYamlScalar(formatToolsField(cfg))}`);
694
- if (cfg.model) fmFields.push(`model: ${JSON.stringify(cfg.model)}`);
695
- if (cfg.thinking) fmFields.push(`thinking: ${formatYamlScalar(cfg.thinking)}`);
694
+ // Never model:/thinking: — the loader ignores them, so writing them back
695
+ // would recreate a pin that looks effective and is not.
696
+ if (cfg.agentTier) fmFields.push(`tier: ${formatYamlScalar(cfg.agentTier)}`);
696
697
  if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`);
697
698
  if (cfg.persistSession) fmFields.push("persist_session: true");
698
699
  if (cfg.sessionDir) fmFields.push(`session_dir: ${JSON.stringify(cfg.sessionDir)}`);
@@ -15,6 +15,7 @@ import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-wor
15
15
  import type { ManagedSpawnRequest as ProtocolManagedSpawnRequest, WorkflowTier } from "@signalridge/pi-subagents-protocol";
16
16
  import { isWorkflowTier, parseManagedSpawnRequest } from "@signalridge/pi-subagents-protocol";
17
17
  import { resumeAgent, runAgent, type ToolActivity } from "./agent-runner.js";
18
+ import type { AgentTierResolutionSnapshot } from "./agent-tiers.js";
18
19
  import {
19
20
  INTERNAL_AGENT_CONFIG_OVERRIDE,
20
21
  type InternalAgentConfigOverride,
@@ -523,6 +524,8 @@ export interface SpawnOptions {
523
524
  thinkingLevel?: ThinkingLevel;
524
525
  /** Semantic workflow tier resolved by pi-subagents at session start. */
525
526
  tier?: WorkflowTier;
527
+ /** User-named model tier; resolved by pi-subagents at session start. */
528
+ agentTier?: string;
526
529
  isBackground?: boolean;
527
530
  /**
528
531
  * Skip the maxConcurrent queue check for this spawn — start immediately even
@@ -553,6 +556,8 @@ export interface SpawnOptions {
553
556
  onSessionCreated?: (session: AgentSession) => void;
554
557
  /** Called after pi-subagents resolves a semantic workflow tier. */
555
558
  onTierResolved?: (snapshot: WorkflowTierResolutionSnapshot) => void;
559
+ /** Called after pi-subagents resolves a user-named agent tier. */
560
+ onAgentTierResolved?: (snapshot: AgentTierResolutionSnapshot) => void;
556
561
  /** Called synchronously after a new record is allocated, before session creation. */
557
562
  onSpawned?: (id: string) => void;
558
563
  /** Called at the end of each agentic turn with the cumulative count. */
@@ -1270,6 +1275,7 @@ export class AgentManager {
1270
1275
  ...(internalOverride ? { [INTERNAL_AGENT_CONFIG_OVERRIDE]: internalOverride } : {}),
1271
1276
  thinkingLevel: options.thinkingLevel,
1272
1277
  tier: options.tier,
1278
+ agentTier: options.agentTier,
1273
1279
  // Worktree wins for the working dir (the agent must run in the copy —
1274
1280
  // which, with a custom cwd, was created from that target). Config stays
1275
1281
  // with the parent project when a caller-supplied cwd is in play; it must
@@ -1323,6 +1329,21 @@ export class AgentManager {
1323
1329
  options.onTierResolved?.(snapshot);
1324
1330
  }
1325
1331
  },
1332
+ // Recorded on the same record as the workflow snapshot but under its own
1333
+ // field, so a run can carry both without either overwriting the other's
1334
+ // account of how its model was chosen.
1335
+ onAgentTierResolved: (snapshot) => {
1336
+ if (!record.detached) {
1337
+ record.invocation = {
1338
+ ...(record.invocation ?? {}),
1339
+ agentTier: snapshot.tier,
1340
+ thinking: snapshot.thinking,
1341
+ agentTierSnapshot: { ...snapshot },
1342
+ };
1343
+ this.syncManagedRecord(record, true);
1344
+ options.onAgentTierResolved?.(snapshot);
1345
+ }
1346
+ },
1326
1347
  onSessionCreated: (session) => {
1327
1348
  if (record.detached) {
1328
1349
  this.trackRecordSessionTeardown(id, session);
@@ -19,6 +19,7 @@ import {
19
19
  SettingsManager,
20
20
  } from "@earendil-works/pi-coding-agent";
21
21
  import type { WorkflowTier } from "@signalridge/pi-subagents-protocol";
22
+ import { type AgentTierResolutionSnapshot, resolveAgentTier } from "./agent-tiers.js";
22
23
  import { BUILTIN_TOOL_NAMES, getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
23
24
  import { runInChildSessionContext } from "./child-context.js";
24
25
  import { buildParentContext, extractText } from "./context.js";
@@ -380,6 +381,12 @@ export interface RunOptions {
380
381
  thinkingLevel?: ThinkingLevel;
381
382
  /** Semantic workflow tier; resolved here rather than by workflow callers. */
382
383
  tier?: WorkflowTier;
384
+ /**
385
+ * User-named model tier for an ordinary spawn. Resolved here so the top-level
386
+ * Agent tool, nested delegation, the scheduler and cross-extension RPC all get
387
+ * the same precedence and the same fail-closed errors from one place.
388
+ */
389
+ agentTier?: string;
383
390
  /** Parent thinking level used only when a tier profile omits thinking. */
384
391
  parentThinking?: ThinkingLevel;
385
392
  /** Override working directory (e.g. for worktree isolation). */
@@ -407,6 +414,8 @@ export interface RunOptions {
407
414
  onSessionCreated?: (session: AgentSession) => void;
408
415
  /** Called after pi-subagents resolves a semantic workflow tier. */
409
416
  onTierResolved?: (snapshot: WorkflowTierResolutionSnapshot) => void;
417
+ /** Called after pi-subagents resolves a user-named agent tier. */
418
+ onAgentTierResolved?: (snapshot: AgentTierResolutionSnapshot) => void;
410
419
  /** Called at the end of each agentic turn with the cumulative count. */
411
420
  onTurnEnd?: (turnCount: number) => void;
412
421
  /**
@@ -622,6 +631,21 @@ export async function runAgent(
622
631
  : undefined;
623
632
  if (tierResolution?.snapshot) options.onTierResolved?.(tierResolution.snapshot);
624
633
 
634
+ // Agent tiers resolve in the same place and before the same await, so every
635
+ // spawn path shares one precedence and one set of fail-closed errors. A tier
636
+ // that applies decides model and thinking outright: it is current policy,
637
+ // while an agent's legacy `model:`/`thinking:` frontmatter is the older, weaker
638
+ // statement of the same thing. Throwing here refuses the spawn rather than
639
+ // quietly running a model the caller did not choose.
640
+ const agentTierResolution = resolveAgentTier({
641
+ requestedTier: options.agentTier,
642
+ agentConfig,
643
+ parentModel: ctx.model,
644
+ parentThinking,
645
+ modelRegistry: ctx.modelRegistry,
646
+ });
647
+ if (agentTierResolution.snapshot) options.onAgentTierResolved?.(agentTierResolution.snapshot);
648
+
625
649
  // Resolve working directory: worktree override > parent cwd
626
650
  const effectiveCwd = options.cwd ?? ctx.cwd;
627
651
  // Filesystem work happens in effectiveCwd; config discovery in configCwd.
@@ -842,12 +866,33 @@ export async function runAgent(
842
866
  }
843
867
  }
844
868
 
845
- const model = options.model ?? tierResolution?.model ?? resolveDefaultModel(
869
+ // `options.model` stays highest: it is a resolved Model handed over by a
870
+ // programmatic caller, which is a more explicit act than naming a tier.
871
+ const model = options.model ?? agentTierResolution.model ?? tierResolution?.model ?? resolveDefaultModel(
846
872
  ctx.model, ctx.modelRegistry, agentConfig?.model,
847
873
  );
848
- const thinkingLevel = options.tier !== undefined
849
- ? tierResolution?.thinkingLevel
850
- : options.thinkingLevel ?? agentConfig?.thinking;
874
+ const thinkingLevel = agentTierResolution.snapshot
875
+ ? agentTierResolution.thinkingLevel
876
+ : options.tier !== undefined
877
+ ? tierResolution?.thinkingLevel
878
+ : options.thinkingLevel ?? agentConfig?.thinking;
879
+
880
+ if (agentTierResolution.snapshot) {
881
+ const { configuredModel, source } = agentTierResolution.snapshot;
882
+ const scopeVerdict = checkModelScope({
883
+ model,
884
+ cwd: ctx.cwd,
885
+ modelRegistry: ctx.modelRegistry,
886
+ // A tier the caller named is a runtime choice by the model, which is what
887
+ // scopeModels exists to police; a tier that came from the agent file or
888
+ // the configured default is the user's own config and only warns.
889
+ callerSupplied: source === "call",
890
+ agentLabel: agentConfig?.displayName ?? type,
891
+ modelInput: configuredModel === "inherit" ? undefined : configuredModel,
892
+ });
893
+ if (scopeVerdict.kind === "error") throw new Error(scopeVerdict.message);
894
+ if (scopeVerdict.kind === "warn" && ctx.hasUI) ctx.ui.notify(scopeVerdict.message, "warning");
895
+ }
851
896
 
852
897
  if (options.tier) {
853
898
  const configuredModel = tierResolution?.snapshot?.configuredModel;