@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/dist/nested-tools.js
CHANGED
|
@@ -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,
|
package/dist/output-file.js
CHANGED
|
@@ -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
|
-
//
|
|
95
|
-
// messages
|
|
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 ??
|
|
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 {
|
|
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
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
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:
|
|
233
|
-
isolated:
|
|
234
|
-
thinkingLevel:
|
|
235
|
-
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:
|
|
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(
|
|
246
|
-
isolated:
|
|
246
|
+
maxTurns: normalizeMaxTurns(invocation.maxTurns),
|
|
247
|
+
isolated: invocation.isolated,
|
|
247
248
|
runInBackground: true,
|
|
248
|
-
isolation:
|
|
249
|
+
isolation: invocation.isolation,
|
|
249
250
|
},
|
|
250
251
|
});
|
|
251
252
|
}
|
package/dist/settings.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
console.warn(`[pi-subagents] Ignoring malformed settings at ${path}
|
|
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
|
|
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
|
+
}
|
package/dist/workflow/host.js
CHANGED
|
@@ -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.
|
|
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.
|
|
28
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
29
|
-
"@earendil-works/pi-tui": ">=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.
|
|
54
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
55
|
-
"@earendil-works/pi-tui": "0.
|
|
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",
|