@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.
- package/CHANGELOG.md +25 -0
- package/README.md +99 -6
- package/dist/agent-manager.d.ts +7 -1
- package/dist/agent-manager.js +63 -2
- package/dist/agent-runner.d.ts +7 -4
- package/dist/agent-runner.js +36 -27
- package/dist/custom-agents.js +10 -6
- package/dist/index.js +45 -4
- package/dist/mention-clone.d.ts +0 -14
- package/dist/mention-clone.js +33 -37
- package/dist/model-routing.d.ts +54 -0
- package/dist/model-routing.js +211 -0
- package/dist/nested-tools.d.ts +4 -1
- package/dist/nested-tools.js +4 -1
- package/dist/output-file.js +10 -4
- package/dist/routing-config.d.ts +10 -0
- package/dist/routing-config.js +34 -0
- package/dist/schedule.js +23 -22
- package/dist/settings.d.ts +16 -0
- package/dist/settings.js +58 -11
- package/dist/types.d.ts +7 -2
- package/dist/ui/model-routing-menu.d.ts +8 -0
- package/dist/ui/model-routing-menu.js +112 -0
- package/dist/workflow/host.js +2 -0
- package/docs/rpc.md +7 -0
- package/docs/workflows.md +6 -0
- package/package.json +7 -7
- package/src/agent-manager.ts +65 -3
- package/src/agent-runner.ts +43 -34
- package/src/custom-agents.ts +9 -6
- package/src/index.ts +43 -4
- package/src/mention-clone.ts +36 -40
- package/src/model-routing.ts +229 -0
- package/src/nested-tools.ts +6 -1
- package/src/output-file.ts +10 -4
- package/src/routing-config.ts +37 -0
- package/src/schedule.ts +23 -22
- package/src/settings.ts +62 -11
- package/src/types.ts +7 -2
- package/src/ui/model-routing-menu.ts +90 -0
- package/src/workflow/host.ts +2 -0
- package/vitest.config.mts +47 -0
package/src/settings.ts
CHANGED
|
@@ -3,12 +3,20 @@
|
|
|
3
3
|
// - Project: <cwd>/.pi/subagents.json — written by /agents → Settings; overrides global on load
|
|
4
4
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
-
import {
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
8
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
8
9
|
import { NO_FALLBACK } from "./agent-types.js";
|
|
10
|
+
import { type JevConfig, parseJevConfig, type RoutingMode } from "./routing-config.js";
|
|
9
11
|
import type { AgentMentionMode, JoinMode, ViewerMarkdownMode, WidgetMode } from "./types.js";
|
|
10
12
|
|
|
11
13
|
export interface SubagentsSettings {
|
|
14
|
+
/** auto (default), observe-only shadow, Jev-first jev, or no routing guidance/off. */
|
|
15
|
+
routingMode?: RoutingMode;
|
|
16
|
+
/** Main-agent routing instructions. false disables an inherited guideline. */
|
|
17
|
+
customGuideline?: string | false;
|
|
18
|
+
/** Model descriptions and optional credentials for automatic routing. */
|
|
19
|
+
jev?: JevConfig | false;
|
|
12
20
|
maxConcurrent?: number;
|
|
13
21
|
/**
|
|
14
22
|
* Max concurrent FOREGROUND (blocking) agents — `0` = unlimited, the default,
|
|
@@ -351,11 +359,29 @@ const MAX_TURNS_CEILING = 10_000;
|
|
|
351
359
|
const GRACE_TURNS_CEILING = 1_000;
|
|
352
360
|
const SUBAGENT_DEPTH_CEILING = 16;
|
|
353
361
|
|
|
354
|
-
/** Drop
|
|
362
|
+
/** Drop invalid operational fields; explicit invalid routing settings disable their route. */
|
|
355
363
|
function sanitize(raw: unknown): SubagentsSettings {
|
|
356
364
|
if (!raw || typeof raw !== "object") return {};
|
|
357
365
|
const r = raw as Record<string, unknown>;
|
|
358
366
|
const out: SubagentsSettings = {};
|
|
367
|
+
if (Object.hasOwn(r, "routingMode")) {
|
|
368
|
+
if (r.routingMode === "auto" || r.routingMode === "shadow" || r.routingMode === "jev" || r.routingMode === "off") {
|
|
369
|
+
out.routingMode = r.routingMode;
|
|
370
|
+
} else {
|
|
371
|
+
out.routingMode = "off";
|
|
372
|
+
console.warn("[pi-subagents] Invalid routingMode; model routing is off. Use auto, shadow, jev or off.");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (Object.hasOwn(r, "customGuideline")) {
|
|
376
|
+
out.customGuideline = r.customGuideline === false ? false : typeof r.customGuideline === "string" ? r.customGuideline.trim() : "";
|
|
377
|
+
}
|
|
378
|
+
if (Object.hasOwn(r, "jev")) {
|
|
379
|
+
try { out.jev = parseJevConfig(r.jev); }
|
|
380
|
+
catch (err) {
|
|
381
|
+
out.jev = false;
|
|
382
|
+
console.warn(`[pi-subagents] Invalid Jev settings: ${err instanceof Error ? err.message : "invalid configuration"}. Using the existing model.`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
359
385
|
if (
|
|
360
386
|
Number.isInteger(r.maxConcurrent) &&
|
|
361
387
|
(r.maxConcurrent as number) >= 1 &&
|
|
@@ -475,22 +501,41 @@ function projectPath(cwd: string): string {
|
|
|
475
501
|
/**
|
|
476
502
|
* Read a settings file. Missing file is silent (returns `{}`). A file that
|
|
477
503
|
* exists but can't be parsed emits a warning to stderr so users aren't
|
|
478
|
-
* silently reverted to defaults
|
|
504
|
+
* silently reverted to defaults. Routing is disabled for that file; startup proceeds.
|
|
479
505
|
*/
|
|
480
506
|
function readSettingsFile(path: string): SubagentsSettings {
|
|
481
507
|
if (!existsSync(path)) return {};
|
|
482
508
|
try {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
509
|
+
const raw: unknown = JSON.parse(readFileSync(path, "utf-8"));
|
|
510
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Settings must be an object");
|
|
511
|
+
return sanitize(raw);
|
|
512
|
+
} catch {
|
|
513
|
+
// JSON parser errors can quote a literal API key. Never echo file contents.
|
|
514
|
+
console.warn(`[pi-subagents] Ignoring malformed settings at ${path}. Model routing is disabled for this file.`);
|
|
515
|
+
return { routingMode: "off", customGuideline: "", jev: false };
|
|
488
516
|
}
|
|
489
517
|
}
|
|
490
518
|
|
|
491
519
|
/** Load merged settings: global provides defaults, project overrides. */
|
|
492
520
|
export function loadSettings(cwd: string = process.cwd()): SubagentsSettings {
|
|
493
|
-
return
|
|
521
|
+
return loadRoutingSettings(cwd).settings;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Keep the supplying config directory for relative guideline paths. */
|
|
525
|
+
export function loadRoutingSettings(cwd: string): { settings: SubagentsSettings; guidelineFile?: string } {
|
|
526
|
+
const global = readSettingsFile(globalPath());
|
|
527
|
+
const project = readSettingsFile(projectPath(cwd));
|
|
528
|
+
const settings = { ...global, ...project };
|
|
529
|
+
const root = dirname(project.customGuideline !== undefined ? projectPath(cwd) : globalPath());
|
|
530
|
+
const path = settings.customGuideline;
|
|
531
|
+
return { settings, guidelineFile: typeof path === "string" && path
|
|
532
|
+
? resolve(root, path === "~" || path.startsWith("~/") ? homedir() + path.slice(1) : path) : undefined };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** Preserve routing inheritance when an unrelated menu setting is saved. */
|
|
536
|
+
export function projectRoutingSettings(cwd: string): Pick<SubagentsSettings, "routingMode" | "customGuideline" | "jev"> {
|
|
537
|
+
const settings = readSettingsFile(projectPath(cwd));
|
|
538
|
+
return { routingMode: settings.routingMode, customGuideline: settings.customGuideline, jev: settings.jev };
|
|
494
539
|
}
|
|
495
540
|
|
|
496
541
|
/**
|
|
@@ -565,7 +610,7 @@ export function applyAndEmitLoaded(
|
|
|
565
610
|
): SubagentsSettings {
|
|
566
611
|
const settings = loadSettings(cwd);
|
|
567
612
|
applySettings(settings, appliers);
|
|
568
|
-
emit("subagents:settings_loaded", { settings });
|
|
613
|
+
emit("subagents:settings_loaded", { settings: publicSettings(settings) });
|
|
569
614
|
return settings;
|
|
570
615
|
}
|
|
571
616
|
|
|
@@ -582,6 +627,12 @@ export function saveAndEmitChanged(
|
|
|
582
627
|
cwd: string = process.cwd(),
|
|
583
628
|
): { message: string; level: "info" | "warning" } {
|
|
584
629
|
const persisted = saveSettings(snapshot, cwd);
|
|
585
|
-
emit("subagents:settings_changed", { settings: snapshot, persisted });
|
|
630
|
+
emit("subagents:settings_changed", { settings: publicSettings(snapshot), persisted });
|
|
586
631
|
return persistToastFor(successMsg, persisted);
|
|
587
632
|
}
|
|
633
|
+
|
|
634
|
+
/** Public snapshots never expose a literal Typesafe credential. */
|
|
635
|
+
export function publicSettings(settings: SubagentsSettings): SubagentsSettings {
|
|
636
|
+
if (!settings.jev) return { ...settings };
|
|
637
|
+
return { ...settings, jev: { models: settings.jev.models.map(entry => ({ ...entry })) } };
|
|
638
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* types.ts — Type definitions for the subagent system.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
5
|
+
import type { ThinkingLevel, Usage } from "@earendil-works/pi-ai";
|
|
6
6
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { RoutingDecision } from "./model-routing.js";
|
|
7
8
|
import type { LifetimeUsage } from "./usage.js";
|
|
8
9
|
|
|
9
10
|
export type { ThinkingLevel };
|
|
@@ -18,7 +19,7 @@ export const DEFAULT_AGENT_NAMES = ["general-purpose", "Explore", "Plan"] as con
|
|
|
18
19
|
export type MemoryScope = "user" | "project" | "local";
|
|
19
20
|
|
|
20
21
|
/** OpenAI Responses/Codex request processing tier. */
|
|
21
|
-
export type ServiceTier = "auto" | "default" | "flex" | "priority" | "scale";
|
|
22
|
+
export type ServiceTier = "auto" | "default" | "flex" | "fast" | "priority" | "scale";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Isolation mode for agent execution.
|
|
@@ -159,6 +160,10 @@ export type MentionResolution =
|
|
|
159
160
|
| { kind: "tombstone"; entry: AgentTombstone };
|
|
160
161
|
|
|
161
162
|
export interface AgentRecord {
|
|
163
|
+
/** Credential-free startup decision, separate from effective session metadata. */
|
|
164
|
+
routing?: RoutingDecision;
|
|
165
|
+
/** Classifier spend; excluded from coding-context token counters. */
|
|
166
|
+
routingUsage?: Usage;
|
|
162
167
|
id: string;
|
|
163
168
|
type: SubagentType;
|
|
164
169
|
/**
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Input, Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { loadRoutingPolicy } from "../model-routing.js";
|
|
4
|
+
import { parseJevConfig, type RoutingMode } from "../routing-config.js";
|
|
5
|
+
import { loadSettings, projectRoutingSettings, type SubagentsSettings } from "../settings.js";
|
|
6
|
+
|
|
7
|
+
type RoutingSettings = Pick<SubagentsSettings, "routingMode" | "customGuideline" | "jev">;
|
|
8
|
+
|
|
9
|
+
/** Input handles paste/editing, but its unmasked renderer is never called. */
|
|
10
|
+
export async function maskedApiKey(ctx: ExtensionCommandContext): Promise<string | undefined> {
|
|
11
|
+
return ctx.ui.custom<string | undefined>((_tui, _theme, _kb, done) => {
|
|
12
|
+
const input = new Input();
|
|
13
|
+
input.onSubmit = value => done(value.trim());
|
|
14
|
+
input.onEscape = () => done(undefined);
|
|
15
|
+
return {
|
|
16
|
+
render: (width: number) => new Text(`Typesafe API key: ${"*".repeat(input.getValue().length)}\nEnter to save; Esc to cancel`, 0, 0).render(width),
|
|
17
|
+
invalidate: () => {},
|
|
18
|
+
handleInput: (data: string) => input.handleInput(data),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Simple mode, guideline, models and credentials; saves project overrides. */
|
|
24
|
+
export async function showRoutingMenu(ctx: ExtensionCommandContext): Promise<RoutingSettings | undefined> {
|
|
25
|
+
const settings = loadSettings(ctx.cwd);
|
|
26
|
+
const local = projectRoutingSettings(ctx.cwd);
|
|
27
|
+
const policy = loadRoutingPolicy(ctx.cwd);
|
|
28
|
+
const labels = { agents: "Custom agents", guideline: "Custom guideline", jev: "Jev", baseline: "Existing model" };
|
|
29
|
+
const inactive = policy.source === "agents" ? "Guideline and Jev are inactive while custom agents exist."
|
|
30
|
+
: policy.source === "guideline" ? "Jev is inactive while a custom guideline is configured." : "";
|
|
31
|
+
const note = policy.mode === "off" ? "No routing guidance or Jev requests."
|
|
32
|
+
: policy.mode === "shadow" ? `Observe Jev; actual choice: ${labels[policy.source]}. Jev calls may incur charges.`
|
|
33
|
+
: policy.mode === "jev" ? `Jev first; fallback: ${labels[policy.fallbackSource ?? "baseline"]}.${policy.jev ? "" : " Configure Jev models and credentials to use Jev."}`
|
|
34
|
+
: policy.diagnostic ?? inactive;
|
|
35
|
+
const choice = await ctx.ui.select(`Model routing: ${policy.mode} — ${note || labels[policy.source]}`, [
|
|
36
|
+
"Routing mode", "Custom guideline path", "Jev models and descriptions", "Typesafe API key", "Use Pi/environment credentials", "Disable custom guideline", "Disable Jev", "Back",
|
|
37
|
+
]);
|
|
38
|
+
if (!choice || choice === "Back") return;
|
|
39
|
+
if (choice === "Routing mode") {
|
|
40
|
+
const modes: { mode: RoutingMode; label: string }[] = [
|
|
41
|
+
{ mode: "auto", label: "auto — default priority: agents, guideline, Jev, existing model" },
|
|
42
|
+
{ mode: "shadow", label: "shadow — record Jev suggestions; keep the default model (may incur charges)" },
|
|
43
|
+
{ mode: "jev", label: "jev — Jev first; default-priority fallback on uncertainty or failure" },
|
|
44
|
+
{ mode: "off", label: "off — no routing guidance or Jev requests" },
|
|
45
|
+
];
|
|
46
|
+
const selected = await ctx.ui.select(`Routing mode (current: ${policy.mode})`, modes.map(entry => entry.label));
|
|
47
|
+
const mode = modes.find(entry => entry.label === selected)?.mode;
|
|
48
|
+
return mode ? { routingMode: mode } : undefined;
|
|
49
|
+
}
|
|
50
|
+
if (choice === "Disable custom guideline") return { customGuideline: false };
|
|
51
|
+
if (choice === "Disable Jev") return { jev: false };
|
|
52
|
+
if (choice === "Custom guideline path") {
|
|
53
|
+
const path = await ctx.ui.input("Routing guideline Markdown path", typeof settings.customGuideline === "string" ? settings.customGuideline : "~/.pi/agent/agents/custom-route.md");
|
|
54
|
+
return path?.trim() ? { customGuideline: path.trim() } : undefined;
|
|
55
|
+
}
|
|
56
|
+
const models = settings.jev ? settings.jev.models.map(entry => ({ ...entry })) : [];
|
|
57
|
+
// Creating a project block must not silently copy a global credential.
|
|
58
|
+
const localKey = local.jev ? local.jev.TYPESAFE_API_KEY : undefined;
|
|
59
|
+
if (choice === "Use Pi/environment credentials") return models.length ? { jev: { models } } : undefined;
|
|
60
|
+
if (choice === "Typesafe API key") {
|
|
61
|
+
if (!models.length) { ctx.ui.notify("Configure Jev models first.", "info"); return; }
|
|
62
|
+
const key = await maskedApiKey(ctx);
|
|
63
|
+
if (!key) return;
|
|
64
|
+
try { return { jev: parseJevConfig({ models, TYPESAFE_API_KEY: key }) }; }
|
|
65
|
+
catch (err) { ctx.ui.notify(err instanceof Error ? err.message : "Invalid Jev settings", "warning"); return; }
|
|
66
|
+
}
|
|
67
|
+
for (;;) {
|
|
68
|
+
const options = [...models.map((entry, index) => `${index + 1}. ${entry.model}`), "Add model", "Save", "Cancel"];
|
|
69
|
+
const action = await ctx.ui.select("Jev models — describe which tasks each model should handle", options);
|
|
70
|
+
if (!action || action === "Cancel") return;
|
|
71
|
+
if (action === "Save") {
|
|
72
|
+
try { return { jev: parseJevConfig({ models, ...(localKey ? { TYPESAFE_API_KEY: localKey } : {}) }) }; }
|
|
73
|
+
catch (err) { ctx.ui.notify(err instanceof Error ? err.message : "Invalid Jev models", "warning"); continue; }
|
|
74
|
+
}
|
|
75
|
+
const index = options.indexOf(action);
|
|
76
|
+
const current = action === "Add model" ? undefined : models[index];
|
|
77
|
+
if (current) {
|
|
78
|
+
const edit = await ctx.ui.select(current.model, ["Edit", "Remove", "Back"]);
|
|
79
|
+
if (edit === "Remove") { models.splice(index, 1); continue; }
|
|
80
|
+
if (edit !== "Edit") continue;
|
|
81
|
+
}
|
|
82
|
+
const model = await ctx.ui.input("Exact provider/model-id", current?.model);
|
|
83
|
+
if (!model) continue;
|
|
84
|
+
const description = await ctx.ui.editor("Tasks this model is suitable for", current?.description ?? "");
|
|
85
|
+
if (!description?.trim()) continue;
|
|
86
|
+
const entry = { model: model.trim(), description: description.trim() };
|
|
87
|
+
if (current) models[index] = entry;
|
|
88
|
+
else models.push(entry);
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/workflow/host.ts
CHANGED
|
@@ -298,6 +298,8 @@ export function createWorkflowHost(deps: WorkflowHostOptions): WorkflowHost {
|
|
|
298
298
|
request.prompt,
|
|
299
299
|
{
|
|
300
300
|
description: request.label,
|
|
301
|
+
agentConfig: config,
|
|
302
|
+
routing: { modelExplicit: modelInput !== undefined, thinkingExplicit: request.effort !== undefined || config?.thinking !== undefined, entrypoint: "workflow" },
|
|
301
303
|
// The stamp is what keeps this child out of the session's
|
|
302
304
|
// `maxConcurrent` pool — see `occupiesPoolSlot`. The run already
|
|
303
305
|
// bounds how many of its agents run at once, and counting them
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
// The print-mode e2e suite (test/subagents-print-mode-e2e.test.ts) drives REAL
|
|
5
|
+
// faux-model turns through pi-coding-agent + pi-agent-core. That requires ONE
|
|
6
|
+
// shared @earendil-works/pi-ai instance so the faux provider the test registers
|
|
7
|
+
// lands in the same api-registry the session streams through. npm physically
|
|
8
|
+
// duplicates pi-ai (a top-level copy and one nested under pi-coding-agent), which
|
|
9
|
+
// otherwise yields two registries and "No API provider registered" errors.
|
|
10
|
+
// Inlining the @earendil-works packages routes them through Vite's resolver so
|
|
11
|
+
// dedupe can collapse pi-ai to a single instance — for the parent AND for every
|
|
12
|
+
// subagent session the extension spawns. dedupe alone is insufficient (it only
|
|
13
|
+
// affects modules Vite resolves; without inline the runtime stays externalized).
|
|
14
|
+
test: {
|
|
15
|
+
server: { deps: { inline: [/@earendil-works\/pi-/] } },
|
|
16
|
+
// Local reporting only — deliberately no `thresholds`, and not wired into
|
|
17
|
+
// CI. src/index.ts is mostly the /agents wizard, which is TUI flow with
|
|
18
|
+
// almost no logic and is not worth a fake-TUI harness; any global floor
|
|
19
|
+
// would therefore either sit below what the rest of the suite achieves
|
|
20
|
+
// (and ratchet down as tests are deleted) or force exactly that harness.
|
|
21
|
+
// Coverage here measures lines touched, not behavior pinned.
|
|
22
|
+
//
|
|
23
|
+
// `istanbul`, NOT the `v8` default. v8 under-reports badly on this suite:
|
|
24
|
+
// `src/env.ts` came out at 54.54% for the full run but 90.9% for
|
|
25
|
+
// `vitest run --coverage test/env.test.ts` — coverage cannot fall as more
|
|
26
|
+
// tests run. Bisecting pins it on test/subagents-print-mode-e2e.test.ts,
|
|
27
|
+
// which boots a REAL pi session; pi's own resource loader re-loads the
|
|
28
|
+
// extension and its imports, so the same source file appears to v8 twice —
|
|
29
|
+
// once executed, once barely — and the merge takes the wrong one rather
|
|
30
|
+
// than the union. The damage was not confined to env.ts: v8 also reported
|
|
31
|
+
// module-level `const` declarations in src/settings.ts as uncovered, which
|
|
32
|
+
// is impossible since they run on import.
|
|
33
|
+
//
|
|
34
|
+
// istanbul instruments at transform time and accumulates per source path,
|
|
35
|
+
// so a second load adds to the same counters instead of shadowing them.
|
|
36
|
+
// Same suite, same run: v8 68.6% vs istanbul 76.5% statements, and the
|
|
37
|
+
// per-file numbers now match what a single-file run reports. If you switch
|
|
38
|
+
// this back to v8, re-check env.ts against `--coverage test/env.test.ts`
|
|
39
|
+
// before trusting anything the table says.
|
|
40
|
+
coverage: {
|
|
41
|
+
provider: "istanbul",
|
|
42
|
+
reporter: ["text", "html"],
|
|
43
|
+
include: ["src/**/*.ts"],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
resolve: { dedupe: ["@earendil-works/pi-ai"] },
|
|
47
|
+
});
|