@signalridge/pi-subagents 1.3.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 +28 -0
- package/README.md +21 -12
- package/package.json +1 -1
- package/src/agent-file-toggle.ts +3 -2
- package/src/agent-runner.ts +1 -9
- package/src/agent-tiers.ts +39 -0
- package/src/custom-agents.ts +24 -3
- package/src/default-agents.ts +5 -4
- package/src/index.ts +17 -1
- package/src/types.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
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
|
+
|
|
3
31
|
## 1.3.0
|
|
4
32
|
### Minor Changes
|
|
5
33
|
|
package/README.md
CHANGED
|
@@ -231,8 +231,8 @@ All fields are optional — sensible defaults for everything.
|
|
|
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
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
|
-
|
|
|
235
|
-
|
|
|
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 |
|
|
236
236
|
| `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited |
|
|
237
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` |
|
|
238
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:` |
|
|
@@ -464,15 +464,21 @@ the next pi session, since the description is built once at registration.
|
|
|
464
464
|
1. `tier` passed to the `Agent` call
|
|
465
465
|
2. `tier:` in the agent's frontmatter
|
|
466
466
|
3. `agentTiers.defaultTier`
|
|
467
|
-
4.
|
|
468
|
-
5. the parent session's model and thinking
|
|
467
|
+
4. the parent session's model and thinking
|
|
469
468
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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.
|
|
473
474
|
|
|
474
475
|
### Refusals
|
|
475
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
|
+
|
|
476
482
|
These fail **before** the spawn, with the tier key and where it came from named.
|
|
477
483
|
None of them silently substitutes another model:
|
|
478
484
|
|
|
@@ -501,11 +507,14 @@ standing in for the other.
|
|
|
501
507
|
|
|
502
508
|
### Migrating from `model:`/`thinking:`
|
|
503
509
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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.
|
|
509
518
|
|
|
510
519
|
## Model Scope
|
|
511
520
|
|
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-runner.ts
CHANGED
|
@@ -644,15 +644,7 @@ export async function runAgent(
|
|
|
644
644
|
parentThinking,
|
|
645
645
|
modelRegistry: ctx.modelRegistry,
|
|
646
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
|
-
}
|
|
647
|
+
if (agentTierResolution.snapshot) options.onAgentTierResolved?.(agentTierResolution.snapshot);
|
|
656
648
|
|
|
657
649
|
// Resolve working directory: worktree override > parent cwd
|
|
658
650
|
const effectiveCwd = options.cwd ?? ctx.cwd;
|
package/src/agent-tiers.ts
CHANGED
|
@@ -237,6 +237,45 @@ export function resolveAgentTier(input: ResolveAgentTierInput): AgentTierResolut
|
|
|
237
237
|
return { model, thinkingLevel, snapshot };
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Every tier name that is referenced but not defined.
|
|
242
|
+
*
|
|
243
|
+
* The resolver refuses these at spawn time anyway, but a `defaultTier` typo
|
|
244
|
+
* would otherwise sit quiet until the first agent that names no tier — which
|
|
245
|
+
* may be minutes into a session, in the middle of something. Checking the
|
|
246
|
+
* references once, when settings and agents are loaded, moves that discovery to
|
|
247
|
+
* where it is cheap and where the fix is obvious.
|
|
248
|
+
*
|
|
249
|
+
* `agentNames` maps an agent to the tier its frontmatter asks for, so a typo in
|
|
250
|
+
* one agent file is reported the same way as one in `defaultTier`.
|
|
251
|
+
*/
|
|
252
|
+
export function findUnknownAgentTierReferences(
|
|
253
|
+
settings: AgentTiersSettings,
|
|
254
|
+
agentTiers: ReadonlyMap<string, string> = new Map(),
|
|
255
|
+
): string[] {
|
|
256
|
+
const defined = new Set(Object.keys(settings.profiles ?? {}));
|
|
257
|
+
// With nothing configured there is no catalogue to be wrong about; the
|
|
258
|
+
// resolver simply never applies a tier.
|
|
259
|
+
if (defined.size === 0 && settings.defaultTier === undefined) return [];
|
|
260
|
+
|
|
261
|
+
const problems: string[] = [];
|
|
262
|
+
if (settings.defaultTier !== undefined && !defined.has(settings.defaultTier)) {
|
|
263
|
+
problems.push(
|
|
264
|
+
`agentTiers.defaultTier is "${settings.defaultTier}", which is not a defined tier. ` +
|
|
265
|
+
`Available: ${tierKeyList(settings)}`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
for (const [agent, tier] of [...agentTiers].sort(([a], [b]) => a.localeCompare(b))) {
|
|
269
|
+
if (!defined.has(tier)) {
|
|
270
|
+
problems.push(
|
|
271
|
+
`Agent "${agent}" asks for tier "${tier}", which is not a defined tier. ` +
|
|
272
|
+
`Available: ${tierKeyList(settings)}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return problems;
|
|
277
|
+
}
|
|
278
|
+
|
|
240
279
|
/**
|
|
241
280
|
* The tier catalogue, rendered for the `Agent` tool description.
|
|
242
281
|
*
|
package/src/custom-agents.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
|
8
8
|
import { isValidAgentTierKey } from "./agent-tiers.js";
|
|
9
9
|
import { BUILTIN_TOOL_NAMES } from "./agent-types.js";
|
|
10
10
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
11
|
-
import type { AgentConfig, MemoryScope
|
|
11
|
+
import type { AgentConfig, MemoryScope } from "./types.js";
|
|
12
12
|
import { sanitizeDisplayText } from "./ui/safe-text.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -164,6 +164,7 @@ function loadFromDir(
|
|
|
164
164
|
const { frontmatter: fm, body } = parsed;
|
|
165
165
|
|
|
166
166
|
const { builtinToolNames, extSelectors } = parseToolsField(fm.tools);
|
|
167
|
+
warnLegacyModelFields(fm, path, warn);
|
|
167
168
|
|
|
168
169
|
agents.set(name, {
|
|
169
170
|
name,
|
|
@@ -176,8 +177,6 @@ function loadFromDir(
|
|
|
176
177
|
excludeExtensions: csvListOptional(fm.exclude_extensions),
|
|
177
178
|
skills: inheritField(fm.skills ?? fm.inherit_skills),
|
|
178
179
|
agentTier: parseTier(fm.tier, path, warn),
|
|
179
|
-
model: str(fm.model),
|
|
180
|
-
thinking: str(fm.thinking) as ThinkingLevel | undefined,
|
|
181
180
|
maxTurns: nonNegativeInt(fm.max_turns),
|
|
182
181
|
persistSession: fm.persist_session != null ? fm.persist_session === true : undefined,
|
|
183
182
|
outputTranscript: fm.output_transcript != null ? fm.output_transcript !== false : undefined,
|
|
@@ -197,6 +196,25 @@ function loadFromDir(
|
|
|
197
196
|
priorities.set(name, priority);
|
|
198
197
|
}
|
|
199
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Report a `model:`/`thinking:` pin left over from before tiers.
|
|
201
|
+
*
|
|
202
|
+
* An agent file no longer chooses its own model — the tier catalogue in
|
|
203
|
+
* `subagents.json` does, and a per-file pin would be a way around it. The file
|
|
204
|
+
* still loads: a stale pin is a migration the author has not done yet, not a
|
|
205
|
+
* reason to take the agent away mid-session. It simply has no effect, and the
|
|
206
|
+
* warning names the file so it can be fixed.
|
|
207
|
+
*/
|
|
208
|
+
function warnLegacyModelFields(fm: Record<string, unknown>, path: string, warn: WarningSink): void {
|
|
209
|
+
const present = ["model", "thinking"].filter((field) => fm[field] != null);
|
|
210
|
+
if (present.length === 0) return;
|
|
211
|
+
warn(
|
|
212
|
+
`Ignoring ${present.join(" and ")} in ${path}: agents pick a model with "tier:" now. ` +
|
|
213
|
+
`Replace it with a tier from agentTiers.profiles, or remove it to use the default tier.`,
|
|
214
|
+
`legacy-model:${warningIdentity(path)}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
200
218
|
/** Read and parse one agent file, warning or throwing with its path on failure. */
|
|
201
219
|
function readAgentFile(
|
|
202
220
|
path: string,
|
|
@@ -279,6 +297,9 @@ function label(val: unknown): string | undefined {
|
|
|
279
297
|
*/
|
|
280
298
|
function parseTier(val: unknown, path: string, warn: WarningSink): string | undefined {
|
|
281
299
|
if (val === undefined || val === null) return undefined;
|
|
300
|
+
// Deliberately not "no tier means no model": which model an agent runs is
|
|
301
|
+
// decided by the tier catalogue, so an agent that names no tier falls to
|
|
302
|
+
// `agentTiers.defaultTier` rather than pinning anything itself.
|
|
282
303
|
if (isValidAgentTierKey(val)) return val;
|
|
283
304
|
warn(
|
|
284
305
|
`Ignoring invalid tier in ${path}: expected a non-empty single-word key`,
|
package/src/default-agents.ts
CHANGED
|
@@ -34,10 +34,11 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
|
|
|
34
34
|
builtinToolNames: READ_ONLY_TOOLS,
|
|
35
35
|
extensions: true,
|
|
36
36
|
skills: true,
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
37
|
+
// No model pin. Which model a subagent runs is the tier catalogue's
|
|
38
|
+
// decision; a built-in that pinned one would be the same end-run around it
|
|
39
|
+
// that agent frontmatter is no longer allowed to make, and it would name a
|
|
40
|
+
// vendor on a machine that may not have it. With no tier configured this
|
|
41
|
+
// inherits the parent's model, which is the documented fallback.
|
|
41
42
|
systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
|
|
42
43
|
You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
|
43
44
|
Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools.
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
buildAgentTierListText,
|
|
32
32
|
buildAgentTierParameterDescription,
|
|
33
33
|
buildCompactAgentTierListText,
|
|
34
|
+
findUnknownAgentTierReferences,
|
|
34
35
|
getAgentTiersSettings,
|
|
35
36
|
getDefaultAgentTierText,
|
|
36
37
|
setAgentTiersSettings,
|
|
@@ -1191,7 +1192,7 @@ function activateRootRuntime(
|
|
|
1191
1192
|
|
|
1192
1193
|
return available.map((name) => {
|
|
1193
1194
|
const cfg = getAgentConfig(name);
|
|
1194
|
-
const modelSuffix = cfg?.
|
|
1195
|
+
const modelSuffix = cfg?.agentTier ? ` (tier: ${cfg.agentTier})` : "";
|
|
1195
1196
|
const toolsSuffix = ` (Tools: ${formatToolsSuffix(cfg)})`;
|
|
1196
1197
|
return `- ${name}: ${cfg?.description ?? name}${modelSuffix}${toolsSuffix}`;
|
|
1197
1198
|
}).join("\n");
|
|
@@ -1239,6 +1240,21 @@ function activateRootRuntime(
|
|
|
1239
1240
|
});
|
|
1240
1241
|
pi.events.emit("subagents:settings_loaded", { settings: startupSettings });
|
|
1241
1242
|
|
|
1243
|
+
// A tier that nothing defines is a typo, and the resolver would only reach it
|
|
1244
|
+
// on the first spawn that needs it — possibly minutes in, mid-task. Report it
|
|
1245
|
+
// now, while the fix is obvious and nothing has run.
|
|
1246
|
+
const tierReferenceProblems = findUnknownAgentTierReferences(
|
|
1247
|
+
getAgentTiersSettings(),
|
|
1248
|
+
new Map(
|
|
1249
|
+
getAvailableTypes()
|
|
1250
|
+
.map((name): [string, string | undefined] => [name, getAgentConfig(name)?.agentTier])
|
|
1251
|
+
.filter((entry): entry is [string, string] => entry[1] !== undefined),
|
|
1252
|
+
),
|
|
1253
|
+
);
|
|
1254
|
+
for (const problem of tierReferenceProblems) {
|
|
1255
|
+
console.warn(`[pi-subagents] ${problem}`);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1242
1258
|
// ---- Agent tool ----
|
|
1243
1259
|
|
|
1244
1260
|
// Schedule param + its guideline are gated on `schedulingEnabled` (read once
|
package/src/types.ts
CHANGED
|
@@ -59,7 +59,14 @@ export interface AgentConfig {
|
|
|
59
59
|
* tiers existed.
|
|
60
60
|
*/
|
|
61
61
|
agentTier?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Programmatic-only model override. Never populated from an agent file: which
|
|
64
|
+
* model a subagent runs is the tier catalogue's decision, and `model:` in
|
|
65
|
+
* frontmatter is read as documentation of an unfinished migration rather than
|
|
66
|
+
* a pin. Left on the type for callers that construct a config in process.
|
|
67
|
+
*/
|
|
62
68
|
model?: string;
|
|
69
|
+
/** Programmatic-only, for the same reason as `model` above. */
|
|
63
70
|
thinking?: ThinkingLevel;
|
|
64
71
|
maxTurns?: number;
|
|
65
72
|
/** Persist this subagent as a normal pi session instead of keeping it in memory only. */
|