@diousk/pi-subagents-fast 0.24.0 → 0.25.1

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.
@@ -64,7 +64,7 @@ interface NestedSpawnOptions {
64
64
  invocation?: AgentInvocation;
65
65
  signal?: AbortSignal;
66
66
  onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
67
- onSessionCreated?: (session: AgentSession) => void;
67
+ onSessionCreated?: (session: AgentSession, agentConfig?: AgentConfig) => void;
68
68
  depth: number;
69
69
  parentAgentId: string;
70
70
  maxSubagentDepth: number;
@@ -259,7 +259,7 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
259
259
  const rootSessionId = context.manager.getRecord(context.parentAgentId)?.rootSessionId;
260
260
  const childDepth = context.depth + 1;
261
261
  const options: NestedSpawnOptions = {
262
- routing: { policy: loadRoutingPolicy(context.configCwd, registry), modelExplicit: !!invocation.modelInput, thinkingExplicit: invocation.thinking !== undefined, entrypoint: "nested" },
262
+ routing: { policy: loadRoutingPolicy(context.configCwd, registry), allowedAgentTypes: allowed ? [...allowed] : undefined, modelExplicit: !!invocation.modelInput, thinkingExplicit: invocation.thinking !== undefined, entrypoint: "nested" },
263
263
  agentConfig: config,
264
264
  description: params.description,
265
265
  model,
@@ -308,20 +308,18 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
308
308
  // explain itself. Filed under the ROOT session and this branch's config
309
309
  // root, so a nested transcript lands in the same `tasks/` directory as its
310
310
  // ancestors' rather than in a directory of its own.
311
- const transcriptSessionId =
312
- rootSessionId !== undefined && (config?.outputTranscript ?? getOutputTranscriptDefault())
313
- ? rootSessionId
314
- : undefined;
315
311
  let childId: string | undefined;
316
- const attachTranscript = (id: string): void => {
312
+ const attachTranscript = (id: string, selected = config): void => {
317
313
  childId = id;
318
- if (transcriptSessionId === undefined) return;
314
+ if (rootSessionId === undefined) return;
319
315
  const rec = context.manager.getRecord(id);
320
- if (!rec) return;
321
- rec.outputFile = createOutputFilePath(context.configCwd, id, transcriptSessionId);
316
+ if (!rec || rec.outputFile || (!rec.session && (rec.status === "queued" || options.routing?.policy?.mode === "jev" || rec.routing?.mode === "jev"))) return;
317
+ if (!(selected?.outputTranscript ?? getOutputTranscriptDefault())) return;
318
+ rec.outputFile = createOutputFilePath(context.configCwd, id, rootSessionId);
322
319
  writeInitialEntry(rec.outputFile, id, params.prompt, ctx.cwd);
323
320
  };
324
- options.onSessionCreated = (session) => {
321
+ options.onSessionCreated = (session, selected) => {
322
+ if (childId !== undefined) attachTranscript(childId, selected);
325
323
  const rec = childId === undefined ? undefined : context.manager.getRecord(childId);
326
324
  if (rec?.outputFile && childId !== undefined) {
327
325
  rec.outputCleanup = streamToOutputFile(session, rec.outputFile, childId, ctx.cwd);
@@ -6,7 +6,7 @@ export type RoutingMode = "auto" | "shadow" | "jev" | "off";
6
6
 
7
7
  export interface JevConfig {
8
8
  TYPESAFE_API_KEY?: string;
9
- models: { model: string; description: string; thinkingLevel: ThinkingLevel }[];
9
+ models: { model: string; description: string; thinkingLevel: ThinkingLevel; instruction?: string }[];
10
10
  }
11
11
 
12
12
  /** Validate the whole block; never merge candidate lists or credentials. */
@@ -21,23 +21,28 @@ export function parseJevConfig(raw: unknown): JevConfig | false {
21
21
  (typeof value.TYPESAFE_API_KEY !== "string" || !value.TYPESAFE_API_KEY || /\s|[\x00-\x1f\x7f]/.test(value.TYPESAFE_API_KEY))) {
22
22
  throw new Error("TYPESAFE_API_KEY must be a nonempty token without whitespace or control characters");
23
23
  }
24
- if (!Array.isArray(value.models) || value.models.length < 1 || value.models.length > 254) {
25
- throw new Error("jev.models must contain 1–254 models");
24
+ const entries = value.models ?? [];
25
+ if (!Array.isArray(entries) || entries.length > 254 || value.models === null) {
26
+ throw new Error("jev.models must contain 0–254 models");
26
27
  }
27
28
  const seen = new Set<string>();
28
- const models = value.models.map((entry: unknown) => {
29
+ const models = entries.map((entry: unknown) => {
29
30
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error("Each Jev model needs model and description");
30
31
  const candidate = entry as Record<string, unknown>;
31
- if (Object.keys(candidate).some(key => key !== "model" && key !== "description" && key !== "thinkingLevel") ||
32
+ if (Object.keys(candidate).some(key => key !== "model" && key !== "description" && key !== "thinkingLevel" && key !== "instruction") ||
32
33
  typeof candidate.model !== "string" || !/^[^\s/]+\/\S+$/.test(candidate.model) ||
33
34
  typeof candidate.description !== "string" || !candidate.description.trim() || candidate.description.length > 4000) {
34
35
  throw new Error("Each Jev model needs an exact provider/model-id and a description of 1–4000 characters");
35
36
  }
36
37
  const thinkingLevel = ROUTING_THINKING_LEVELS.find(level => level === candidate.thinkingLevel);
37
38
  if (!thinkingLevel) throw new Error("Each Jev model requires thinkingLevel: minimal, low, medium, high, xhigh or max");
39
+ if (candidate.instruction !== undefined && (typeof candidate.instruction !== "string" || candidate.instruction.length > 16_000)) {
40
+ throw new Error("Jev model instruction must be a string of at most 16000 characters");
41
+ }
42
+ const instruction = typeof candidate.instruction === "string" ? candidate.instruction.trim() : "";
38
43
  if (seen.has(candidate.model)) throw new Error("jev.models contains duplicate models");
39
44
  seen.add(candidate.model);
40
- return { model: candidate.model, description: candidate.description.trim(), thinkingLevel };
45
+ return { model: candidate.model, description: candidate.description.trim(), thinkingLevel, ...(instruction ? { instruction } : {}) };
41
46
  });
42
47
  return { models, ...(typeof value.TYPESAFE_API_KEY === "string" ? { TYPESAFE_API_KEY: value.TYPESAFE_API_KEY } : {}) };
43
48
  }
@@ -32,7 +32,7 @@ export async function showRoutingMenu(ctx: ExtensionCommandContext, records: ()
32
32
  : policy.source === "guideline" ? "Jev is inactive while a custom guideline is configured." : "";
33
33
  const note = policy.mode === "off" ? "No routing guidance or Jev requests."
34
34
  : policy.mode === "shadow" ? `Observe Jev; actual choice: ${labels[policy.source]}. Jev calls may incur charges.`
35
- : policy.mode === "jev" ? `Jev first; fallback: ${labels[policy.fallbackSource ?? "baseline"]}.${policy.jev ? "" : " Configure Jev models and credentials to use Jev."}`
35
+ : policy.mode === "jev" ? `Wait for Jev to choose an agent/model; fallback: ${labels[policy.fallbackSource ?? "baseline"]}.${policy.jev ? "" : " Configure agent/model profiles and TypeSafe credentials to use Jev."}`
36
36
  : policy.diagnostic ?? inactive;
37
37
  const choice = await ctx.ui.select(`Model routing: ${policy.mode} — ${note || labels[policy.source]}`, [
38
38
  "Runtime status", "Routing mode", "Custom guideline path", "Jev models and descriptions", "Typesafe API key", "Use Pi/environment credentials", "Disable custom guideline", "Disable Jev", "Back",
@@ -62,9 +62,8 @@ export async function showRoutingMenu(ctx: ExtensionCommandContext, records: ()
62
62
  const models = settings.jev ? settings.jev.models.map(entry => ({ ...entry })) : [];
63
63
  // Creating a project block must not silently copy a global credential.
64
64
  const localKey = local.jev ? local.jev.TYPESAFE_API_KEY : undefined;
65
- if (choice === "Use Pi/environment credentials") return models.length ? { jev: { models } } : undefined;
65
+ if (choice === "Use Pi/environment credentials") return { jev: { models } };
66
66
  if (choice === "Typesafe API key") {
67
- if (!models.length) { ctx.ui.notify("Configure Jev models first.", "info"); return; }
68
67
  const key = await maskedApiKey(ctx);
69
68
  if (!key) return;
70
69
  try { return { jev: parseJevConfig({ models, TYPESAFE_API_KEY: key }) }; }
@@ -92,7 +91,9 @@ export async function showRoutingMenu(ctx: ExtensionCommandContext, records: ()
92
91
  const selectedLevel = await ctx.ui.select(`Thinking level (required${current ? `; current: ${current.thinkingLevel}` : ""})`, [...ROUTING_THINKING_LEVELS]);
93
92
  const thinkingLevel = ROUTING_THINKING_LEVELS.find(level => level === selectedLevel);
94
93
  if (!thinkingLevel) continue;
95
- const entry = { model: model.trim(), description: description.trim(), thinkingLevel };
94
+ const instruction = await ctx.ui.editor("Additional instruction (optional; blank to omit)", current?.instruction ?? "");
95
+ if (instruction === undefined) continue;
96
+ const entry = { model: model.trim(), description: description.trim(), thinkingLevel, ...(instruction.trim() ? { instruction: instruction.trim() } : {}) };
96
97
  if (current) models[index] = entry;
97
98
  else models.push(entry);
98
99
  }
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { matchesKey, Text } from "@earendil-works/pi-tui";
3
- import { eligibleModels, loadRoutingPolicy } from "../model-routing.js";
3
+ import { eligibleModels, loadRoutingPolicy, routingCandidates } from "../model-routing.js";
4
4
  import { isScopeModelsEnabled } from "../model-scope.js";
5
5
  import { loadRoutingSettings, projectRoutingSettings } from "../settings.js";
6
6
  import type { AgentRecord } from "../types.js";
@@ -10,7 +10,7 @@ export async function routingStatusText(ctx: ExtensionCommandContext, records: r
10
10
  const { settings, guidelineFile } = loadRoutingSettings(ctx.cwd);
11
11
  const local = projectRoutingSettings(ctx.cwd);
12
12
  const policy = loadRoutingPolicy(ctx.cwd);
13
- const config = settings.jev;
13
+ const config = settings.jev === false ? false : policy.jev ?? settings.jev;
14
14
  const apiKey = config ? config.TYPESAFE_API_KEY : undefined;
15
15
  const available = eligibleModels(ctx);
16
16
  const labels = { agents: "Custom agents", guideline: "Custom guideline", jev: "Jev", baseline: "Existing model" };
@@ -49,6 +49,14 @@ export async function routingStatusText(ctx: ExtensionCommandContext, records: r
49
49
  lines.push(`${profile.model} | thinking: ${profile.thinkingLevel} | ${available.has(profile.model) ? "eligible" : "unavailable or excluded by scope"}`, ` ${profile.description}`);
50
50
  }
51
51
  } else lines.push("None. Every profile requires model, description and thinkingLevel.");
52
+ if (policy.mode === "jev" || policy.mode === "shadow") {
53
+ const candidates = routingCandidates(ctx, policy, ctx.model);
54
+ lines.push("", `Combined eligible profiles: ${candidates.length} (maximum 254)`);
55
+ for (const agent of policy.agentProfiles ?? []) {
56
+ const candidate = candidates.find(entry => entry.agentConfig?.name === agent.name);
57
+ lines.push(`Agent: ${agent.name} | ${candidate ? `${candidate.model} | thinking: ${candidate.thinkingLevel ?? "inherited"} | eligible` : "unavailable or excluded by scope"}`, ` ${agent.description}`);
58
+ }
59
+ }
52
60
  lines.push("", "Recent retained agents (latest 10; includes nested/workflow agents):");
53
61
  const recent = [...records].filter(record => record.routing).sort((a, b) => b.startedAt - a.startedAt).slice(0, 10);
54
62
  if (!recent.length) lines.push("No routing decisions yet. Start a fresh subagent; resumed agents do not reroute.");
@@ -58,6 +66,8 @@ export async function routingStatusText(ctx: ExtensionCommandContext, records: r
58
66
  ` Mode: ${route.mode} | Source: ${route.source} | Result: ${route.code}`,
59
67
  ` ${route.reason}`);
60
68
  if (route.model) lines.push(` Selected: ${route.model} | requested thinking: ${route.thinkingLevel ?? "inherited"}`);
69
+ if (route.agent) lines.push(` Selected agent: ${route.agent}`);
70
+ if (route.suggestedAgent) lines.push(` Shadow agent suggestion: ${route.suggestedAgent}`);
61
71
  if (route.suggestedModel) lines.push(` Shadow suggestion: ${route.suggestedModel} | thinking: ${route.suggestedThinkingLevel ?? "inherited"}`);
62
72
  if (record.invocation?.modelId) lines.push(` Actual: ${record.invocation.modelId} | effective thinking: ${record.invocation.thinking ?? "unknown"}`);
63
73
  if (route.confidence !== undefined) lines.push(` Confidence: ${(route.confidence * 100).toFixed(1)}%`);