@arhen/pi-core-subagent 1.3.61 → 1.3.62
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/README.md +7 -4
- package/package.json +6 -6
- package/src/format.ts +75 -0
- package/src/index.ts +25 -2
- package/src/manager.ts +11 -29
- package/src/modelconfig.ts +131 -0
- package/src/models.ts +184 -0
- package/src/schemas.ts +24 -4
- package/src/types.ts +52 -0
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ flowchart LR
|
|
|
53
53
|
- **Agent files respected.** A spawn goal (name + task) that matches a user agent file's `description` (`.agents/agents`, `.claude/agents`, `.pi/agents` — project then home) loads that file — body = system prompt, frontmatter `model`/`tools` apply, file `model` validated against the pi model registry. File wins over inline; no match → on-demand definition.
|
|
54
54
|
- **Two toolsets, plus explicit override.** Read-only (`read, grep, find, ls, codemode` — default) or write (`read, grep, find, ls, bash, edit, write, codemode` — `write: true`); `tools:` sets an explicit per-task allowlist. Children run with `noExtensions`, so `codemode` is the one extension they get: it lets a child batch `tools.*` calls instead of spending a model turn per call.
|
|
55
55
|
- **In-process** — children are `AgentSession`s in the same runtime. No process spawn, no context bleed.
|
|
56
|
-
- **Zero parent-context injection.** No catalog, no context hook.
|
|
56
|
+
- **Zero parent-context injection.** No catalog, no context hook. 9 slim tools total.
|
|
57
57
|
- **Throttled updates** — widget/stream updates coalesce to ~6/s; no per-event deep clones.
|
|
58
58
|
- **Bounded always** — a child is limited only by wall clock: explicit `maxRuntimeMs`, else the 1 h ceiling with `/subagents auto-limit on`, else the 6 h safety ceiling (default).
|
|
59
59
|
|
|
@@ -284,10 +284,13 @@ Background (default) + intercom — the run returns a runId immediately; you sta
|
|
|
284
284
|
|
|
285
285
|
**Resuming a failed child:** a child that dies mid-work (provider rate limit, timeout, network error) keeps its session JSONL and its worktree branch. `resume_subagent({ runId, taskId, model?: "openai/gpt-5", message? })` reopens that session with full context, re-attaches the branch, and prompts it to recap and continue — no respawn, no lost tokens. `model` swaps provider when the original one is exhausted. Refused for tasks that never started (no session file); those you respawn. Wait for the run to settle before resuming (the tool tells you if it hasn't).
|
|
286
286
|
|
|
287
|
+
**Choosing a model:** `subagent_models` lists what this session may use — the same set `/scoped-models` shows when scoping is configured, and every model with usable credentials when it is not (the output states which case applies). Each row gives the exact `model` value to pass, the thinking levels the runtime honors, the context window, and pi's catalog price per million tokens (`free` only when the reported rates are zero; absent rates read `unavailable`; catalog rates are a planning guide, not a billing quote). `model` is optional: omit it to inherit the leader's current session model, or name one (a matched agent file's `model` frontmatter still wins) to pin the run. `~/.pi/agent/subagent-models.json` may shape the listing only — `prefer` sorts, `hide` omits, `default` is surfaced as a suggestion and never applied. It never grants or blocks a model: a hidden model still runs when named, because pi owns what may run.
|
|
288
|
+
|
|
287
289
|
## Tools
|
|
288
290
|
|
|
289
291
|
| Tool | Purpose |
|
|
290
292
|
|---|---|
|
|
293
|
+
| `subagent_models` | list the models a subagent task may name, each with the exact `model` value, thinking levels the runtime honors, context window, and catalog price; scoped to the session's enabled set when scoping is configured, else the full available catalogue (the output says which) |
|
|
291
294
|
| `subagent` | single / `tasks` (parallel or graph via `needs`) / `chain` (`{previous}`); every run is background — returns a runId, completion notifies you; `autoAwait:true` parks the call until the run finishes and returns the final result inline; children always carry talk tools (ask/notify/mailbox); `notifyPerTask` (default true) wakes you as each task completes |
|
|
292
295
|
| `subagent_status` | live per-task snapshot (non-blocking), including each child's session file path |
|
|
293
296
|
| `subagent_result` | full output of a run or one task |
|
|
@@ -299,7 +302,7 @@ Background (default) + intercom — the run returns a runId immediately; you sta
|
|
|
299
302
|
|
|
300
303
|
### Per-task fields
|
|
301
304
|
|
|
302
|
-
`agent` (name you invent — required), `task` (required), `prompt` (system prompt, optional — minimal default used), `write` (toolset, default read-only), plus optional `model` (`provider/model-id
|
|
305
|
+
`agent` (name you invent — required), `task` (required), `prompt` (system prompt, optional — minimal default used), `write` (toolset, default read-only), plus optional `model` (`provider/model-id`; omitted → the leader's current session model, a matched agent file's frontmatter wins), `thinking` (validated enum: `off|minimal|low|medium|high|xhigh|max`), `tools` (explicit allowlist), `cwd`, `maxRuntimeMs`, `id`, `needs` (dependency edges — see [Graph mode](#graph-mode--needs)). Top-level only: `autoAwait`, `notifyPerTask`, `concurrency`.
|
|
303
306
|
|
|
304
307
|
### Child talk tools (always on)
|
|
305
308
|
|
|
@@ -356,9 +359,9 @@ The extension has no multiplexer integration and does not want one: it exposes t
|
|
|
356
359
|
|
|
357
360
|
## Context budget
|
|
358
361
|
|
|
359
|
-
- Parent tools:
|
|
362
|
+
- Parent tools: 9 schemas with short descriptions. **No catalog, no context hook** — nothing injected per request.
|
|
360
363
|
- Background completion: 3-line notice. Full text only via `subagent_result`.
|
|
361
|
-
- Children: isolated sessions; talk tools always injected; each child's prompt states its own task id and its siblings' so mailbox addressing works. Model resolution: explicit `provider/model-id` or bare id via the pi model registry → the parent's current model → settings default. Thinking levels validated against the resolved model's `thinkingLevelMap
|
|
364
|
+
- Children: isolated sessions; talk tools always injected; each child's prompt states its own task id and its siblings' so mailbox addressing works. Model resolution: explicit `provider/model-id` or bare id via the pi model registry → the parent's current model → settings default. Thinking levels validated against the resolved model's `thinkingLevelMap`; `subagent_models` lists the levels the runtime honors when you need a safe set.
|
|
362
365
|
|
|
363
366
|
## What this is built on
|
|
364
367
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arhen/pi-core-subagent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.62",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi extension: fast in-process subagents with a dependency-graph scheduler (needs edges gate tasks and carry upstream output into dependent prompts), plus background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
"graph-protocol"
|
|
30
30
|
],
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@earendil-works/pi-ai": "
|
|
33
|
-
"@earendil-works/pi-agent-core": "
|
|
34
|
-
"@earendil-works/pi-coding-agent": "
|
|
35
|
-
"@earendil-works/pi-tui": "
|
|
36
|
-
"typebox": "
|
|
32
|
+
"@earendil-works/pi-ai": "*",
|
|
33
|
+
"@earendil-works/pi-agent-core": "*",
|
|
34
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
35
|
+
"@earendil-works/pi-tui": "*",
|
|
36
|
+
"typebox": "*"
|
|
37
37
|
},
|
|
38
38
|
"pi": {
|
|
39
39
|
"extensions": [
|
package/src/format.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import {
|
|
5
5
|
MAX_TASKS,
|
|
6
|
+
type ModelCatalog,
|
|
7
|
+
type ModelPricing,
|
|
6
8
|
type RunSnapshot,
|
|
7
9
|
type RunStatus,
|
|
8
10
|
type TaskSnapshot,
|
|
@@ -271,3 +273,76 @@ export function makeNotice(run: RunSnapshot, kind: string): string {
|
|
|
271
273
|
lines.push(`Use subagent_result(runId: "${run.id}") for full output.`);
|
|
272
274
|
return lines.join("\n");
|
|
273
275
|
}
|
|
276
|
+
|
|
277
|
+
/** Per-million-token rates, terse. A zero rate reads as "free"; absent rates are "unavailable", not free. */
|
|
278
|
+
function priceTag(cost: ModelPricing | undefined): string {
|
|
279
|
+
if (!cost) return "unavailable (provider did not report rates)";
|
|
280
|
+
if (cost.input === 0 && cost.output === 0 && cost.cacheRead === 0 && cost.cacheWrite === 0) return "free";
|
|
281
|
+
const rate = (n: number) => (n === 0 ? "free" : `$${n.toFixed(n < 0.01 ? 4 : 2)}`);
|
|
282
|
+
const parts = [`in ${rate(cost.input)}`, `out ${rate(cost.output)}`];
|
|
283
|
+
if (cost.cacheRead === undefined) parts.push("cache-read unavailable");
|
|
284
|
+
else if (cost.cacheRead > 0) parts.push(`cache-read ${rate(cost.cacheRead)}`);
|
|
285
|
+
if (cost.cacheWrite === undefined) parts.push("cache-write unavailable");
|
|
286
|
+
else if (cost.cacheWrite > 0) parts.push(`cache-write ${rate(cost.cacheWrite)}`);
|
|
287
|
+
return `${parts.join(", ")} per Mtok`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Render the model catalog as the `subagent_models` tool result. Pure, so the exact text an agent
|
|
292
|
+
* receives is testable: the tool handler holds no formatting of its own. Throws when the list is
|
|
293
|
+
* empty — the SDK tool loop discards a returned `isError` for any `execute` that does not throw.
|
|
294
|
+
*/
|
|
295
|
+
export function renderModelCatalog(catalog: ModelCatalog): {
|
|
296
|
+
content: { type: "text"; text: string }[];
|
|
297
|
+
details: ModelCatalog;
|
|
298
|
+
} {
|
|
299
|
+
if (catalog.models.length === 0) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`No models can be listed for subagent tasks: ${catalog.unavailable ?? "the model registry returned no models"}. This is not an empty catalog — a task without \`model\` still inherits the session model, but naming one needs the references. Fix model configuration, then retry.`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const lines = catalog.models.map((model) =>
|
|
305
|
+
[
|
|
306
|
+
`- model: "${model.reference}"`,
|
|
307
|
+
` ${model.name}; ${model.contextWindow > 0 ? `${model.contextWindow.toLocaleString("en-US")} context` : "context window unreported"}`,
|
|
308
|
+
` price: ${priceTag(model.cost)}`,
|
|
309
|
+
model.reasoning
|
|
310
|
+
? ` thinking levels: ${model.thinkingLevels.join(" | ")}`
|
|
311
|
+
: ` thinking: not supported — omit it or pass "off"`,
|
|
312
|
+
].join("\n"),
|
|
313
|
+
);
|
|
314
|
+
const heading =
|
|
315
|
+
catalog.scope === "session"
|
|
316
|
+
? `${catalog.models.length} model(s) enabled for this session. Pass the \`model\` value verbatim in a subagent task:`
|
|
317
|
+
: `${catalog.models.length} model(s) available — this session has no model scoping, so every model with usable credentials is listed. Pass the \`model\` value verbatim in a subagent task:`;
|
|
318
|
+
const suggestion = catalog.preferredDefault
|
|
319
|
+
? `\n\nThis configuration suggests \`model: "${catalog.preferredDefault}"\`. It is a preference, never applied automatically.`
|
|
320
|
+
: "";
|
|
321
|
+
const unlistedDefault = catalog.unlistedDefault
|
|
322
|
+
? `\n\nNOTE: configured default \`${catalog.unlistedDefault}\` is not in the displayed catalog; it is not suggested or applied.`
|
|
323
|
+
: "";
|
|
324
|
+
const hidden =
|
|
325
|
+
catalog.hidden && catalog.hidden > 0
|
|
326
|
+
? `\n\n${catalog.hidden} enabled model(s) are hidden by your model preferences.`
|
|
327
|
+
: "";
|
|
328
|
+
const unused = catalog.unusedPatterns?.length
|
|
329
|
+
? `\n\nNOTE: these preference patterns matched no listed model and did nothing: ${catalog.unusedPatterns.join(", ")}. They may target models that are not enabled.`
|
|
330
|
+
: "";
|
|
331
|
+
const ambiguous = catalog.ambiguous?.length
|
|
332
|
+
? `\n\nDo not pass these references: ${catalog.ambiguous.join(", ")} — ${catalog.reason}.`
|
|
333
|
+
: "";
|
|
334
|
+
const unresolved = catalog.unresolved?.length ? `\n\n${catalog.unresolvedReason}` : "";
|
|
335
|
+
const configError = catalog.configError
|
|
336
|
+
? `\n\nWARNING: the model preferences file could not be used (${catalog.configError}). Continuing with no preferences.`
|
|
337
|
+
: "";
|
|
338
|
+
const billing = "\n\nPrices are pi catalog list rates per Mtok, not a billing quote.";
|
|
339
|
+
return {
|
|
340
|
+
content: [
|
|
341
|
+
{
|
|
342
|
+
type: "text",
|
|
343
|
+
text: `${heading}\n${lines.join("\n")}${suggestion}${unlistedDefault}${hidden}${unused}${ambiguous}${unresolved}${configError}${billing}`,
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
details: catalog,
|
|
347
|
+
};
|
|
348
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
compactLines,
|
|
5
|
+
formatUsage,
|
|
6
|
+
makeSummary,
|
|
7
|
+
renderModelCatalog,
|
|
8
|
+
statusIcon,
|
|
9
|
+
taskLine,
|
|
10
|
+
truncateText,
|
|
11
|
+
} from "./format.ts";
|
|
4
12
|
import { waveNotation } from "./graph.ts";
|
|
5
13
|
import { cloneRun, type ParkedMsg, SubagentManager } from "./manager.ts";
|
|
14
|
+
import { listSelectableModels } from "./models.ts";
|
|
6
15
|
import { createPeekPane, type PeekTask } from "./peek.ts";
|
|
7
16
|
import {
|
|
8
17
|
AwaitParam,
|
|
18
|
+
ModelsParam,
|
|
9
19
|
ReplyParam,
|
|
10
20
|
ResultParam,
|
|
11
21
|
ResumeParam,
|
|
@@ -14,7 +24,7 @@ import {
|
|
|
14
24
|
SubagentParams,
|
|
15
25
|
type SubagentParamsShape,
|
|
16
26
|
} from "./schemas.ts";
|
|
17
|
-
import { type RunDetails, type RunSnapshot, TERMINAL } from "./types.ts";
|
|
27
|
+
import { type ModelCatalog, type RunDetails, type RunSnapshot, TERMINAL } from "./types.ts";
|
|
18
28
|
import { cleanupMerged, ownerAlive, reapDeadWorktrees, repoRoot, sweepStale } from "./worktree.ts";
|
|
19
29
|
|
|
20
30
|
export default function (pi: ExtensionAPI) {
|
|
@@ -124,6 +134,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
124
134
|
manager.clearRuns();
|
|
125
135
|
});
|
|
126
136
|
|
|
137
|
+
pi.registerTool<typeof ModelsParam, ModelCatalog>({
|
|
138
|
+
name: "subagent_models",
|
|
139
|
+
label: "Subagent Models",
|
|
140
|
+
description:
|
|
141
|
+
"List the models a subagent task may name, each with the exact `model` value to pass. The list is scoped to this session's enabled models when scoping is configured (the same set `/scoped-models` shows); when no scoping is configured, every model with usable credentials is listed and the output says so. Each entry includes its context window and pi catalog price per million tokens, and the thinking levels the runtime honors — a level shown here is not silently clamped. References shown here are safe to pass; ambiguous ones are called out. Naming a model is optional: omit `model` to inherit the session model, or call this when choosing one.",
|
|
142
|
+
promptSnippet: "List the models a subagent task can name (reference, thinking levels, context, price).",
|
|
143
|
+
parameters: ModelsParam,
|
|
144
|
+
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
145
|
+
return renderModelCatalog(listSelectableModels(ctx));
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
127
149
|
pi.registerTool<typeof SubagentParams, RunDetails>({
|
|
128
150
|
name: "subagent",
|
|
129
151
|
label: "Subagent",
|
|
@@ -132,6 +154,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
132
154
|
"Run isolated subagents (own context, own session) in the background: returns a runId immediately, completion notifies you. One call = one agent (`agent`+`task`) or many (`tasks`, or `chain` with `{previous}`). `needs` edges gate tasks and prepend upstream outputs to their prompts. A user agent file (`.agents/agents`, `.claude/agents`, `.pi/agents`; project dirs, then home) whose `description` matches the goal is authoritative: body = system prompt, frontmatter `model`/`tools` apply, but explicit per-call `tools`/`write` override the file's tools. Write agents get an isolated git worktree; the result reports the branch. Children always carry talk tools (ask/notify the leader, message siblings).",
|
|
133
155
|
promptSnippet: "Define and delegate work to specialized subagents.",
|
|
134
156
|
promptGuidelines: [
|
|
157
|
+
"`model` is optional: omit it to inherit your current session model, or name one (agent-file `model` frontmatter wins) to pin the run. Call subagent_models for the exact references, thinking levels, and prices this session may use.",
|
|
135
158
|
"Use subagent when independent review, testing, research, or parallel analysis improves quality.",
|
|
136
159
|
"Batch every sub-task in ONE call: subagent({ tasks: [...] }) — never multiple parallel subagent calls.",
|
|
137
160
|
"Declare ordering with `needs` edges on the tasks, never by splitting into separate calls; dependents receive upstream outputs automatically — do not restate them. Prefer flat `tasks` (plain parallel); add `needs` only when ordering genuinely matters.",
|
package/src/manager.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
} from "./format.ts";
|
|
38
38
|
import { applyUpstream, resolveNeeds, runWaveScheduler } from "./graph.ts";
|
|
39
39
|
import { createMailbox, type Mailbox } from "./mailbox.ts";
|
|
40
|
+
import { chooseModel, resolveChildModel } from "./models.ts";
|
|
40
41
|
import type { SubagentParamsShape, TaskInput } from "./schemas.ts";
|
|
41
42
|
import {
|
|
42
43
|
MAX_TASKS,
|
|
@@ -174,27 +175,6 @@ export function clampResumeThinking(
|
|
|
174
175
|
return clampThinkingLevel(model, thinking) as ThinkingLevel;
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
export function resolveChildModel(ctx: ExtensionContext, explicit: string | undefined) {
|
|
178
|
-
if (!explicit?.trim()) return ctx.model;
|
|
179
|
-
const ref = explicit.trim();
|
|
180
|
-
if (!ctx.modelRegistry) return ctx.model;
|
|
181
|
-
const available = ctx.modelRegistry.getAvailable();
|
|
182
|
-
const sessionProvider = ctx.model?.provider;
|
|
183
|
-
if (sessionProvider && !ref.includes("/")) {
|
|
184
|
-
const own = available.filter((m) => m.provider === sessionProvider);
|
|
185
|
-
const hit = own.find((m) => m.id === ref) ?? own.find((m) => m.id.endsWith(`/${ref}`));
|
|
186
|
-
if (hit) return hit;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const byId = available.find((m) => m.id === ref);
|
|
190
|
-
if (byId) return byId;
|
|
191
|
-
for (let slash = ref.indexOf("/"); slash > 0; slash = ref.indexOf("/", slash + 1)) {
|
|
192
|
-
const model = ctx.modelRegistry.find(ref.slice(0, slash), ref.slice(slash + 1));
|
|
193
|
-
if (model) return model;
|
|
194
|
-
}
|
|
195
|
-
throw new Error(`Model not found: ${ref}`);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
178
|
const PROBE_THINKING_LEVELS: ThinkingLevel[] = ["low", "minimal", "medium", "high", "xhigh", "max"];
|
|
199
179
|
|
|
200
180
|
/**
|
|
@@ -816,7 +796,7 @@ export class SubagentManager {
|
|
|
816
796
|
|
|
817
797
|
let model: Model<Api> | undefined;
|
|
818
798
|
try {
|
|
819
|
-
model = resolveChildModel(ctx, file
|
|
799
|
+
model = resolveChildModel(ctx, chooseModel(file, input.model).requested);
|
|
820
800
|
validateThinking(model, thinking);
|
|
821
801
|
|
|
822
802
|
const checked = await ensureUsableModel(ctx, model, signal, thinking);
|
|
@@ -1207,12 +1187,12 @@ export class SubagentManager {
|
|
|
1207
1187
|
const input = inputs[i] as TaskInput;
|
|
1208
1188
|
const cwd = input.cwd ?? ctx.cwd;
|
|
1209
1189
|
const file = resolveAgentFile(input.agent, input.task, cwd, getAgentDir());
|
|
1210
|
-
const
|
|
1190
|
+
const choice = chooseModel(file, input.model);
|
|
1211
1191
|
try {
|
|
1212
|
-
validateThinking(resolveChildModel(ctx, requested), input.thinking);
|
|
1192
|
+
validateThinking(resolveChildModel(ctx, choice.requested), input.thinking);
|
|
1213
1193
|
} catch (err) {
|
|
1214
|
-
const where =
|
|
1215
|
-
? ` (from agent file ${
|
|
1194
|
+
const where = choice.sourceFile
|
|
1195
|
+
? ` (from agent file ${choice.sourceFile}, which overrides the requested model${input.model ? ` "${input.model}"` : ""})`
|
|
1216
1196
|
: "";
|
|
1217
1197
|
throw new Error(
|
|
1218
1198
|
`Task ${input.id ?? `task_${i + 1}`} (${input.agent}): ${err instanceof Error ? err.message : String(err)}${where}`,
|
|
@@ -1435,11 +1415,13 @@ export class SubagentManager {
|
|
|
1435
1415
|
const tools = task.tools?.filter((t) => !(CHILD_TALK_TOOLS as readonly string[]).includes(t));
|
|
1436
1416
|
const write = tools?.some((t) => WRITE_CAPABLE.includes(t)) ?? false;
|
|
1437
1417
|
// A resume may swap the model, so the level stored on the task can be one the new model
|
|
1438
|
-
// rejects (a mode-clamped xhigh onto a model that only takes low|high|max). Clamp it
|
|
1439
|
-
// the
|
|
1418
|
+
// rejects (a mode-clamped xhigh onto a model that only takes low|high|max). Clamp it against
|
|
1419
|
+
// the model runChild will actually use — including a matched agent file's, whose frontmatter
|
|
1420
|
+
// overrides the inline model at spawn — or the clamp would validate a different model.
|
|
1421
|
+
const file = resolveAgentFile(task.agent, task.task, task.cwd, getAgentDir());
|
|
1440
1422
|
let resumeModel: Model<Api> | undefined;
|
|
1441
1423
|
try {
|
|
1442
|
-
resumeModel = resolveChildModel(ctx, opts.model ?? task.model);
|
|
1424
|
+
resumeModel = resolveChildModel(ctx, chooseModel(file, opts.model ?? task.model).requested);
|
|
1443
1425
|
} catch {}
|
|
1444
1426
|
const requestedThinking = (opts.thinking ?? task.thinking) as ThinkingLevel | undefined;
|
|
1445
1427
|
const thinking = clampResumeThinking(resumeModel, requestedThinking);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Optional user preferences for `subagent_models`. This filters and orders what the tool shows and
|
|
6
|
+
* nothing else: it never grants or blocks a model, because pi already owns what may run. A missing
|
|
7
|
+
* file, malformed JSON, or an unknown key degrades to no preferences, so a config typo cannot break
|
|
8
|
+
* delegation.
|
|
9
|
+
*/
|
|
10
|
+
export interface ModelPreferences {
|
|
11
|
+
/** Patterns to sort first, in declaration order. */
|
|
12
|
+
prefer: string[];
|
|
13
|
+
/** Patterns to omit from the listing. */
|
|
14
|
+
hide: string[];
|
|
15
|
+
/** Suggested model, surfaced but never applied as a substitute. */
|
|
16
|
+
default?: string;
|
|
17
|
+
/** Set when the file existed but could not be used, so the failure is reportable, not silent. */
|
|
18
|
+
error?: string;
|
|
19
|
+
/** The path that was read, or would be. */
|
|
20
|
+
path: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const MODEL_CONFIG_FILENAME = "subagent-models.json";
|
|
24
|
+
|
|
25
|
+
export const NO_PREFERENCES = (path: string): ModelPreferences => ({ prefer: [], hide: [], path });
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Match a model reference against a config pattern. `provider/id` is matched as a whole; a bare
|
|
29
|
+
* provider also matches everything from that provider, so `"openai-codex"` is shorthand for
|
|
30
|
+
* `"openai-codex/*"`. `*` matches any run of characters, other metacharacters are literal, and
|
|
31
|
+
* matching is case-insensitive.
|
|
32
|
+
*/
|
|
33
|
+
export function matchesPattern(reference: string, pattern: string): boolean {
|
|
34
|
+
const ref = reference.toLowerCase();
|
|
35
|
+
const pat = pattern.trim().toLowerCase();
|
|
36
|
+
if (!pat) return false;
|
|
37
|
+
if (!pat.includes("*")) return ref === pat || ref.startsWith(`${pat}/`);
|
|
38
|
+
const escaped = pat.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
39
|
+
return new RegExp(`^${escaped}$`).test(ref);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function asStringArray(value: unknown, field: string, errors: string[]): string[] {
|
|
43
|
+
if (value === undefined) return [];
|
|
44
|
+
if (!Array.isArray(value)) {
|
|
45
|
+
errors.push(`${field} must be an array of strings`);
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const out: string[] = [];
|
|
49
|
+
for (const item of value) {
|
|
50
|
+
if (typeof item !== "string" || !item.trim()) errors.push(`${field} contains a non-string entry`);
|
|
51
|
+
else out.push(item.trim());
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Parse config text. Exported so the validation rules are testable without touching the filesystem. */
|
|
57
|
+
export function parsePreferences(text: string, path = MODEL_CONFIG_FILENAME): ModelPreferences {
|
|
58
|
+
let raw: unknown;
|
|
59
|
+
try {
|
|
60
|
+
raw = JSON.parse(text);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return {
|
|
63
|
+
prefer: [],
|
|
64
|
+
hide: [],
|
|
65
|
+
path,
|
|
66
|
+
error: `not valid JSON (${err instanceof Error ? err.message : String(err)})`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
70
|
+
return { prefer: [], hide: [], path, error: "must be a JSON object" };
|
|
71
|
+
}
|
|
72
|
+
const obj = raw as Record<string, unknown>;
|
|
73
|
+
const errors: string[] = [];
|
|
74
|
+
const known = new Set(["prefer", "hide", "default"]);
|
|
75
|
+
const unknown = Object.keys(obj).filter((k) => !known.has(k));
|
|
76
|
+
if (unknown.length > 0) errors.push(`unknown key(s): ${unknown.join(", ")}`);
|
|
77
|
+
|
|
78
|
+
const prefer = asStringArray(obj.prefer, "prefer", errors);
|
|
79
|
+
const hide = asStringArray(obj.hide, "hide", errors);
|
|
80
|
+
let fallback: string | undefined;
|
|
81
|
+
if (obj.default !== undefined) {
|
|
82
|
+
if (typeof obj.default !== "string" || !obj.default.trim()) errors.push("default must be a non-empty string");
|
|
83
|
+
else fallback = obj.default.trim();
|
|
84
|
+
}
|
|
85
|
+
if (errors.length > 0) return { prefer: [], hide: [], path, error: errors.join("; ") };
|
|
86
|
+
return { prefer, hide, ...(fallback ? { default: fallback } : {}), path };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Read the preferences file if present. Never throws: an unusable file degrades to no preferences. */
|
|
90
|
+
export function loadPreferences(agentDir: string): ModelPreferences {
|
|
91
|
+
const path = join(agentDir, MODEL_CONFIG_FILENAME);
|
|
92
|
+
if (!existsSync(path)) return NO_PREFERENCES(path);
|
|
93
|
+
try {
|
|
94
|
+
return parsePreferences(readFileSync(path, "utf8"), path);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return {
|
|
97
|
+
prefer: [],
|
|
98
|
+
hide: [],
|
|
99
|
+
path,
|
|
100
|
+
error: `could not be read (${err instanceof Error ? err.message : String(err)})`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Hide first, then order: preferred matches by declaration order, the rest after, stable. Also
|
|
107
|
+
* reports patterns that matched nothing — an inert `hide` entry must not read as though it worked.
|
|
108
|
+
*/
|
|
109
|
+
export function applyPreferences<T extends { reference: string }>(
|
|
110
|
+
entries: T[],
|
|
111
|
+
prefs: ModelPreferences,
|
|
112
|
+
): { entries: T[]; unusedPatterns: string[] } {
|
|
113
|
+
if (prefs.error) return { entries: [...entries], unusedPatterns: [] };
|
|
114
|
+
const used = new Set<string>();
|
|
115
|
+
const kept = entries.filter((entry) => {
|
|
116
|
+
const hit = prefs.hide.find((pattern) => matchesPattern(entry.reference, pattern));
|
|
117
|
+
if (hit) used.add(hit);
|
|
118
|
+
return !hit;
|
|
119
|
+
});
|
|
120
|
+
const unusedPatterns = prefs.hide.filter((pattern) => !used.has(pattern));
|
|
121
|
+
if (prefs.prefer.length === 0) return { entries: kept, unusedPatterns };
|
|
122
|
+
|
|
123
|
+
const ranked = kept.map((entry, index) => {
|
|
124
|
+
const pattern = prefs.prefer.find((candidate) => matchesPattern(entry.reference, candidate));
|
|
125
|
+
if (pattern) used.add(pattern);
|
|
126
|
+
return { entry, index, rank: pattern ? prefs.prefer.indexOf(pattern) : Number.MAX_SAFE_INTEGER };
|
|
127
|
+
});
|
|
128
|
+
for (const pattern of prefs.prefer) if (!used.has(pattern)) unusedPatterns.push(pattern);
|
|
129
|
+
ranked.sort((a, b) => a.rank - b.rank || a.index - b.index);
|
|
130
|
+
return { entries: ranked.map((r) => r.entry), unusedPatterns };
|
|
131
|
+
}
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { type Api, getSupportedThinkingLevels, type Model, type ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type ExtensionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { applyPreferences, loadPreferences, type ModelPreferences } from "./modelconfig.ts";
|
|
4
|
+
import type { ModelCatalog, ModelPricing, SelectableModel } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a task's `model` reference against the registry. An absent reference inherits the
|
|
8
|
+
* session model — delegation stays usable without naming one; naming one is a pin, not a
|
|
9
|
+
* requirement.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveChildModel(ctx: ExtensionContext, explicit: string | undefined) {
|
|
12
|
+
if (!explicit?.trim()) return ctx.model;
|
|
13
|
+
const ref = explicit.trim();
|
|
14
|
+
if (!ctx.modelRegistry) return ctx.model;
|
|
15
|
+
const available = ctx.modelRegistry.getAvailable();
|
|
16
|
+
const sessionProvider = ctx.model?.provider;
|
|
17
|
+
if (sessionProvider && !ref.includes("/")) {
|
|
18
|
+
const own = available.filter((m) => m.provider === sessionProvider);
|
|
19
|
+
const hit = own.find((m) => m.id === ref) ?? own.find((m) => m.id.endsWith(`/${ref}`));
|
|
20
|
+
if (hit) return hit;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const byId = available.find((m) => m.id === ref);
|
|
24
|
+
if (byId) return byId;
|
|
25
|
+
for (let slash = ref.indexOf("/"); slash > 0; slash = ref.indexOf("/", slash + 1)) {
|
|
26
|
+
const model = ctx.modelRegistry.find(ref.slice(0, slash), ref.slice(slash + 1));
|
|
27
|
+
if (model) return model;
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Model not found: ${ref}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ModelChoice {
|
|
33
|
+
/** The reference to resolve, or undefined when the caller named none (inherit the session model). */
|
|
34
|
+
requested: string | undefined;
|
|
35
|
+
/** Set when an agent file supplied the model, so errors can say where it came from. */
|
|
36
|
+
sourceFile?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Single owner of the model-precedence rule: a matched agent file's `model` wins over the inline
|
|
41
|
+
* one. Spawn, pre-creation validation, and resume all resolve through this, so they cannot disagree
|
|
42
|
+
* about which model a task actually uses.
|
|
43
|
+
*/
|
|
44
|
+
export function chooseModel(
|
|
45
|
+
file: { model?: string; path?: string } | undefined,
|
|
46
|
+
inline: string | undefined,
|
|
47
|
+
): ModelChoice {
|
|
48
|
+
const fromFile = file?.model?.trim();
|
|
49
|
+
return fromFile ? { requested: fromFile, sourceFile: file?.path } : { requested: inline };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The thinking levels a model honors at runtime, from pi's own resolver, so the catalog cannot
|
|
54
|
+
* promise a level the runtime would silently clamp: `xhigh`/`max` count only when explicitly
|
|
55
|
+
* mapped. An accepted input can still be clamped; the catalog lists only honored levels.
|
|
56
|
+
*/
|
|
57
|
+
export function supportedThinkingLevels(model: Model<Api> | undefined): ModelThinkingLevel[] {
|
|
58
|
+
if (!model) return [];
|
|
59
|
+
return [...getSupportedThinkingLevels(model)];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Keep only finite, non-negative per-Mtok rates; a partial or absent cost is unavailable, not free. */
|
|
63
|
+
export function normalizeCost(cost: unknown): ModelPricing | undefined {
|
|
64
|
+
if (!cost || typeof cost !== "object") return undefined;
|
|
65
|
+
const raw = cost as Partial<Record<keyof ModelPricing, unknown>>;
|
|
66
|
+
const rate = (value: unknown): number | undefined =>
|
|
67
|
+
typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
68
|
+
const input = rate(raw.input);
|
|
69
|
+
const output = rate(raw.output);
|
|
70
|
+
if (input === undefined || output === undefined) return undefined;
|
|
71
|
+
const cacheRead = rate(raw.cacheRead);
|
|
72
|
+
const cacheWrite = rate(raw.cacheWrite);
|
|
73
|
+
return {
|
|
74
|
+
input,
|
|
75
|
+
output,
|
|
76
|
+
...(cacheRead !== undefined ? { cacheRead } : {}),
|
|
77
|
+
...(cacheWrite !== undefined ? { cacheWrite } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Models a subagent task may name, scoped the way pi scopes them.
|
|
83
|
+
*
|
|
84
|
+
* `ctx.scopedModels` is pi's own resolution of `enabledModels` and `--models` — the set
|
|
85
|
+
* `/scoped-models` shows. When no scoping is configured pi reports an empty list, and only then
|
|
86
|
+
* does this fall back to the full available catalogue; `scope` tells the caller which case applies.
|
|
87
|
+
*
|
|
88
|
+
* A reference is listed only when it resolves back to the model it describes. Resolution is not a
|
|
89
|
+
* pure `provider/id` split (a model whose bare id contains a slash can shadow another provider's
|
|
90
|
+
* `provider/id`), so building the string naively could advertise a reference that silently selects
|
|
91
|
+
* a different model. Anything ambiguous is reported as such, and a registry fault is reported
|
|
92
|
+
* separately because the caller's fix differs.
|
|
93
|
+
*/
|
|
94
|
+
export function listSelectableModels(ctx: ExtensionContext, preferences?: ModelPreferences): ModelCatalog {
|
|
95
|
+
if (!ctx.modelRegistry) {
|
|
96
|
+
return { models: [], scope: "all", unavailable: "this context exposes no model registry" };
|
|
97
|
+
}
|
|
98
|
+
const scoped = ctx.scopedModels ?? [];
|
|
99
|
+
const scope: ModelCatalog["scope"] = scoped.length > 0 ? "session" : "all";
|
|
100
|
+
|
|
101
|
+
let candidate: Model<Api>[];
|
|
102
|
+
try {
|
|
103
|
+
candidate = scoped.length > 0 ? scoped.map((entry) => entry.model) : (ctx.modelRegistry.getAvailable() ?? []);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
return { models: [], scope, unavailable: err instanceof Error ? err.message : String(err) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const seen = new Set<string>();
|
|
109
|
+
const models: SelectableModel[] = [];
|
|
110
|
+
const ambiguous: string[] = [];
|
|
111
|
+
const unresolved: string[] = [];
|
|
112
|
+
for (const model of candidate) {
|
|
113
|
+
const reference = `${model.provider}/${model.id}`;
|
|
114
|
+
if (seen.has(reference)) continue;
|
|
115
|
+
seen.add(reference);
|
|
116
|
+
|
|
117
|
+
let resolved: Model<Api> | undefined;
|
|
118
|
+
let lookupError: string | undefined;
|
|
119
|
+
try {
|
|
120
|
+
resolved = resolveChildModel(ctx, reference);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
lookupError = err instanceof Error ? err.message : String(err);
|
|
123
|
+
}
|
|
124
|
+
if (!resolved) {
|
|
125
|
+
unresolved.push(lookupError ? `${reference} (${lookupError})` : reference);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (resolved.provider !== model.provider || resolved.id !== model.id) {
|
|
129
|
+
ambiguous.push(reference);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
models.push({
|
|
133
|
+
reference,
|
|
134
|
+
provider: model.provider,
|
|
135
|
+
id: model.id,
|
|
136
|
+
name: typeof model.name === "string" && model.name.trim() ? model.name : model.id,
|
|
137
|
+
reasoning: Boolean(model.reasoning),
|
|
138
|
+
thinkingLevels: supportedThinkingLevels(model),
|
|
139
|
+
contextWindow:
|
|
140
|
+
typeof model.contextWindow === "number" && Number.isFinite(model.contextWindow) && model.contextWindow > 0
|
|
141
|
+
? model.contextWindow
|
|
142
|
+
: 0,
|
|
143
|
+
cost: normalizeCost((model as { cost?: unknown }).cost),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Preferences filter and order only. `preferences` is injectable so the file-to-catalog wiring
|
|
148
|
+
// is testable without touching the user's real config.
|
|
149
|
+
const prefs = preferences ?? loadPreferences(getAgentDir());
|
|
150
|
+
const { entries: shown, unusedPatterns } = applyPreferences(models, prefs);
|
|
151
|
+
const hiddenCount = models.length - shown.length;
|
|
152
|
+
const suggested = prefs.error ? undefined : prefs.default;
|
|
153
|
+
const listedDefault = suggested && shown.some((entry) => entry.reference === suggested);
|
|
154
|
+
return {
|
|
155
|
+
models: shown,
|
|
156
|
+
scope,
|
|
157
|
+
...(listedDefault ? { preferredDefault: suggested } : {}),
|
|
158
|
+
...(suggested && !listedDefault ? { unlistedDefault: suggested } : {}),
|
|
159
|
+
...(prefs.error ? { configError: `${prefs.path}: ${prefs.error}` } : {}),
|
|
160
|
+
...(hiddenCount > 0 ? { hidden: hiddenCount } : {}),
|
|
161
|
+
...(unusedPatterns.length > 0 ? { unusedPatterns } : {}),
|
|
162
|
+
...(ambiguous.length > 0
|
|
163
|
+
? {
|
|
164
|
+
ambiguous,
|
|
165
|
+
reason: `another model's bare id would win resolution for ${ambiguous.join(", ")}`,
|
|
166
|
+
}
|
|
167
|
+
: {}),
|
|
168
|
+
...(unresolved.length > 0
|
|
169
|
+
? { unresolved, unresolvedReason: `the registry could not resolve these entries: ${unresolved.join(", ")}` }
|
|
170
|
+
: {}),
|
|
171
|
+
...(shown.length === 0
|
|
172
|
+
? {
|
|
173
|
+
unavailable:
|
|
174
|
+
models.length > 0
|
|
175
|
+
? `every listed model is hidden by ${prefs.path} — unhide one or remove \`hide\``
|
|
176
|
+
: unresolved.length > 0
|
|
177
|
+
? `the registry could not resolve any of its available models: ${unresolved.join("; ")}`
|
|
178
|
+
: scope === "session"
|
|
179
|
+
? "the session's enabled models could not be resolved"
|
|
180
|
+
: "no model has usable credentials",
|
|
181
|
+
}
|
|
182
|
+
: {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
package/src/schemas.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
1
|
+
import { type ModelThinkingLevel, StringEnum } from "@earendil-works/pi-ai";
|
|
2
2
|
import { type Static, Type } from "typebox";
|
|
3
3
|
import { DEFAULT_CONCURRENCY, MAX_CONCURRENCY } from "./manager.ts";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** Schema vocabulary; per-model supported levels come from pi-ai. */
|
|
6
|
+
const THINKING_LEVELS = [
|
|
7
|
+
"off",
|
|
8
|
+
"minimal",
|
|
9
|
+
"low",
|
|
10
|
+
"medium",
|
|
11
|
+
"high",
|
|
12
|
+
"xhigh",
|
|
13
|
+
"max",
|
|
14
|
+
] as const satisfies readonly ModelThinkingLevel[];
|
|
6
15
|
const TaskItem = Type.Object({
|
|
7
16
|
id: Type.Optional(Type.String({ description: "Optional stable task id" })),
|
|
8
17
|
agent: Type.String({ minLength: 1, description: "Agent name you invent (defined inline via `prompt`)" }),
|
|
@@ -13,7 +22,12 @@ const TaskItem = Type.Object({
|
|
|
13
22
|
description: "true = write toolset (adds bash, edit, write); default false = read-only (read, grep, find, ls)",
|
|
14
23
|
}),
|
|
15
24
|
),
|
|
16
|
-
model: Type.Optional(
|
|
25
|
+
model: Type.Optional(
|
|
26
|
+
Type.String({
|
|
27
|
+
description:
|
|
28
|
+
"Model override (provider/model-id). Omit to inherit the session model; call subagent_models for the exact references this session accepts.",
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
17
31
|
thinking: Type.Optional(StringEnum(THINKING_LEVELS, { description: "Thinking level override" })),
|
|
18
32
|
cwd: Type.Optional(Type.String({ description: "Working directory (default: current project)" })),
|
|
19
33
|
tools: Type.Optional(Type.Array(Type.String(), { description: "Explicit tool allowlist (overrides the toolset)" })),
|
|
@@ -35,7 +49,12 @@ export const SubagentParams = Type.Object({
|
|
|
35
49
|
),
|
|
36
50
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "Parallel tasks" })),
|
|
37
51
|
chain: Type.Optional(Type.Array(TaskItem, { description: "Sequential tasks; {previous} = prior output" })),
|
|
38
|
-
model: Type.Optional(
|
|
52
|
+
model: Type.Optional(
|
|
53
|
+
Type.String({
|
|
54
|
+
description:
|
|
55
|
+
"Model override (single mode). Omit to inherit the session model; call subagent_models for the exact references this session accepts.",
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
39
58
|
thinking: Type.Optional(StringEnum(THINKING_LEVELS, { description: "Thinking level override (single mode)" })),
|
|
40
59
|
cwd: Type.Optional(Type.String({ description: "Working directory (single mode). Default: current project." })),
|
|
41
60
|
concurrency: Type.Optional(
|
|
@@ -64,6 +83,7 @@ export type TaskInput = Static<typeof TaskItem>;
|
|
|
64
83
|
export type SubagentParamsShape = Static<typeof SubagentParams>;
|
|
65
84
|
|
|
66
85
|
export const RunIdParam = Type.Object({ runId: Type.String({ description: "Run id from subagent()" }) });
|
|
86
|
+
export const ModelsParam = Type.Object({});
|
|
67
87
|
export const ResultParam = Type.Object({
|
|
68
88
|
runId: Type.String(),
|
|
69
89
|
taskId: Type.Optional(Type.String({ description: "Specific task id; defaults to all" })),
|
package/src/types.ts
CHANGED
|
@@ -71,3 +71,55 @@ export interface RunDetails {
|
|
|
71
71
|
export interface PendingReply {
|
|
72
72
|
resolve: (message: string) => void;
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
/** Per-million-token USD rates, as pi reports them. */
|
|
76
|
+
export interface ModelPricing {
|
|
77
|
+
input: number;
|
|
78
|
+
output: number;
|
|
79
|
+
cacheRead?: number;
|
|
80
|
+
cacheWrite?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One selectable model, as the agent needs it to choose: what to pass, what it supports, what it costs. */
|
|
84
|
+
export interface SelectableModel {
|
|
85
|
+
/** The value to pass as `model` ("provider/id"). */
|
|
86
|
+
reference: string;
|
|
87
|
+
provider: string;
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
reasoning: boolean;
|
|
91
|
+
/** Levels the runtime honors without clamping. */
|
|
92
|
+
thinkingLevels: string[];
|
|
93
|
+
/** 0 when the provider did not report one. */
|
|
94
|
+
contextWindow: number;
|
|
95
|
+
/** pi's catalog rates, or undefined when the provider reported none — distinct from free. */
|
|
96
|
+
cost?: ModelPricing;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ModelCatalog {
|
|
100
|
+
models: SelectableModel[];
|
|
101
|
+
/**
|
|
102
|
+
* What the list was scoped to. pi resolves `enabledModels` (and `--models`) into scoped models,
|
|
103
|
+
* so this is normally the session's own enabled set rather than every model with credentials.
|
|
104
|
+
*/
|
|
105
|
+
scope: "session" | "all";
|
|
106
|
+
/** The config's suggested model, surfaced for the caller to weigh. Never applied automatically. */
|
|
107
|
+
preferredDefault?: string;
|
|
108
|
+
unlistedDefault?: string;
|
|
109
|
+
/** Set when the preferences file existed but was unusable, so a typo is reported, not silent. */
|
|
110
|
+
configError?: string;
|
|
111
|
+
/** How many models the config hid, so a surprising absence is explained. */
|
|
112
|
+
hidden?: number;
|
|
113
|
+
/** Preferred/hidden patterns that matched no listed model — inert config, reported not hidden. */
|
|
114
|
+
unusedPatterns?: string[];
|
|
115
|
+
/** References that are NOT safe to pass because another model's bare id would win resolution. */
|
|
116
|
+
ambiguous?: string[];
|
|
117
|
+
/** Why the ambiguous references are unsafe, in full, so the agent can act instead of retrying blindly. */
|
|
118
|
+
reason?: string;
|
|
119
|
+
/** Entries the registry itself could not resolve — a registry fault, distinct from a name collision. */
|
|
120
|
+
unresolved?: string[];
|
|
121
|
+
/** Why those entries failed, so a registry fault is never mistaken for a collision. */
|
|
122
|
+
unresolvedReason?: string;
|
|
123
|
+
/** Set when the catalog is empty, so the caller knows it is not looking at a legitimate empty list. */
|
|
124
|
+
unavailable?: string;
|
|
125
|
+
}
|