@diousk/pi-subagents-fast 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.23.0] - 2026-10-02
11
+
12
+ ### Changed
13
+ - **BREAKING: Jev model profiles require `thinkingLevel`.** Add `minimal`, `low`, `medium`, `high`, `xhigh` or `max` to every `jev.models` entry. Successful routing applies the model and thinking level together; shadow and fallback preserve the original pair. The routing menu requires a level, and invalid or missing levels disable the Jev block with a diagnostic.
14
+
10
15
  ## [0.22.0] - 2026-10-02
11
16
 
12
17
  > **Breaking: requires Pi 1.0.0 or newer.** Update Pi before using this version.
package/README.md CHANGED
@@ -634,7 +634,7 @@ When background agents complete, they notify the main agent. The **join mode** c
634
634
 
635
635
  ## Model routing
636
636
 
637
- Open `/agents → Model routing` to choose a mode and configure a guideline path, models with descriptions, or a masked TypeSafe API key. The menu shows the active mode and source and saves project settings. For machine-wide defaults, edit `~/.pi/agent/subagents.json`; project overrides go in `.pi/subagents.json`. `PI_CODING_AGENT_DIR` changes the global directory along with Pi's other configuration.
637
+ Open `/agents → Model routing` to choose a mode and configure a guideline path, models with descriptions and required thinking levels, or a masked TypeSafe API key. The menu shows the active mode and source and saves project settings. For machine-wide defaults, edit `~/.pi/agent/subagents.json`; project overrides go in `.pi/subagents.json`. `PI_CODING_AGENT_DIR` changes the global directory along with Pi's other configuration.
638
638
 
639
639
  Set one top-level field to control routing; omitting it means `auto`:
640
640
 
@@ -660,7 +660,7 @@ The default priority is:
660
660
  | 3 | `jev` is configured | Jev chooses a model from your descriptions for a fresh delegated task |
661
661
  | 4 | None of the above | The existing model is used |
662
662
 
663
- **Custom agents:** keep using `~/.pi/agent/agents/<name>.md`, `.pi/agents/<name>.md` or `.agents/agents/<name>.md`. No routing setting is needed. Built-in and disabled agents do not activate priority 1. Agent-file model/thinking pins supply the default choice over `Agent` parameters; a confident Jev choice in `jev` mode can replace the model.
663
+ **Custom agents:** keep using `~/.pi/agent/agents/<name>.md`, `.pi/agents/<name>.md` or `.agents/agents/<name>.md`. No routing setting is needed. Built-in and disabled agents do not activate priority 1. Agent-file model/thinking pins supply the default choice over `Agent` parameters; a confident Jev choice in `jev` mode can replace both model and thinking level.
664
664
 
665
665
  **Custom guideline:** write your routing rules in `~/.pi/agent/agents/custom-route.md`, then configure:
666
666
 
@@ -679,16 +679,16 @@ For example, the Markdown can say “Use anthropic/claude-haiku-4-5 for simple e
679
679
  "jev": {
680
680
  "TYPESAFE_API_KEY": "your-typesafe-key",
681
681
  "models": [
682
- { "model": "anthropic/claude-haiku-4-5", "description": "Simple edits, lookup and concise summaries" },
683
- { "model": "openai-codex/gpt-6.1-sol", "description": "Complex debugging, architecture and concurrency" }
682
+ { "model": "anthropic/claude-haiku-4-5", "description": "Simple edits, lookup and concise summaries", "thinkingLevel": "low" },
683
+ { "model": "openai-codex/gpt-6.1-sol", "description": "Complex debugging, architecture and concurrency", "thinkingLevel": "high" }
684
684
  ]
685
685
  }
686
686
  }
