@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.
- package/CHANGELOG.md +11 -0
- package/README.md +28 -10
- package/dist/agent-manager.d.ts +1 -1
- package/dist/agent-manager.js +26 -4
- package/dist/agent-runner.d.ts +5 -3
- package/dist/agent-runner.js +6 -4
- package/dist/index.js +19 -11
- package/dist/model-routing.d.ts +18 -2
- package/dist/model-routing.js +37 -10
- package/dist/nested-tools.d.ts +1 -1
- package/dist/nested-tools.js +10 -9
- package/dist/routing-config.d.ts +1 -0
- package/dist/routing-config.js +10 -5
- package/dist/ui/model-routing-menu.js +6 -7
- package/dist/ui/routing-status.js +14 -2
- package/docs/rpc.md +2 -0
- package/docs/workflows.md +2 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +26 -5
- package/src/agent-runner.ts +9 -5
- package/src/index.ts +17 -11
- package/src/model-routing.ts +46 -11
- package/src/nested-tools.ts +9 -11
- package/src/routing-config.ts +11 -6
- package/src/ui/model-routing-menu.ts +5 -4
- package/src/ui/routing-status.ts +12 -2
package/src/nested-tools.ts
CHANGED
|
@@ -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 (
|
|
314
|
+
if (rootSessionId === undefined) return;
|
|
319
315
|
const rec = context.manager.getRecord(id);
|
|
320
|
-
if (!rec) return;
|
|
321
|
-
|
|
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);
|
package/src/routing-config.ts
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/src/ui/routing-status.ts
CHANGED
|
@@ -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)}%`);
|