@signalridge/pi-subagents 1.2.0 → 1.3.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 +60 -0
- package/README.md +102 -4
- package/examples/agent-tool-description.md +2 -3
- package/package.json +1 -1
- package/src/agent-manager.ts +21 -0
- package/src/agent-runner.ts +57 -4
- package/src/agent-tiers.ts +297 -0
- package/src/custom-agents.ts +21 -0
- package/src/index.ts +40 -13
- package/src/invocation-config.ts +33 -8
- package/src/nested-tools.ts +8 -2
- package/src/schedule.ts +3 -0
- package/src/settings.ts +257 -5
- package/src/types.ts +21 -0
- package/src/ui/conversation-viewer.ts +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
### Minor Changes
|
|
5
|
+
|
|
6
|
+
- 1860def: Name your model tiers, and stop the orchestrator choosing models by hand.
|
|
7
|
+
|
|
8
|
+
A tier is one name for a (model, thinking) pair, defined in `subagents.json`:
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"agentTiers": {
|
|
13
|
+
"defaultTier": "medium",
|
|
14
|
+
"profiles": {
|
|
15
|
+
"small": { "description": "Fast, cheap exploration", "model": "deepseek/deepseek-v4-flash", "thinking": "max" },
|
|
16
|
+
"research": { "description": "Long-context research", "model": "kimi/k3", "thinking": "max" }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Keys are arbitrary — `small`/`medium`/`large` are an example, not a vocabulary.
|
|
23
|
+
A profile requires both `model` and `thinking` (either may be `"inherit"`);
|
|
24
|
+
`description` is optional and is what the host reads when choosing.
|
|
25
|
+
|
|
26
|
+
Resolution order: the `tier` on the call, then `tier:` in the agent's
|
|
27
|
+
frontmatter, then `agentTiers.defaultTier`, then the agent's legacy
|
|
28
|
+
`model:`/`thinking:`, then the parent session. A tier that applies decides both
|
|
29
|
+
fields outright, and an agent carrying both a tier and legacy pins logs a warning
|
|
30
|
+
naming the file. Unknown keys, malformed profiles and unavailable models refuse
|
|
31
|
+
the spawn before it starts, naming the tier and where it came from — none of them
|
|
32
|
+
quietly substitutes another model.
|
|
33
|
+
|
|
34
|
+
The catalogue is rendered into the `Agent` tool description at registration, so
|
|
35
|
+
the host knows the vocabulary before its first call rather than having to
|
|
36
|
+
remember a lookup tool. Custom descriptions gain `{{tierList}}`,
|
|
37
|
+
`{{compactTierList}}` and `{{defaultTier}}`.
|
|
38
|
+
|
|
39
|
+
Resolution happens once, in `agent-runner.ts`, so the top-level `Agent` tool,
|
|
40
|
+
nested delegation, the scheduler and cross-extension RPC share one precedence and
|
|
41
|
+
one set of refusals.
|
|
42
|
+
|
|
43
|
+
`pi-workflows` is untouched: its tiers stay the fixed `small | medium | large` of
|
|
44
|
+
the cross-package protocol, which is what keeps a workflow definition validatable
|
|
45
|
+
at parse time and portable between machines. The two systems share no fields.
|
|
46
|
+
|
|
47
|
+
The agent conversation overlay now marks its own edges with a rule at the top
|
|
48
|
+
and the bottom. It floats over the transcript and previously drew no border at
|
|
49
|
+
all — two variables named `hrTop` and `hrMid` were a bold title row and a blank
|
|
50
|
+
line — so there was no telling where the agent's conversation ended and the
|
|
51
|
+
parent's resumed. Two rules, none in between: an inner rule competes with the
|
|
52
|
+
pair that marks the boundary, and a four-sided box would cost two columns on
|
|
53
|
+
every row for the same job.
|
|
54
|
+
|
|
55
|
+
The LLM-facing `Agent` and nested-`Agent` schemas no longer accept `model` or
|
|
56
|
+
`thinking` — a caller picks a tier instead. This is a minor, not a major: a tool
|
|
57
|
+
schema is read by a model at runtime rather than compiled against, so no import,
|
|
58
|
+
settings key or RPC payload changes shape. Programmatic callers and the legacy
|
|
59
|
+
RPC still accept both fields, and agents with no tier configured behave exactly
|
|
60
|
+
as before. The migration is: define the profiles once, then replace each agent's
|
|
61
|
+
`model:`/`thinking:` with `tier: <name>`.
|
|
62
|
+
|
|
3
63
|
## 1.0.0
|
|
4
64
|
### Major Changes
|
|
5
65
|
|
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
|
-
| `
|
|
234
|
-
| `
|
|
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` | inherit parent | **Legacy.** 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. Ignored when a tier applies |
|
|
235
|
+
| `thinking` | inherit | **Legacy.** off, minimal, low, medium, high, xhigh, max — actual availability depends on your pi version and model; pi clamps unsupported levels down. Ignored when a tier applies |
|
|
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
|
-
| `
|
|
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,104 @@ 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. legacy `model:`/`thinking:` in the agent's frontmatter
|
|
468
|
+
5. the parent session's model and thinking
|
|
469
|
+
|
|
470
|
+
A tier that applies decides both fields outright — it is current policy, while a
|
|
471
|
+
per-agent `model:` pin is the older, weaker statement of the same thing. An agent
|
|
472
|
+
carrying both logs a warning naming the file to clean up.
|
|
473
|
+
|
|
474
|
+
### Refusals
|
|
475
|
+
|
|
476
|
+
These fail **before** the spawn, with the tier key and where it came from named.
|
|
477
|
+
None of them silently substitutes another model:
|
|
478
|
+
|
|
479
|
+
- a tier key nobody defined (from the call, the agent file, or `defaultTier`)
|
|
480
|
+
- a profile dropped as malformed during settings load
|
|
481
|
+
- a profile whose model is not available on this machine
|
|
482
|
+
- a syntactically invalid key (blank, whitespace, over 64 characters)
|
|
483
|
+
|
|
484
|
+
### Merging global and project settings
|
|
485
|
+
|
|
486
|
+
`~/.pi/agent/subagents.json` supplies the catalogue; `<cwd>/.pi/subagents.json`
|
|
487
|
+
edits it. A project profile replaces its global namesake **whole** — never field
|
|
488
|
+
by field, which would let a project change a model while inheriting a thinking
|
|
489
|
+
level nobody chose for that pair. A project profile that fails validation blocks
|
|
490
|
+
its global namesake rather than reviving it, and `defaultTier` is a simple
|
|
491
|
+
project-over-global override.
|
|
492
|
+
|
|
493
|
+
### Not the same as `workflow.tiers`
|
|
494
|
+
|
|
495
|
+
`pi-workflows` has its own tiers, and they stay fixed at `small | medium | large`.
|
|
496
|
+
That vocabulary is part of the cross-package protocol: it lets a workflow
|
|
497
|
+
definition be validated at parse time and stay portable between machines, neither
|
|
498
|
+
of which survives arbitrary names. The two systems share no fields — a spawn
|
|
499
|
+
records `agentTier`/`agentTierSnapshot` or `tier`/`tierSnapshot`, never one
|
|
500
|
+
standing in for the other.
|
|
501
|
+
|
|
502
|
+
### Migrating from `model:`/`thinking:`
|
|
503
|
+
|
|
504
|
+
Existing agents keep working: with no `agentTiers` configured, resolution falls
|
|
505
|
+
through to the legacy fields exactly as before. To migrate, define the profiles
|
|
506
|
+
once and replace the per-agent pins with `tier: <name>`. Programmatic callers and
|
|
507
|
+
the legacy RPC may still pass `model`/`thinking` directly; only the LLM-facing
|
|
508
|
+
`Agent` and nested-Agent schemas dropped them.
|
|
509
|
+
|
|
412
510
|
## Model Scope
|
|
413
511
|
|
|
414
512
|
**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
|
|
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
package/src/agent-manager.ts
CHANGED
|
@@ -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);
|
package/src/agent-runner.ts
CHANGED
|
@@ -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,29 @@ 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) {
|
|
648
|
+
options.onAgentTierResolved?.(agentTierResolution.snapshot);
|
|
649
|
+
if (agentConfig?.model !== undefined || agentConfig?.thinking !== undefined) {
|
|
650
|
+
console.warn(
|
|
651
|
+
`[pi-subagents] Agent "${agentConfig.name}" sets both tier "${agentTierResolution.snapshot.tier}" ` +
|
|
652
|
+
`and legacy model/thinking frontmatter; the tier wins. Remove model:/thinking: from the agent file.`,
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
625
657
|
// Resolve working directory: worktree override > parent cwd
|
|
626
658
|
const effectiveCwd = options.cwd ?? ctx.cwd;
|
|
627
659
|
// Filesystem work happens in effectiveCwd; config discovery in configCwd.
|
|
@@ -842,12 +874,33 @@ export async function runAgent(
|
|
|
842
874
|
}
|
|
843
875
|
}
|
|
844
876
|
|
|
845
|
-
|
|
877
|
+
// `options.model` stays highest: it is a resolved Model handed over by a
|
|
878
|
+
// programmatic caller, which is a more explicit act than naming a tier.
|
|
879
|
+
const model = options.model ?? agentTierResolution.model ?? tierResolution?.model ?? resolveDefaultModel(
|
|
846
880
|
ctx.model, ctx.modelRegistry, agentConfig?.model,
|
|
847
881
|
);
|
|
848
|
-
const thinkingLevel =
|
|
849
|
-
?
|
|
850
|
-
: options.
|
|
882
|
+
const thinkingLevel = agentTierResolution.snapshot
|
|
883
|
+
? agentTierResolution.thinkingLevel
|
|
884
|
+
: options.tier !== undefined
|
|
885
|
+
? tierResolution?.thinkingLevel
|
|
886
|
+
: options.thinkingLevel ?? agentConfig?.thinking;
|
|
887
|
+
|
|
888
|
+
if (agentTierResolution.snapshot) {
|
|
889
|
+
const { configuredModel, source } = agentTierResolution.snapshot;
|
|
890
|
+
const scopeVerdict = checkModelScope({
|
|
891
|
+
model,
|
|
892
|
+
cwd: ctx.cwd,
|
|
893
|
+
modelRegistry: ctx.modelRegistry,
|
|
894
|
+
// A tier the caller named is a runtime choice by the model, which is what
|
|
895
|
+
// scopeModels exists to police; a tier that came from the agent file or
|
|
896
|
+
// the configured default is the user's own config and only warns.
|
|
897
|
+
callerSupplied: source === "call",
|
|
898
|
+
agentLabel: agentConfig?.displayName ?? type,
|
|
899
|
+
modelInput: configuredModel === "inherit" ? undefined : configuredModel,
|
|
900
|
+
});
|
|
901
|
+
if (scopeVerdict.kind === "error") throw new Error(scopeVerdict.message);
|
|
902
|
+
if (scopeVerdict.kind === "warn" && ctx.hasUI) ctx.ui.notify(scopeVerdict.message, "warning");
|
|
903
|
+
}
|
|
851
904
|
|
|
852
905
|
if (options.tier) {
|
|
853
906
|
const configuredModel = tierResolution?.snapshot?.configuredModel;
|