687
687
  ```
688
688
 
689
- `TYPESAFE_API_KEY` is optional when Pi already has TypeSafe credentials or the environment variable is set. A literal key must be a nonempty token without whitespace or control characters and applies only to that classifier request; it is saved in the settings file, masked in the menu and omitted from settings events, prompts and routing records. Without a literal key, Pi's native credential availability check runs before classification. Rejected credentials fall back without retrying. The extension does not change environment variables or register a routing provider. `models` accepts 1–254 unique entries with descriptions of 1–4000 characters. Jev entries use exact `provider/model-id` spelling; fuzzy names remain available for explicit `Agent` parameters.
689
+ `TYPESAFE_API_KEY` is optional when Pi already has TypeSafe credentials or the environment variable is set. A literal key must be a nonempty token without whitespace or control characters and applies only to that classifier request; it is saved in the settings file, masked in the menu and omitted from settings events, prompts and routing records. Without a literal key, Pi's native credential availability check runs before classification. Rejected credentials fall back without retrying. The extension does not change environment variables or register a routing provider. `models` accepts 1–254 unique entries with descriptions of 1–4000 characters. Every entry requires `thinkingLevel`: `minimal`, `low`, `medium`, `high`, `xhigh` or `max`. Existing profiles must add this field; a missing or invalid value disables the whole Jev block and uses the default-priority fallback. Jev entries use exact `provider/model-id` spelling; fuzzy names remain available for explicit `Agent` parameters.
690
690
 
691
- Under `auto`, supplying **either** `model` or `thinking` explicitly skips Jev. An agent-file pin also skips it. In workflows, either `model` or `effort` skips it, and workflow options retain their precedence over agent-file defaults. Inherited models remain eligible for Jev. Under `jev`, these choices supply the fallback model, but do not skip classification. Under `shadow`, they remain the actual choice while Jev records a comparison. Jev changes only the model; Pi still determines the effective thinking level.
691
+ Under `auto`, supplying **either** `model` or `thinking` explicitly skips Jev. An agent-file pin also skips it. In workflows, either `model` or `effort` skips it, and workflow options retain their precedence over agent-file defaults. Inherited models remain eligible for Jev. Under `jev`, these choices supply the fallback model, but do not skip classification. Under `shadow`, they remain the actual choice while Jev records a comparison. A selected Jev profile applies both its model and `thinkingLevel`. Pi clamps the requested thinking level to what that model supports. Shadow and fallback preserve the original model and thinking level.
692
692
 
693
693
  Automatic choices are limited to authenticated models and the current nonempty Pi model scope, regardless of the `scopeModels` setting. When `scopeModels` is enabled, Jev candidates also respect `enabledModels`. Unavailable candidates are excluded and the selected model is checked again after classification, including any changed scope. Jev failure, confidence below 0.6 or a two-second timeout keeps the default-priority model already selected by the main agent, explicit caller or agent definition, otherwise the existing model. This fallback does not make a second Jev request. Cancellation stops startup without launching a fallback. Queued agents read the current mode and classify after dequeue; nested calls share a separate classifier concurrency limit of four and do not take another agent slot.
694
694
 
@@ -696,7 +696,7 @@ Jev applies to fresh tool, nested, workflow, scheduled, RPC and direct-mention s
696
696
 
697
697
  Project `routingMode` and `customGuideline` replace the global values; omission inherits them, and `customGuideline: false` disables the guideline. A project `jev` block replaces the **whole** global block, including credentials; omitted fields inside that block do not inherit. `"jev": false` disables global Jev. An invalid mode or malformed settings file disables routing. An invalid explicit Jev block disables that block instead of restoring global paid routing. Other settings changes preserve these inheritance rules.
698
698
 
699
- Classifier usage is recorded separately as `routingUsage`, including in `shadow`; it does not consume coding context or workflow output budgets. Its reported cost is added once to the agent and ancestor cost totals, and `reportUsage` can return its token usage to the parent. `routing` records the mode, source, reason, applied `model` and confidence; shadow records `suggestedModel` and leaves `model` unset. `fallbackSource` identifies the default source when a suggestion is observed or Jev falls back. Shadow results show “Jev shadow”. Zero catalog prices are marked `unpriced`; the Agent result shows “Jev price unavailable” instead of treating that as proof the classifier is free.
699
+ Classifier usage is recorded separately as `routingUsage`, including in `shadow`; it does not consume coding context or workflow output budgets. Its reported cost is added once to the agent and ancestor cost totals, and `reportUsage` can return its token usage to the parent. `routing` records the mode, source, reason, applied `model`, requested `thinkingLevel` and confidence; shadow records `suggestedModel` and `suggestedThinkingLevel` and leaves both applied fields unset. `fallbackSource` identifies the default source when a suggestion is observed or Jev falls back. Shadow results show “Jev shadow”. Zero catalog prices are marked `unpriced`; the Agent result shows “Jev price unavailable” instead of treating that as proof the classifier is free.
700
700
 
701
701
  ## Model Scope
702
702
 
@@ -731,7 +731,7 @@ Runtime tuning values set via `/agents` → Settings (max concurrency, max foreg
731
731
 
732
732
  **Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `10`, max foreground concurrency `0` = unlimited, default max turns unlimited, grace turns `5`, nested depth `2`, join mode `smart`, defaults enabled).
733
733
 
734
- Routing settings: `routingMode` (`auto | shadow | jev | off`, default `auto`), `customGuideline` (`string | false`, default unset) and `jev` (`{ TYPESAFE_API_KEY?: string, models: { model, description }[] } | false`, default unset). See [Model routing](#model-routing) for examples and the whole-block override rule.
734
+ Routing settings: `routingMode` (`auto | shadow | jev | off`, default `auto`), `customGuideline` (`string | false`, default unset) and `jev` (`{ TYPESAFE_API_KEY?: string, models: { model, description, thinkingLevel }[] } | false`, default unset). See [Model routing](#model-routing) for examples and the whole-block override rule.
735
735
 
736
736
  **Nested depth** (`maxSubagentDepth`, default `2`): the hard ceiling on [nested delegation](#nested-subagents), counted from the main session (main = 0, its subagents = 1). `0` or `1` disables nesting project-wide regardless of any agent's `allowed_subagents`. Read when a subagent session is built, so a change applies to agents started after it.
737
737
 
@@ -861,7 +861,7 @@ The four agent-lifecycle events — `subagents:started`, `:completed`, `:failed`
861
861
 
862
862
  `usage` answers the other question — what was billed — and so does include `cacheRead`, because the prefix really is re-read and re-charged on every call. It is a pi `Usage`, the same shape pi puts on `ToolResultEvent` and `AssistantMessage`, so `usage.cost.total` is where a listener already expects the money and anything pi adds to `Usage` arrives without a change here. Neither field derives from the other; `tokens` is a view model, `usage` is the data.
863
863
 
864
- Completed/failed payloads and persisted `subagents:record` entries also carry `routing` (`source`, `code`, `reason`, optional `model`, supplied `description`, `confidence`, `unpriced`, `guidelinePath`, `guidelineHash`) and optional `routingUsage` (classifier-only Pi `Usage`). The guideline hash is SHA-256 of the original file contents. Coding `tokens` excludes classifier tokens; total cost includes the reported classifier cost once. Settings events omit `jev.TYPESAFE_API_KEY`.
864
+ Completed/failed payloads and persisted `subagents:record` entries also carry `routing` (`source`, `code`, `reason`, optional `model`, `thinkingLevel`, `suggestedModel`, `suggestedThinkingLevel`, supplied `description`, `confidence`, `unpriced`, `guidelinePath`, `guidelineHash`) and optional `routingUsage` (classifier-only Pi `Usage`). The guideline hash is SHA-256 of the original file contents. Coding `tokens` excludes classifier tokens; total cost includes the reported classifier cost once. Settings events omit `jev.TYPESAFE_API_KEY`.
865
865
 
866
866
  ## Cross-Extension RPC
867
867
 
@@ -1080,7 +1080,7 @@ src/
1080
1080
  invocation-config.ts # Shared tool-parameter schemas (isolation, join, thinking, ...)
1081
1081
  model-resolver.ts # Model resolution: exact provider/modelId with fuzzy fallback
1082
1082
  model-routing.ts # Source priority and bounded native Jev classification before startup
1083
- routing-config.ts # Minimal Jev model/description and credential config validation
1083
+ routing-config.ts # Jev model/description/thinking-level and credential config validation
1084
1084
  enabled-models.ts # Read pi's enabledModels settings (project over global)
1085
1085
  model-scope.ts # scopeModels allowlist policy, shared by top-level and nested tools
1086
1086
  mention.ts # `@handle message` grammar: suggestion triggers and send parsing
@@ -504,11 +504,13 @@ export class AgentManager {
504
504
  });
505
505
  record.routing = { ...routed.decision, guidelinePath: policy.guidelinePath, guidelineHash: policy.guidelineHash };
506
506
  if (routed.model && policy.mode === "shadow") {
507
- record.routing = { ...record.routing, code: "shadow", model: undefined, suggestedModel: routed.decision.model,
507
+ record.routing = { ...record.routing, code: "shadow", model: undefined, thinkingLevel: undefined, suggestedModel: routed.decision.model,
508
508
  fallbackSource: policy.source, reason: "Jev suggested a model; shadow mode kept the default-priority model" };
509
509
  }
510
510
  else if (routed.model) {
511
511
  options.model = routed.model;
512
+ options.thinkingLevel = routed.thinkingLevel;
513
+ record.invocation = { ...record.invocation, thinking: routed.thinkingLevel, requestedThinking: undefined };
512
514
  }
513
515
  }
514
516
  catch {
@@ -1,4 +1,4 @@
1
- import type { Api, Model, Usage } from "@earendil-works/pi-ai";
1
+ import type { Api, Model, ThinkingLevel, Usage } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import type { JevConfig, RoutingMode } from "./routing-config.js";
4
4
  import type { AgentConfig } from "./types.js";
@@ -33,6 +33,8 @@ export interface RoutingDecision {
33
33
  code: "baseline" | "explicit" | "off" | "shadow" | "config_unavailable" | "credentials_unavailable" | "guideline_unavailable" | "no_candidates" | "classifier_unavailable" | "cancelled" | "timeout" | "invalid_answer" | "abstained" | "unavailable_choice" | "selected" | "classifier_error";
34
34
  model?: string;
35
35
  suggestedModel?: string;
36
+ thinkingLevel?: ThinkingLevel;
37
+ suggestedThinkingLevel?: ThinkingLevel;
36
38
  description?: string;
37
39
  confidence?: number;
38
40
  unpriced?: boolean;
@@ -49,6 +51,7 @@ export declare class ModelRouter {
49
51
  private acquire;
50
52
  choose(ctx: ExtensionContext, policy: RoutingPolicy, prompt: string, description: string, baseline: Model<Api> | undefined, signal: AbortSignal, onUsage: (usage: Usage) => void): Promise<{
51
53
  model?: Model<Api>;
54
+ thinkingLevel?: ThinkingLevel;
52
55
  decision: RoutingDecision;
53
56
  }>;
54
57
  }
@@ -59,10 +59,10 @@ export function routingGuidance(policy) {
59
59
  ? `Routing source: Custom guideline (${policy.guidelinePath}). Follow it when choosing Agent model/thinking (or workflow model/effort). Pass your default choice explicitly.\n<custom_routing_guideline>\n${policy.guideline}\n</custom_routing_guideline>`
60
60
  : policy.diagnostic ?? "Custom routing guideline is unavailable. Use the existing model.";
61
61
  break;
62
- case "jev": guidance = "Routing: Jev chooses the model for fresh agents when neither model nor thinking is explicitly set. Omit both to use automatic routing. Explicit choices and agent-file pins keep their existing precedence.";
62
+ case "jev": guidance = "Routing: Jev chooses the model and thinking level for fresh agents when neither model nor thinking is explicitly set. Omit both to use automatic routing. Explicit choices and agent-file pins keep their existing precedence.";
63
63
  }
64
64
  if (policy.mode === "jev")
65
- return "Routing mode: jev. Jev gets first choice of model for every fresh delegated task, including explicit model choices and agent-file model pins. Choose an agent and a default model using the guidance below; that choice is the fallback if Jev is unavailable or uncertain. Thinking keeps its existing precedence.\n" + guidance;
65
+ return "Routing mode: jev. Jev gets first choice of model for every fresh delegated task, including explicit model choices and agent-file model pins. Choose an agent and a default model using the guidance below; that choice is the fallback if Jev is unavailable or uncertain. The selected profile supplies both model and thinking level.\n" + guidance;
66
66
  if (policy.mode === "shadow")
67
67
  return "Routing mode: shadow. Jev records a suggestion for each fresh delegated task but never changes the model. Choose using the default guidance below, or keep the existing model when no guidance applies.\n" + guidance;
68
68
  return guidance && policy.source !== "jev" ? guidance + "\nJev is inactive under auto mode while this routing source applies." : guidance;
@@ -139,7 +139,7 @@ export class ModelRouter {
139
139
  release = await this.acquire(controller.signal);
140
140
  if (!release)
141
141
  return { decision: { ...decision, code: signal.aborted ? "cancelled" : "timeout", reason: signal.aborted ? "Cancelled" : "Jev timed out" } };
142
- const choices = new Map(candidates.map((entry, index) => [`route_${index}`, entry.model]));
142
+ const choices = new Map(candidates.map((entry, index) => [`route_${index}`, entry]));
143
143
  const criteria = { keep_baseline: "Keep the existing model if none of the described models clearly fits the task." };
144
144
  for (const [index, entry] of candidates.entries())
145
145
  criteria[`route_${index}`] = entry.description;
@@ -186,16 +186,19 @@ export class ModelRouter {
186
186
  return { decision: { ...decision, code: "invalid_answer", reason: "Jev returned no usable choice" } };
187
187
  }
188
188
  decision.confidence = answer.confidence;
189
- if (policy.mode === "shadow")
190
- decision.suggestedModel = choices.get(answer.choice);
189
+ const profile = choices.get(answer.choice);
190
+ if (policy.mode === "shadow") {
191
+ decision.suggestedModel = profile?.model;
192
+ decision.suggestedThinkingLevel = profile?.thinkingLevel;
193
+ }
191
194
  if (answer.choice === "keep_baseline" || answer.confidence < 0.6) {
192
195
  return { decision: { ...decision, code: "abstained", reason: "Jev kept the existing model" } };
193
196
  }
194
- const selected = choices.get(answer.choice);
197
+ const selected = profile?.model;
195
198
  const model = selected ? eligibleModels(ctx).get(selected) : undefined;
196
199
  if (!model || signal.aborted)
197
200
  return { decision: { ...decision, code: "unavailable_choice", reason: "Selected model is no longer available in scope" } };
198
- return { model, decision: { ...decision, fallbackSource: undefined, code: "selected", model: selected, description: candidates.find(entry => entry.model === selected)?.description, reason: "Jev selected a configured model" } };
201
+ return { model, thinkingLevel: profile?.thinkingLevel, decision: { ...decision, fallbackSource: undefined, code: "selected", model: selected, thinkingLevel: profile?.thinkingLevel, description: profile?.description, reason: "Jev selected a configured model profile" } };
199
202
  }
200
203
  catch {
201
204
  // Provider errors may contain credentials. Keep diagnostics code-owned.
@@ -1,9 +1,12 @@
1
+ import type { ThinkingLevel } from "@earendil-works/pi-ai";
2
+ export declare const ROUTING_THINKING_LEVELS: readonly ["minimal", "low", "medium", "high", "xhigh", "max"];
1
3
  export type RoutingMode = "auto" | "shadow" | "jev" | "off";
2
4
  export interface JevConfig {
3
5
  TYPESAFE_API_KEY?: string;
4
6
  models: {
5
7
  model: string;
6
8
  description: string;
9
+ thinkingLevel: ThinkingLevel;
7
10
  }[];
8
11
  }
9
12
  /** Validate the whole block; never merge candidate lists or credentials. */
@@ -1,3 +1,4 @@
1
+ export const ROUTING_THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"];
1
2
  /** Validate the whole block; never merge candidate lists or credentials. */
2
3
  export function parseJevConfig(raw) {
3
4
  if (raw === false)
@@ -20,15 +21,18 @@ export function parseJevConfig(raw) {
20
21
  if (!entry || typeof entry !== "object" || Array.isArray(entry))
21
22
  throw new Error("Each Jev model needs model and description");
22
23
  const candidate = entry;
23
- if (Object.keys(candidate).some(key => key !== "model" && key !== "description") ||
24
+ if (Object.keys(candidate).some(key => key !== "model" && key !== "description" && key !== "thinkingLevel") ||
24
25
  typeof candidate.model !== "string" || !/^[^\s/]+\/\S+$/.test(candidate.model) ||
25
26
  typeof candidate.description !== "string" || !candidate.description.trim() || candidate.description.length > 4000) {
26
27
  throw new Error("Each Jev model needs an exact provider/model-id and a description of 1–4000 characters");
27
28
  }
29
+ const thinkingLevel = ROUTING_THINKING_LEVELS.find(level => level === candidate.thinkingLevel);
30
+ if (!thinkingLevel)
31
+ throw new Error("Each Jev model requires thinkingLevel: minimal, low, medium, high, xhigh or max");
28
32
  if (seen.has(candidate.model))
29
33
  throw new Error("jev.models contains duplicate models");
30
34
  seen.add(candidate.model);
31
- return { model: candidate.model, description: candidate.description.trim() };
35
+ return { model: candidate.model, description: candidate.description.trim(), thinkingLevel };
32
36
  });
33
37
  return { models, ...(typeof value.TYPESAFE_API_KEY === "string" ? { TYPESAFE_API_KEY: value.TYPESAFE_API_KEY } : {}) };
34
38
  }
@@ -1,6 +1,6 @@
1
1
  import { Input, Text } from "@earendil-works/pi-tui";
2
2
  import { loadRoutingPolicy } from "../model-routing.js";
3
- import { parseJevConfig } from "../routing-config.js";
3
+ import { parseJevConfig, ROUTING_THINKING_LEVELS } from "../routing-config.js";
4
4
  import { loadSettings, projectRoutingSettings } from "../settings.js";
5
5
  /** Input handles paste/editing, but its unmasked renderer is never called. */
6
6
  export async function maskedApiKey(ctx) {
@@ -73,7 +73,7 @@ export async function showRoutingMenu(ctx) {
73
73
  }
74
74
  }
75
75
  for (;;) {
76
- const options = [...models.map((entry, index) => `${index + 1}. ${entry.model}`), "Add model", "Save", "Cancel"];
76
+ const options = [...models.map((entry, index) => `${index + 1}. ${entry.model} (${entry.thinkingLevel})`), "Add model", "Save", "Cancel"];
77
77
  const action = await ctx.ui.select("Jev models — describe which tasks each model should handle", options);
78
78
  if (!action || action === "Cancel")
79
79
  return;
@@ -103,7 +103,11 @@ export async function showRoutingMenu(ctx) {
103
103
  const description = await ctx.ui.editor("Tasks this model is suitable for", current?.description ?? "");
104
104
  if (!description?.trim())
105
105
  continue;
106
- const entry = { model: model.trim(), description: description.trim() };
106
+ const selectedLevel = await ctx.ui.select(`Thinking level (required${current ? `; current: ${current.thinkingLevel}` : ""})`, [...ROUTING_THINKING_LEVELS]);
107
+ const thinkingLevel = ROUTING_THINKING_LEVELS.find(level => level === selectedLevel);
108
+ if (!thinkingLevel)
109
+ continue;
110
+ const entry = { model: model.trim(), description: description.trim(), thinkingLevel };
107
111
  if (current)
108
112
  models[index] = entry;
109
113
  else
package/docs/rpc.md CHANGED
@@ -55,9 +55,9 @@ Four things that are not obvious from the tables:
55
55
 
56
56
  ### Model routing
57
57
 
58
- RPC and the manager registry follow the [same routingMode setting](../README.md#model-routing) as the tools. Under `auto` (default), enabled custom agents and a configured guideline take priority; with Jev active, a fresh spawn omitting both `model` and `thinkingLevel` is classified before worktree/session creation. Providing either field skips classification only under `auto`. Under `jev`, Jev chooses first, including over explicit or agent-file models; missing credentials, invalid configuration, uncertainty or errors keep the default-priority choice. Under `shadow`, the same check records a suggestion but preserves that choice. Under `off`, routing guidance and Jev are disabled. `null` means omitted. No mode creates an extra main-agent turn to interpret descriptions or Markdown. `awaitStartup` includes classification and its two-second deadline, while cancellation stops startup.
58
+ RPC and the manager registry follow the [same routingMode setting](../README.md#model-routing) as the tools. Under `auto` (default), enabled custom agents and a configured guideline take priority; with Jev active, a fresh spawn omitting both `model` and `thinkingLevel` is classified before worktree/session creation. Providing either field skips classification only under `auto`. A selected Jev profile applies its model and required `thinkingLevel` together; Pi clamps the level to model support. Shadow and fallback preserve both original values. Under `jev`, Jev chooses first, including over explicit or agent-file models; missing credentials, invalid configuration, uncertainty or errors keep the default-priority choice. Under `shadow`, the same check records a suggestion but preserves that choice. Under `off`, routing guidance and Jev are disabled. `null` means omitted. No mode creates an extra main-agent turn to interpret descriptions or Markdown. `awaitStartup` includes classification and its two-second deadline, while cancellation stops startup.
59
59
 
60
- Completion events expose the credential-free `routing` decision and optional classifier-only `routingUsage`. `routing.mode` identifies the mode, `model` an applied Jev choice, `suggestedModel` an observed shadow choice, and `fallbackSource` the preserved default source. Classifier tokens are separate from coding token totals; reported classifier cost, including shadow requests, is included in total cost once. `routing.unpriced` means the catalog does not provide a price. Settings events omit literal TypeSafe keys.
60
+ Completion events expose the credential-free `routing` decision and optional classifier-only `routingUsage`. `routing.mode` identifies the mode, `model` and `thinkingLevel` an applied Jev profile, `suggestedModel` and `suggestedThinkingLevel` an observed shadow profile, and `fallbackSource` the preserved default source. Classifier tokens are separate from coding token totals; reported classifier cost, including shadow requests, is included in total cost once. `routing.unpriced` means the catalog does not provide a price. Settings events omit literal TypeSafe keys.
61
61
 
62
62
  ### Names that look right and are not
63
63
 
package/docs/workflows.md CHANGED
@@ -308,7 +308,7 @@ A run's concurrency limit is its own, independent of the session's `maxConcurren
308
308
 
