@diousk/pi-subagents-fast 0.20.0 → 0.22.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.
@@ -5,6 +5,7 @@ import { buildAgentRegistry, getAgentConfigIn, getAvailableTypesIn, resolveEnabl
5
5
  import { loadCustomAgents } from "./custom-agents.js";
6
6
  import { isolationParam, resolveAgentInvocationConfig } from "./invocation-config.js";
7
7
  import { resolveModel } from "./model-resolver.js";
8
+ import { loadRoutingPolicy, routingGuidance } from "./model-routing.js";
8
9
  import { checkModelScope } from "./model-scope.js";
9
10
  import { createOutputFilePath, getOutputTranscriptDefault, streamToOutputFile, writeInitialEntry, } from "./output-file.js";
10
11
  import { getForegroundOutcomeNote, getStatusNote, partialOutputSuffix } from "./status-note.js";
@@ -59,7 +60,7 @@ export function createNestedSubagentTools(context) {
59
60
  name: NESTED_TOOL_NAMES[0],
60
61
  label: "Agent",
61
62
  description: "Launch a child-safe nested subagent for bounded delegated work. " +
62
- "Only use agent types allowed by this parent agent; nesting is depth-limited.",
63
+ "Only use agent types allowed by this parent agent; nesting is depth-limited.\n" + routingGuidance(loadRoutingPolicy(context.configCwd)),
63
64
  parameters: Type.Object({
64
65
  prompt: Type.String({ description: "Self-contained task for the nested agent." }),
65
66
  description: Type.String({ description: "Short 3-5 word task description." }),
@@ -140,6 +141,8 @@ export function createNestedSubagentTools(context) {
140
141
  const rootSessionId = context.manager.getRecord(context.parentAgentId)?.rootSessionId;
141
142
  const childDepth = context.depth + 1;
142
143
  const options = {
144
+ routing: { policy: loadRoutingPolicy(context.configCwd, registry), modelExplicit: !!invocation.modelInput, thinkingExplicit: invocation.thinking !== undefined, entrypoint: "nested" },
145
+ agentConfig: config,
143
146
  description: params.description,
144
147
  model,
145
148
  maxTurns: invocation.maxTurns,
@@ -91,20 +91,26 @@ export function writeInitialEntry(path, agentId, prompt, cwd) {
91
91
  * Returns a cleanup function that does a final flush and unsubscribes.
92
92
  */
93
93
  export function streamToOutputFile(session, path, agentId, cwd, startIndex) {
94
- // Index of the first message this stream is responsible for. A spawn writes
95
- // messages[0] as the initial prompt entry, so it starts at 1. A resume hands
94
+ // A spawn writes its initial user prompt separately. Pi can project system
95
+ // messages before that prompt, so skip the first user message by role. A resume hands
96
96
  // in the session's length as of just before the run: the session already
97
97
  // holds every prior turn, and re-emitting those would duplicate history that
98
98
  // is already in the file.
99
- let writtenCount = startIndex ?? 1;
99
+ let writtenCount = startIndex ?? 0;
100
+ let skipInitialUser = startIndex === undefined;
100
101
  const flush = () => {
101
102
  const messages = session.messages;
102
103
  while (writtenCount < messages.length) {
103
104
  const msg = messages[writtenCount];
105
+ if (skipInitialUser && msg.role === "user") {
106
+ skipInitialUser = false;
107
+ writtenCount++;
108
+ continue;
109
+ }
104
110
  const entry = {
105
111
  isSidechain: true,
106
112
  agentId,
107
- type: msg.role === "assistant" ? "assistant" : msg.role === "user" ? "user" : "toolResult",
113
+ type: msg.role === "assistant" ? "assistant" : msg.role === "user" ? "user" : msg.role === "system" ? "system" : "toolResult",
108
114
  message: msg,
109
115
  timestamp: new Date().toISOString(),
110
116
  cwd,
@@ -0,0 +1,10 @@
1
+ export type RoutingMode = "auto" | "shadow" | "jev" | "off";
2
+ export interface JevConfig {
3
+ TYPESAFE_API_KEY?: string;
4
+ models: {
5
+ model: string;
6
+ description: string;
7
+ }[];
8
+ }
9
+ /** Validate the whole block; never merge candidate lists or credentials. */
10
+ export declare function parseJevConfig(raw: unknown): JevConfig | false;
@@ -0,0 +1,34 @@
1
+ /** Validate the whole block; never merge candidate lists or credentials. */
2
+ export function parseJevConfig(raw) {
3
+ if (raw === false)
4
+ return false;
5
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
6
+ throw new Error("jev must be an object or false");
7
+ const value = raw;
8
+ if (Object.keys(value).some(key => key !== "models" && key !== "TYPESAFE_API_KEY")) {
9
+ throw new Error("jev accepts only TYPESAFE_API_KEY and models");
10
+ }
11
+ if (value.TYPESAFE_API_KEY !== undefined &&
12
+ (typeof value.TYPESAFE_API_KEY !== "string" || !value.TYPESAFE_API_KEY || /\s|[\x00-\x1f\x7f]/.test(value.TYPESAFE_API_KEY))) {
13
+ throw new Error("TYPESAFE_API_KEY must be a nonempty token without whitespace or control characters");
14
+ }
15
+ if (!Array.isArray(value.models) || value.models.length < 1 || value.models.length > 254) {
16
+ throw new Error("jev.models must contain 1–254 models");
17
+ }
18
+ const seen = new Set();
19
+ const models = value.models.map((entry) => {
20
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
21
+ throw new Error("Each Jev model needs model and description");
22
+ const candidate = entry;
23
+ if (Object.keys(candidate).some(key => key !== "model" && key !== "description") ||
24
+ typeof candidate.model !== "string" || !/^[^\s/]+\/\S+$/.test(candidate.model) ||
25
+ typeof candidate.description !== "string" || !candidate.description.trim() || candidate.description.length > 4000) {
26
+ throw new Error("Each Jev model needs an exact provider/model-id and a description of 1–4000 characters");
27
+ }
28
+ if (seen.has(candidate.model))
29
+ throw new Error("jev.models contains duplicate models");
30
+ seen.add(candidate.model);
31
+ return { model: candidate.model, description: candidate.description.trim() };
32
+ });
33
+ return { models, ...(typeof value.TYPESAFE_API_KEY === "string" ? { TYPESAFE_API_KEY: value.TYPESAFE_API_KEY } : {}) };
34
+ }
package/dist/schedule.js CHANGED
@@ -17,7 +17,9 @@
17
17
  import { Cron } from "croner";
18
18
  import { nanoid } from "nanoid";
19
19
  import { normalizeMaxTurns } from "./agent-runner.js";
20
- import { resolveSpawnType } from "./agent-types.js";
20
+ import { buildAgentRegistry, getAgentConfigIn, resolveSpawnTypeIn } from "./agent-types.js";
21
+ import { loadCustomAgents } from "./custom-agents.js";
22
+ import { resolveAgentInvocationConfig } from "./invocation-config.js";
21
23
  import { resolveModel } from "./model-resolver.js";
22
24
  export class SubagentScheduler {
23
25
  jobs = new Map();
@@ -207,45 +209,44 @@ export class SubagentScheduler {
207
209
  // Resolve model at fire time — registry contents may have changed since the
208
210
  // job was created (auth added/removed). Fall back silently to spawn-default
209
211
  // if resolution fails; the spawn path handles undefined model gracefully.
210
- let resolvedModel;
211
- if (job.model) {
212
- const r = resolveModel(job.model, ctx.modelRegistry);
213
- if (typeof r !== "string")
214
- resolvedModel = r;
215
- }
216
212
  let agentId;
217
213
  try {
218
- // Re-resolve at fire time against the registry as it stands. This does not
219
- // reload from disk (the scheduler has no reason to rebuild process-global
220
- // state from a timer), so it catches changes that went through /agents or
221
- // an Agent call — not a file deleted directly from a shell. The catch below turns
222
- // this into lastStatus: "error" plus an error event, like any other
223
- // fire-time failure.
224
- const dispatch = resolveSpawnType(job.subagent_type);
214
+ // Read current files into a local registry. Timer dispatch must not mutate
215
+ // the main session's registry or freeze inherited defaults at creation.
216
+ const registry = buildAgentRegistry(loadCustomAgents(ctx.cwd));
217
+ const dispatch = resolveSpawnTypeIn(registry, job.subagent_type);
225
218
  if (!dispatch.ok)
226
219
  throw new Error(dispatch.message);
220
+ const agentConfig = getAgentConfigIn(registry, dispatch.type);
221
+ const invocation = resolveAgentInvocationConfig(agentConfig, {
222
+ model: job.model, thinking: job.thinking, max_turns: job.max_turns, isolated: job.isolated, isolation: job.isolation,
223
+ }, { worktreeAllowed: true, defaultRunInBackground: true });
224
+ const resolved = invocation.modelInput ? resolveModel(invocation.modelInput, ctx.modelRegistry) : undefined;
225
+ const resolvedModel = typeof resolved === "string" ? undefined : resolved;
227
226
  agentId = manager.spawn(pi, ctx, dispatch.type, job.prompt, {
227
+ agentConfig,
228
+ routing: { modelExplicit: !!invocation.modelInput, thinkingExplicit: invocation.thinking !== undefined, entrypoint: "schedule" },
228
229
  description: job.description,
229
230
  isBackground: true,
230
231
  bypassQueue: true,
231
232
  model: resolvedModel,
232
- maxTurns: job.max_turns,
233
- isolated: job.isolated,
234
- thinkingLevel: job.thinking,
235
- isolation: job.isolation,
233
+ maxTurns: invocation.maxTurns,
234
+ isolated: invocation.isolated,
235
+ thinkingLevel: invocation.thinking,
236
+ isolation: invocation.isolation,
236
237
  // A scheduled run has no tool call to build this, so without it the
237
238
  // conversation viewer shows nothing about how the job was configured.
238
239
  // The model is left out on purpose: agent-manager fills in the effective
239
240
  // one when the session reports it, and naming the pre-session pick here
240
241
  // would only be right until then.
241
242
  invocation: {
242
- thinking: job.thinking,
243
+ thinking: invocation.thinking,
243
244
  // Normalized like the Agent tool's own snapshot: `0` means unlimited,
244
245
  // and rendering it as "max turns: 0" would read as a limit of none.
245
- maxTurns: normalizeMaxTurns(job.max_turns),
246
- isolated: job.isolated,
246
+ maxTurns: normalizeMaxTurns(invocation.maxTurns),
247
+ isolated: invocation.isolated,
247
248
  runInBackground: true,
248
- isolation: job.isolation,
249
+ isolation: invocation.isolation,
249
250
  },
250
251
  });
251
252
  }
@@ -1,5 +1,12 @@
1
+ import { type JevConfig, type RoutingMode } from "./routing-config.js";
1
2
  import type { AgentMentionMode, JoinMode, ViewerMarkdownMode, WidgetMode } from "./types.js";
2
3
  export interface SubagentsSettings {
4
+ /** auto (default), observe-only shadow, Jev-first jev, or no routing guidance/off. */
5
+ routingMode?: RoutingMode;
6
+ /** Main-agent routing instructions. false disables an inherited guideline. */
7
+ customGuideline?: string | false;
8
+ /** Model descriptions and optional credentials for automatic routing. */
9
+ jev?: JevConfig | false;
3
10
  maxConcurrent?: number;
4
11
  /**
5
12
  * Max concurrent FOREGROUND (blocking) agents — `0` = unlimited, the default,
@@ -325,6 +332,13 @@ export interface SettingsAppliers {
325
332
  export type SettingsEmit = (event: string, payload: unknown) => void;
326
333
  /** Load merged settings: global provides defaults, project overrides. */
327
334
  export declare function loadSettings(cwd?: string): SubagentsSettings;
335
+ /** Keep the supplying config directory for relative guideline paths. */
336
+ export declare function loadRoutingSettings(cwd: string): {
337
+ settings: SubagentsSettings;
338
+ guidelineFile?: string;
339
+ };
340
+ /** Preserve routing inheritance when an unrelated menu setting is saved. */
341
+ export declare function projectRoutingSettings(cwd: string): Pick<SubagentsSettings, "routingMode" | "customGuideline" | "jev">;
328
342
  /**
329
343
  * Write project-local settings. Global is never touched from code.
330
344
  * Returns `true` on success, `false` if the write (or mkdir) failed so the
@@ -358,3 +372,5 @@ export declare function saveAndEmitChanged(snapshot: SubagentsSettings, successM
358
372
  message: string;
359
373
  level: "info" | "warning";
360
374
  };
375
+ /** Public snapshots never expose a literal Typesafe credential. */
376
+ export declare function publicSettings(settings: SubagentsSettings): SubagentsSettings;
package/dist/settings.js CHANGED
@@ -2,9 +2,11 @@
2
2
  // - Global: ~/.pi/agent/subagents.json (via getAgentDir()) — manual defaults, never written here
3
3
  // - Project: <cwd>/.pi/subagents.json — written by /agents → Settings; overrides global on load
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
- import { dirname, join } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join, resolve } from "node:path";
6
7
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
7
8
  import { NO_FALLBACK } from "./agent-types.js";
9
+ import { parseJevConfig } from "./routing-config.js";
8
10
  const VALID_JOIN_MODES = new Set(["async", "group", "smart"]);
9
11
  const VALID_TOOL_DESCRIPTION_MODES = new Set(["full", "compact", "custom"]);
10
12
  const VALID_WIDGET_MODES = new Set(["all", "background", "off"]);
@@ -17,12 +19,33 @@ const MAX_CONCURRENT_CEILING = 1024;
17
19
  const MAX_TURNS_CEILING = 10_000;
18
20
  const GRACE_TURNS_CEILING = 1_000;
19
21
  const SUBAGENT_DEPTH_CEILING = 16;
20
- /** Drop fields that don't match the expected shape. Silent — garbage becomes absent. */
22
+ /** Drop invalid operational fields; explicit invalid routing settings disable their route. */
21
23
  function sanitize(raw) {
22
24
  if (!raw || typeof raw !== "object")
23
25
  return {};
24
26
  const r = raw;
25
27
  const out = {};
28
+ if (Object.hasOwn(r, "routingMode")) {
29
+ if (r.routingMode === "auto" || r.routingMode === "shadow" || r.routingMode === "jev" || r.routingMode === "off") {
30
+ out.routingMode = r.routingMode;
31
+ }
32
+ else {
33
+ out.routingMode = "off";
34
+ console.warn("[pi-subagents] Invalid routingMode; model routing is off. Use auto, shadow, jev or off.");
35
+ }
36
+ }
37
+ if (Object.hasOwn(r, "customGuideline")) {
38
+ out.customGuideline = r.customGuideline === false ? false : typeof r.customGuideline === "string" ? r.customGuideline.trim() : "";
39
+ }
40
+ if (Object.hasOwn(r, "jev")) {
41
+ try {
42
+ out.jev = parseJevConfig(r.jev);
43
+ }
44
+ catch (err) {
45
+ out.jev = false;
46
+ console.warn(`[pi-subagents] Invalid Jev settings: ${err instanceof Error ? err.message : "invalid configuration"}. Using the existing model.`);
47
+ }
48
+ }
26
49
  if (Number.isInteger(r.maxConcurrent) &&
27
50
  r.maxConcurrent >= 1 &&
28
51
  r.maxConcurrent <= MAX_CONCURRENT_CEILING) {
@@ -131,23 +154,41 @@ function projectPath(cwd) {
131
154
  /**
132
155
  * Read a settings file. Missing file is silent (returns `{}`). A file that
133
156
  * exists but can't be parsed emits a warning to stderr so users aren't
134
- * silently reverted to defaults — and still returns `{}` so startup proceeds.
157
+ * silently reverted to defaults. Routing is disabled for that file; startup proceeds.
135
158
  */
136
159
  function readSettingsFile(path) {
137
160
  if (!existsSync(path))
138
161
  return {};
139
162
  try {
140
- return sanitize(JSON.parse(readFileSync(path, "utf-8")));
163
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
164
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
165
+ throw new Error("Settings must be an object");
166
+ return sanitize(raw);
141
167
  }
142
- catch (err) {
143
- const reason = err instanceof Error ? err.message : String(err);
144
- console.warn(`[pi-subagents] Ignoring malformed settings at ${path}: ${reason}`);
145
- return {};
168
+ catch {
169
+ // JSON parser errors can quote a literal API key. Never echo file contents.
170
+ console.warn(`[pi-subagents] Ignoring malformed settings at ${path}. Model routing is disabled for this file.`);
171
+ return { routingMode: "off", customGuideline: "", jev: false };
146
172
  }
147
173
  }
148
174
  /** Load merged settings: global provides defaults, project overrides. */
149
175
  export function loadSettings(cwd = process.cwd()) {
150
- return { ...readSettingsFile(globalPath()), ...readSettingsFile(projectPath(cwd)) };
176
+ return loadRoutingSettings(cwd).settings;
177
+ }
178
+ /** Keep the supplying config directory for relative guideline paths. */
179
+ export function loadRoutingSettings(cwd) {
180
+ const global = readSettingsFile(globalPath());
181
+ const project = readSettingsFile(projectPath(cwd));
182
+ const settings = { ...global, ...project };
183
+ const root = dirname(project.customGuideline !== undefined ? projectPath(cwd) : globalPath());
184
+ const path = settings.customGuideline;
185
+ return { settings, guidelineFile: typeof path === "string" && path
186
+ ? resolve(root, path === "~" || path.startsWith("~/") ? homedir() + path.slice(1) : path) : undefined };
187
+ }
188
+ /** Preserve routing inheritance when an unrelated menu setting is saved. */
189
+ export function projectRoutingSettings(cwd) {
190
+ const settings = readSettingsFile(projectPath(cwd));
191
+ return { routingMode: settings.routingMode, customGuideline: settings.customGuideline, jev: settings.jev };
151
192
  }
152
193
  /**
153
194
  * Write project-local settings. Global is never touched from code.
@@ -235,7 +276,7 @@ export function persistToastFor(successMsg, persisted) {
235
276
  export function applyAndEmitLoaded(appliers, emit, cwd = process.cwd()) {
236
277
  const settings = loadSettings(cwd);
237
278
  applySettings(settings, appliers);
238
- emit("subagents:settings_loaded", { settings });
279
+ emit("subagents:settings_loaded", { settings: publicSettings(settings) });
239
280
  return settings;
240
281
  }
241
282
  /**
@@ -246,6 +287,12 @@ export function applyAndEmitLoaded(appliers, emit, cwd = process.cwd()) {
246
287
  */
247
288
  export function saveAndEmitChanged(snapshot, successMsg, emit, cwd = process.cwd()) {
248
289
  const persisted = saveSettings(snapshot, cwd);
249
- emit("subagents:settings_changed", { settings: snapshot, persisted });
290
+ emit("subagents:settings_changed", { settings: publicSettings(snapshot), persisted });
250
291
  return persistToastFor(successMsg, persisted);
251
292
  }
293
+ /** Public snapshots never expose a literal Typesafe credential. */
294
+ export function publicSettings(settings) {
295
+ if (!settings.jev)
296
+ return { ...settings };
297
+ return { ...settings, jev: { models: settings.jev.models.map(entry => ({ ...entry })) } };
298
+ }
package/dist/types.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * types.ts — Type definitions for the subagent system.
3
3
  */
4
- import type { ThinkingLevel } from "@earendil-works/pi-ai";
4
+ import type { ThinkingLevel, Usage } from "@earendil-works/pi-ai";
5
5
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
6
+ import type { RoutingDecision } from "./model-routing.js";
6
7
  import type { LifetimeUsage } from "./usage.js";
7
8
  export type { ThinkingLevel };
8
9
  /** Agent type: any string name (built-in defaults or user-defined). */
@@ -12,7 +13,7 @@ export declare const DEFAULT_AGENT_NAMES: readonly ["general-purpose", "Explore"
12
13
  /** Memory scope for persistent agent memory. */
13
14
  export type MemoryScope = "user" | "project" | "local";
14
15
  /** OpenAI Responses/Codex request processing tier. */
15
- export type ServiceTier = "auto" | "default" | "flex" | "priority" | "scale";
16
+ export type ServiceTier = "auto" | "default" | "flex" | "fast" | "priority" | "scale";
16
17
  /**
17
18
  * Isolation mode for agent execution.
18
19
  *
@@ -148,6 +149,10 @@ export type MentionResolution = {
148
149
  entry: AgentTombstone;
149
150
  };
150
151
  export interface AgentRecord {
152
+ /** Credential-free startup decision, separate from effective session metadata. */
153
+ routing?: RoutingDecision;
154
+ /** Classifier spend; excluded from coding-context token counters. */
155
+ routingUsage?: Usage;
151
156
  id: string;
152
157
  type: SubagentType;
153
158
  /**
@@ -0,0 +1,8 @@
1
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import { type SubagentsSettings } from "../settings.js";
3
+ type RoutingSettings = Pick<SubagentsSettings, "routingMode" | "customGuideline" | "jev">;
4
+ /** Input handles paste/editing, but its unmasked renderer is never called. */
5
+ export declare function maskedApiKey(ctx: ExtensionCommandContext): Promise<string | undefined>;
6
+ /** Simple mode, guideline, models and credentials; saves project overrides. */
7
+ export declare function showRoutingMenu(ctx: ExtensionCommandContext): Promise<RoutingSettings | undefined>;
8
+ export {};
@@ -0,0 +1,112 @@
1
+ import { Input, Text } from "@earendil-works/pi-tui";
2
+ import { loadRoutingPolicy } from "../model-routing.js";
3
+ import { parseJevConfig } from "../routing-config.js";
4
+ import { loadSettings, projectRoutingSettings } from "../settings.js";
5
+ /** Input handles paste/editing, but its unmasked renderer is never called. */
6
+ export async function maskedApiKey(ctx) {
7
+ return ctx.ui.custom((_tui, _theme, _kb, done) => {
8
+ const input = new Input();
9
+ input.onSubmit = value => done(value.trim());
10
+ input.onEscape = () => done(undefined);
11
+ return {
12
+ render: (width) => new Text(`Typesafe API key: ${"*".repeat(input.getValue().length)}\nEnter to save; Esc to cancel`, 0, 0).render(width),
13
+ invalidate: () => { },
14
+ handleInput: (data) => input.handleInput(data),
15
+ };
16
+ });
17
+ }
18
+ /** Simple mode, guideline, models and credentials; saves project overrides. */
19
+ export async function showRoutingMenu(ctx) {
20
+ const settings = loadSettings(ctx.cwd);
21
+ const local = projectRoutingSettings(ctx.cwd);
22
+ const policy = loadRoutingPolicy(ctx.cwd);
23
+ const labels = { agents: "Custom agents", guideline: "Custom guideline", jev: "Jev", baseline: "Existing model" };
24
+ const inactive = policy.source === "agents" ? "Guideline and Jev are inactive while custom agents exist."
25
+ : policy.source === "guideline" ? "Jev is inactive while a custom guideline is configured." : "";
26
+ const note = policy.mode === "off" ? "No routing guidance or Jev requests."
27
+ : policy.mode === "shadow" ? `Observe Jev; actual choice: ${labels[policy.source]}. Jev calls may incur charges.`
28
+ : policy.mode === "jev" ? `Jev first; fallback: ${labels[policy.fallbackSource ?? "baseline"]}.${policy.jev ? "" : " Configure Jev models and credentials to use Jev."}`
29
+ : policy.diagnostic ?? inactive;
30
+ const choice = await ctx.ui.select(`Model routing: ${policy.mode} — ${note || labels[policy.source]}`, [
31
+ "Routing mode", "Custom guideline path", "Jev models and descriptions", "Typesafe API key", "Use Pi/environment credentials", "Disable custom guideline", "Disable Jev", "Back",
32
+ ]);
33
+ if (!choice || choice === "Back")
34
+ return;
35
+ if (choice === "Routing mode") {
36
+ const modes = [
37
+ { mode: "auto", label: "auto — default priority: agents, guideline, Jev, existing model" },
38
+ { mode: "shadow", label: "shadow — record Jev suggestions; keep the default model (may incur charges)" },
39
+ { mode: "jev", label: "jev — Jev first; default-priority fallback on uncertainty or failure" },
40
+ { mode: "off", label: "off — no routing guidance or Jev requests" },
41
+ ];
42
+ const selected = await ctx.ui.select(`Routing mode (current: ${policy.mode})`, modes.map(entry => entry.label));
43
+ const mode = modes.find(entry => entry.label === selected)?.mode;
44
+ return mode ? { routingMode: mode } : undefined;
45
+ }
46
+ if (choice === "Disable custom guideline")
47
+ return { customGuideline: false };
48
+ if (choice === "Disable Jev")
49
+ return { jev: false };
50
+ if (choice === "Custom guideline path") {
51
+ const path = await ctx.ui.input("Routing guideline Markdown path", typeof settings.customGuideline === "string" ? settings.customGuideline : "~/.pi/agent/agents/custom-route.md");
52
+ return path?.trim() ? { customGuideline: path.trim() } : undefined;
53
+ }
54
+ const models = settings.jev ? settings.jev.models.map(entry => ({ ...entry })) : [];
55
+ // Creating a project block must not silently copy a global credential.
56
+ const localKey = local.jev ? local.jev.TYPESAFE_API_KEY : undefined;
57
+ if (choice === "Use Pi/environment credentials")
58
+ return models.length ? { jev: { models } } : undefined;
59
+ if (choice === "Typesafe API key") {
60
+ if (!models.length) {
61
+ ctx.ui.notify("Configure Jev models first.", "info");
62
+ return;
63
+ }
64
+ const key = await maskedApiKey(ctx);
65
+ if (!key)
66
+ return;
67
+ try {
68
+ return { jev: parseJevConfig({ models, TYPESAFE_API_KEY: key }) };
69
+ }
70
+ catch (err) {
71
+ ctx.ui.notify(err instanceof Error ? err.message : "Invalid Jev settings", "warning");
72
+ return;
73
+ }
74
+ }
75
+ for (;;) {
76
+ const options = [...models.map((entry, index) => `${index + 1}. ${entry.model}`), "Add model", "Save", "Cancel"];
77
+ const action = await ctx.ui.select("Jev models — describe which tasks each model should handle", options);
78
+ if (!action || action === "Cancel")
79
+ return;
80
+ if (action === "Save") {
81
+ try {
82
+ return { jev: parseJevConfig({ models, ...(localKey ? { TYPESAFE_API_KEY: localKey } : {}) }) };
83
+ }
84
+ catch (err) {
85
+ ctx.ui.notify(err instanceof Error ? err.message : "Invalid Jev models", "warning");
86
+ continue;
87
+ }
88
+ }
89
+ const index = options.indexOf(action);
90
+ const current = action === "Add model" ? undefined : models[index];
91
+ if (current) {
92
+ const edit = await ctx.ui.select(current.model, ["Edit", "Remove", "Back"]);
93
+ if (edit === "Remove") {
94
+ models.splice(index, 1);
95
+ continue;
96
+ }
97
+ if (edit !== "Edit")
98
+ continue;
99
+ }
100
+ const model = await ctx.ui.input("Exact provider/model-id", current?.model);
101
+ if (!model)
102
+ continue;
103
+ const description = await ctx.ui.editor("Tasks this model is suitable for", current?.description ?? "");
104
+ if (!description?.trim())
105
+ continue;
106
+ const entry = { model: model.trim(), description: description.trim() };
107
+ if (current)
108
+ models[index] = entry;
109
+ else
110
+ models.push(entry);
111
+ }
112
+ }
@@ -260,6 +260,8 @@ export function createWorkflowHost(deps) {
260
260
  try {
261
261
  const { record } = await manager.spawnAndWait(pi, ctx, dispatch.type, request.prompt, {
262
262
  description: request.label,
263
+ agentConfig: config,
264
+ routing: { modelExplicit: modelInput !== undefined, thinkingExplicit: request.effort !== undefined || config?.thinking !== undefined, entrypoint: "workflow" },
263
265
  // The stamp is what keeps this child out of the session's
264
266
  // `maxConcurrent` pool — see `occupiesPoolSlot`. The run already
265
267
  // bounds how many of its agents run at once, and counting them
package/docs/rpc.md CHANGED
@@ -34,6 +34,7 @@ For the channel list, the reply envelope, the per-channel snippets and the event
34
34
 
35
35
  | Field | Why it is taken away |
36
36
  |---|---|
37
+ | `routing`, `agentConfig` | Private routing provenance and selected agent definition; callers cannot turn an explicit model into an inherited default or inject a Jev policy |
37
38
  | `parentAgentId` | Ownership. A forged parent hides your agent under someone else's nested tools |
38
39
  | `workflowId` | A forged value would hide an RPC-spawned agent inside someone else's workflow — and take it out of the concurrency pool with it |
39
40
  | `depth`, `maxSubagentDepth` | The nesting cap is inherited, not declared |
@@ -52,6 +53,12 @@ Four things that are not obvious from the tables:
52
53
  - **`structuredOutput` is documented "set only by the workflow host"** (`src/agent-manager.ts:231-234`) and is also not stripped.
53
54
  - **`signal` and the `on*` callbacks are function values.** They work only because the bus is in-process. A caller that genuinely serializes its payload cannot use them, and they arrive as `undefined` rather than failing.
54
55
 
56
+ ### Model routing
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.
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.
61
+
55
62
  ### Names that look right and are not
56
63
 
57
64
  One of these already shipped as a bug in this project's own README example, so it is worth reading the table even if you are sure.
package/docs/workflows.md CHANGED
@@ -250,6 +250,8 @@ Any other key is rejected **by name** at the call. Note that this checks option
250
250
 
251
251
  Combination rules: `resume` cannot be combined with `agentType`, `model`, `effort`, `isolation`, `gate` or `schema` — a resumed child keeps the agent type, model and tree it was started with, and its session predates the `StructuredOutput` tool.
252
252
 
253
+ With Pi 0.99.1, select Sol with `model: "openai-codex/gpt-6.1-sol"` (Pi's Codex login) or `model: "openai/gpt-6.1-sol"` (API key). Use `effort: "low"`, `"medium"`, `"high"`, `"xhigh"`, or `"max"`; Pi maps `minimal` to provider effort `low`. A custom `agentType` can pin `service_tier: fast` in its agent file; `service_tier` is not a workflow option. See the [README's model reference](../README.md#frontmatter-fields).
254
+
253
255
  ### `pipeline()` and `parallel()`
254
256
 
255
257
  ```js
@@ -306,6 +308,10 @@ A run's concurrency limit is its own, independent of the session's `maxConcurren
306
308
 
307
309
  ### Settings and the CLI flag
308
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.
312
+
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
+
309
315
  `workflowsEnabled` is **on**; leaving it unset means *auto*, which is on unless another extension already offers a `Workflow` or `SubagentWorkflow` tool, in which case this one stands down for the session. Setting it explicitly pins it. See [Persistent settings](../README.md#persistent-settings).
310
316
 
311
317
  `pi --subagents-workflow-file=<path>` runs a workflow at startup, including headless under `pi -p`. Use the `=` form — the bare `--flag value` spelling swallows the next argument. See [CLI flags](../README.md#cli-flags).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diousk/pi-subagents-fast",
3
- "version": "0.20.0",
3
+ "version": "0.22.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",
@@ -24,9 +24,9 @@
24
24
  "autonomous"
25
25
  ],
26
26
  "peerDependencies": {
27
- "@earendil-works/pi-ai": ">=0.84.0",
28
- "@earendil-works/pi-coding-agent": ">=0.84.0",
29
- "@earendil-works/pi-tui": ">=0.84.0"
27
+ "@earendil-works/pi-ai": ">=1.0.0",
28
+ "@earendil-works/pi-coding-agent": ">=1.0.0",
29
+ "@earendil-works/pi-tui": ">=1.0.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "@sinclair/typebox": "^0.34.49",
@@ -50,9 +50,9 @@
50
50
  },
51
51
  "devDependencies": {
52
52
  "@biomejs/biome": "^2.4.14",
53
- "@earendil-works/pi-ai": "0.84.2",
54
- "@earendil-works/pi-coding-agent": "0.84.2",
55
- "@earendil-works/pi-tui": "0.84.2",
53
+ "@earendil-works/pi-ai": "1.0.0",
54
+ "@earendil-works/pi-coding-agent": "1.0.0",
55
+ "@earendil-works/pi-tui": "1.0.0",
56
56
  "@types/node": "^25.5.0",
57
57
  "@vitest/coverage-istanbul": "^4.1.10",
58
58
  "typescript": "^6.0.0",