@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 +5 -0
- package/README.md +10 -10
- package/dist/agent-manager.js +3 -1
- package/dist/model-routing.d.ts +4 -1
- package/dist/model-routing.js +10 -7
- package/dist/routing-config.d.ts +3 -0
- package/dist/routing-config.js +6 -2
- package/dist/ui/model-routing-menu.js +7 -3
- package/docs/rpc.md +2 -2
- package/docs/workflows.md +1 -1
- package/package.json +1 -1
- package/src/agent-manager.ts +3 -1
- package/src/model-routing.ts +14 -8
- package/src/routing-config.ts +9 -3
- package/src/ui/model-routing-menu.ts +6 -3
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
|
|
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
|
|
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
|
|
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 #
|
|
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
|
package/dist/agent-manager.js
CHANGED
|
@@ -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 {
|
package/dist/model-routing.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/model-routing.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
190
|
-
|
|
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 =
|
|
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,
|
|
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.
|
package/dist/routing-config.d.ts
CHANGED
|
@@ -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. */
|
package/dist/routing-config.js
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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",
|
package/src/agent-manager.ts
CHANGED
|
@@ -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";
|
package/src/model-routing.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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" } };
|
package/src/routing-config.ts
CHANGED
|
@@ -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
|
|
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
|
}
|