309
309
  ### Settings and the CLI flag
310
310
 
311
- Workflow children follow [Model routing](../README.md#model-routing). With `routingMode: "auto"` (default), custom agents come first, then a main-agent Markdown guideline, then optional Jev, then the existing model. The main agent receives the guideline before writing the script and can express its choice through `agent(prompt, { model, effort })`. Either explicit option skips Jev under `auto`; workflow options retain their precedence over agent-file defaults. Under `jev`, Jev chooses first and those defaults remain the fallback on uncertainty, missing credentials or errors. Under `shadow`, Jev records a suggestion without changing the model; under `off`, routing guidance and Jev are disabled. An inherited parent model is eligible for Jev. Resuming a child does not classify again in any mode.
311
+ Workflow children follow [Model routing](../README.md#model-routing). With `routingMode: "auto"` (default), custom agents come first, then a main-agent Markdown guideline, then optional Jev, then the existing model. The main agent receives the guideline before writing the script and can express its choice through `agent(prompt, { model, effort })`. Either explicit option skips Jev under `auto`; workflow options retain their precedence over agent-file defaults. A selected Jev profile applies its model and required `thinkingLevel` together; Pi clamps the level to model support. Shadow and fallback preserve both original values. Under `jev`, Jev chooses first and those defaults remain the fallback on uncertainty, missing credentials or errors. Under `shadow`, Jev records a suggestion without changing the model; under `off`, routing guidance and Jev are disabled. An inherited parent model is eligible for Jev. Resuming a child does not classify again in any mode.
312
312
 
313
313
  Jev classification happens once at child startup through Pi's native API, with a separate concurrency limit of four. Uncertainty, errors or a two-second timeout keep the existing model; stopping the workflow cancels classification without launching another child. Classifier tokens do not affect `budget.spent()` or coding context. Reported classifier cost rolls into child/ancestor cost totals once; classifier usage and the routing decision remain separately available on the agent record. Catalog-zero classifier prices mean unavailable pricing.
314
314
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diousk/pi-subagents-fast",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "A pi extension that brings Claude Code-like sub-agents and workflow orchestration to pi — parallel execution, live widget, fleet view, custom agent types, mid-run steering, dynamic workflows, Claude Code compatibility, look and feel.",
5
5
  "author": "tintinweb",
6
6
  "license": "MIT",
@@ -742,10 +742,12 @@ export class AgentManager {
742
742
  });
743
743
  record.routing = { ...routed.decision, guidelinePath: policy.guidelinePath, guidelineHash: policy.guidelineHash };
744
744
  if (routed.model && policy.mode === "shadow") {
745
- record.routing = { ...record.routing, code: "shadow", model: undefined, suggestedModel: routed.decision.model,
745
+ record.routing = { ...record.routing, code: "shadow", model: undefined, thinkingLevel: undefined, suggestedModel: routed.decision.model,
746
746
  fallbackSource: policy.source, reason: "Jev suggested a model; shadow mode kept the default-priority model" };
747
747
  } else if (routed.model) {
748
748
  options.model = routed.model;
749
+ options.thinkingLevel = routed.thinkingLevel;
750
+ record.invocation = { ...record.invocation, thinking: routed.thinkingLevel, requestedThinking: undefined };
749
751
  }
750
752
  } catch {
751
753
  record.routing.code = "classifier_error";
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFileSync, statSync } from "node:fs";
3
- import type { Api, ClassifierResult, Model, Usage } from "@earendil-works/pi-ai";
3
+ import type { Api, ClassifierResult, Model, ThinkingLevel, Usage } from "@earendil-works/pi-ai";
4
4
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import { loadCustomAgents } from "./custom-agents.js";
6
6
  import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
@@ -40,6 +40,8 @@ export interface RoutingDecision {
40
40
  "cancelled" | "timeout" | "invalid_answer" | "abstained" | "unavailable_choice" | "selected" | "classifier_error";
41
41
  model?: string;
42
42
  suggestedModel?: string;
43
+ thinkingLevel?: ThinkingLevel;
44
+ suggestedThinkingLevel?: ThinkingLevel;
43
45
  description?: string;
44
46
  confidence?: number;
45
47
  unpriced?: boolean;
@@ -93,9 +95,9 @@ export function routingGuidance(policy: RoutingPolicy): string {
93
95
  ? `Routing source: Custom guideline (${policy.guidelinePath}). Follow it when choosing Agent model/thinking (or workflow model/effort). Pass your default choice explicitly.\n<custom_routing_guideline>\n${policy.guideline}\n</custom_routing_guideline>`
94
96
  : policy.diagnostic ?? "Custom routing guideline is unavailable. Use the existing model.";
95
97
  break;
96
- case "jev": guidance = "Routing: Jev chooses the model for fresh agents when neither model nor thinking is explicitly set. Omit both to use automatic routing. Explicit choices and agent-file pins keep their existing precedence.";
98
+ case "jev": guidance = "Routing: Jev chooses the model and thinking level for fresh agents when neither model nor thinking is explicitly set. Omit both to use automatic routing. Explicit choices and agent-file pins keep their existing precedence.";
97
99
  }
98
- if (policy.mode === "jev") return "Routing mode: jev. Jev gets first choice of model for every fresh delegated task, including explicit model choices and agent-file model pins. Choose an agent and a default model using the guidance below; that choice is the fallback if Jev is unavailable or uncertain. Thinking keeps its existing precedence.\n" + guidance;
100
+ if (policy.mode === "jev") return "Routing mode: jev. Jev gets first choice of model for every fresh delegated task, including explicit model choices and agent-file model pins. Choose an agent and a default model using the guidance below; that choice is the fallback if Jev is unavailable or uncertain. The selected profile supplies both model and thinking level.\n" + guidance;
99
101
  if (policy.mode === "shadow") return "Routing mode: shadow. Jev records a suggestion for each fresh delegated task but never changes the model. Choose using the default guidance below, or keep the existing model when no guidance applies.\n" + guidance;
100
102
  return guidance && policy.source !== "jev" ? guidance + "\nJev is inactive under auto mode while this routing source applies." : guidance;
101
103
  }
@@ -148,7 +150,7 @@ export class ModelRouter {
148
150
  baseline: Model<Api> | undefined,
149
151
  signal: AbortSignal,
150
152
  onUsage: (usage: Usage) => void,
151
- ): Promise<{ model?: Model<Api>; decision: RoutingDecision }> {
153
+ ): Promise<{ model?: Model<Api>; thinkingLevel?: ThinkingLevel; decision: RoutingDecision }> {
152
154
  const decision: RoutingDecision = { mode: policy.mode, source: "jev", fallbackSource: policy.fallbackSource ?? (policy.source === "jev" ? "baseline" : policy.source), code: "baseline", reason: "Using the default-priority model" };
153
155
  const config = policy.jev;
154
156
  if (policy.mode === "off" || (policy.mode === "auto" && policy.source !== "jev")) return { decision: { ...decision, source: policy.source } };
@@ -169,7 +171,7 @@ export class ModelRouter {
169
171
  try {
170
172
  release = await this.acquire(controller.signal);
171
173
  if (!release) return { decision: { ...decision, code: signal.aborted ? "cancelled" : "timeout", reason: signal.aborted ? "Cancelled" : "Jev timed out" } };
172
- const choices = new Map(candidates.map((entry, index) => [`route_${index}`, entry.model]));
174
+ const choices = new Map(candidates.map((entry, index) => [`route_${index}`, entry]));
173
175
  const criteria: Record<string, string> = { keep_baseline: "Keep the existing model if none of the described models clearly fits the task." };
174
176
  for (const [index, entry] of candidates.entries()) criteria[`route_${index}`] = entry.description;
175
177
  const cancelled = new Promise<undefined>(resolve => {
@@ -208,14 +210,18 @@ export class ModelRouter {
208
210
  return { decision: { ...decision, code: "invalid_answer", reason: "Jev returned no usable choice" } };
209
211
  }
210
212
  decision.confidence = answer.confidence;
211
- if (policy.mode === "shadow") decision.suggestedModel = choices.get(answer.choice);
213
+ const profile = choices.get(answer.choice);
214
+ if (policy.mode === "shadow") {
215
+ decision.suggestedModel = profile?.model;
216
+ decision.suggestedThinkingLevel = profile?.thinkingLevel;
217
+ }
212
218
  if (answer.choice === "keep_baseline" || answer.confidence < 0.6) {
213
219
  return { decision: { ...decision, code: "abstained", reason: "Jev kept the existing model" } };
214
220
  }
215
- const selected = choices.get(answer.choice);
221
+ const selected = profile?.model;
216
222
  const model = selected ? eligibleModels(ctx).get(selected) : undefined;
217
223
  if (!model || signal.aborted) return { decision: { ...decision, code: "unavailable_choice", reason: "Selected model is no longer available in scope" } };
218
- return { model, decision: { ...decision, fallbackSource: undefined, code: "selected", model: selected, description: candidates.find(entry => entry.model === selected)?.description, reason: "Jev selected a configured model" } };
224
+ return { model, thinkingLevel: profile?.thinkingLevel, decision: { ...decision, fallbackSource: undefined, code: "selected", model: selected, thinkingLevel: profile?.thinkingLevel, description: profile?.description, reason: "Jev selected a configured model profile" } };
219
225
  } catch {
220
226
  // Provider errors may contain credentials. Keep diagnostics code-owned.
221
227
  return { decision: { ...decision, code: "classifier_error", reason: "Jev could not choose a model; using the existing model" } };
@@ -1,8 +1,12 @@
1
+ import type { ThinkingLevel } from "@earendil-works/pi-ai";
2
+
3
+ export const ROUTING_THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const satisfies readonly ThinkingLevel[];
4
+
1
5
  export type RoutingMode = "auto" | "shadow" | "jev" | "off";
2
6
 
3
7
  export interface JevConfig {
4
8
  TYPESAFE_API_KEY?: string;
5
- models: { model: string; description: string }[];
9
+ models: { model: string; description: string; thinkingLevel: ThinkingLevel }[];
6
10
  }
7
11
 
8
12
  /** Validate the whole block; never merge candidate lists or credentials. */
@@ -24,14 +28,16 @@ export function parseJevConfig(raw: unknown): JevConfig | false {
24
28
  const models = value.models.map((entry: unknown) => {
25
29
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error("Each Jev model needs model and description");
26
30
  const candidate = entry as Record<string, unknown>;
27
- if (Object.keys(candidate).some(key => key !== "model" && key !== "description") ||
31
+ if (Object.keys(candidate).some(key => key !== "model" && key !== "description" && key !== "thinkingLevel") ||
28
32
  typeof candidate.model !== "string" || !/^[^\s/]+\/\S+$/.test(candidate.model) ||
29
33
  typeof candidate.description !== "string" || !candidate.description.trim() || candidate.description.length > 4000) {
30
34
  throw new Error("Each Jev model needs an exact provider/model-id and a description of 1–4000 characters");
31
35
  }
36
+ const thinkingLevel = ROUTING_THINKING_LEVELS.find(level => level === candidate.thinkingLevel);
37
+ if (!thinkingLevel) throw new Error("Each Jev model requires thinkingLevel: minimal, low, medium, high, xhigh or max");
32
38
  if (seen.has(candidate.model)) throw new Error("jev.models contains duplicate models");
33
39
  seen.add(candidate.model);
34
- return { model: candidate.model, description: candidate.description.trim() };
40
+ return { model: candidate.model, description: candidate.description.trim(), thinkingLevel };
35
41
  });
36
42
  return { models, ...(typeof value.TYPESAFE_API_KEY === "string" ? { TYPESAFE_API_KEY: value.TYPESAFE_API_KEY } : {}) };
37
43
  }
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { Input, Text } from "@earendil-works/pi-tui";
3
3
  import { loadRoutingPolicy } from "../model-routing.js";
4
- import { parseJevConfig, type RoutingMode } from "../routing-config.js";
4
+ import { parseJevConfig, ROUTING_THINKING_LEVELS, type RoutingMode } from "../routing-config.js";
5
5
  import { loadSettings, projectRoutingSettings, type SubagentsSettings } from "../settings.js";
6
6
 
7
7
  type RoutingSettings = Pick<SubagentsSettings, "routingMode" | "customGuideline" | "jev">;
@@ -65,7 +65,7 @@ export async function showRoutingMenu(ctx: ExtensionCommandContext): Promise<Rou
65
65
  catch (err) { ctx.ui.notify(err instanceof Error ? err.message : "Invalid Jev settings", "warning"); return; }
66
66
  }
67
67
  for (;;) {
68
- const options = [...models.map((entry, index) => `${index + 1}. ${entry.model}`), "Add model", "Save", "Cancel"];
68
+ const options = [...models.map((entry, index) => `${index + 1}. ${entry.model} (${entry.thinkingLevel})`), "Add model", "Save", "Cancel"];
69
69
  const action = await ctx.ui.select("Jev models — describe which tasks each model should handle", options);
70
70
  if (!action || action === "Cancel") return;
71
71
  if (action === "Save") {
@@ -83,7 +83,10 @@ export async function showRoutingMenu(ctx: ExtensionCommandContext): Promise<Rou
83
83
  if (!model) continue;
84
84
  const description = await ctx.ui.editor("Tasks this model is suitable for", current?.description ?? "");
85
85
  if (!description?.trim()) continue;
86
- const entry = { model: model.trim(), description: description.trim() };
86
+ const selectedLevel = await ctx.ui.select(`Thinking level (required${current ? `; current: ${current.thinkingLevel}` : ""})`, [...ROUTING_THINKING_LEVELS]);
87
+ const thinkingLevel = ROUTING_THINKING_LEVELS.find(level => level === selectedLevel);
88
+ if (!thinkingLevel) continue;
89
+ const entry = { model: model.trim(), description: description.trim(), thinkingLevel };
87
90
  if (current) models[index] = entry;
88
91
  else models.push(entry);
89
92
  }