@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 +88 -0
- package/README.md +111 -4
- package/examples/agent-tool-description.md +2 -3
- package/package.json +1 -1
- package/src/agent-file-toggle.ts +3 -2
- package/src/agent-manager.ts +21 -0
- package/src/agent-runner.ts +49 -4
- package/src/agent-tiers.ts +336 -0
- package/src/custom-agents.ts +45 -3
- package/src/default-agents.ts +5 -4
- package/src/index.ts +57 -14
- 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 +28 -0
- package/src/ui/conversation-viewer.ts +9 -4
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
|
-
| `
|
|
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`~~ | — | **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
|
-
| `
|
|
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
|
|
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-file-toggle.ts
CHANGED
|
@@ -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
|
-
|
|
695
|
-
|
|
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)}`);
|
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,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
|
-
|
|
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 =
|
|
849
|
-
?
|
|
850
|
-
: options.
|
|
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;
|