@cr1ms0n/pi-subagent 0.8.9 → 0.9.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 +11 -1
- package/README.md +218 -115
- package/docs/ARCHITECTURE.md +56 -13
- package/docs/COST-ACCOUNTING.md +116 -66
- package/docs/RELEASING.md +32 -32
- package/docs/SECURITY.md +42 -5
- package/docs/UX.md +158 -141
- package/package.json +2 -2
- package/skills/subagent/SKILL.md +78 -49
- package/src/backends/pi.ts +164 -94
- package/src/child-preflight.ts +166 -0
- package/src/config.ts +254 -252
- package/src/dispatch-preflight.ts +87 -0
- package/src/dispatch-routing.ts +56 -0
- package/src/extension.ts +366 -158
- package/src/format.ts +436 -365
- package/src/jev-router.ts +1036 -0
- package/src/orchestrator.ts +75 -19
- package/src/persistence.ts +643 -335
- package/src/policy.ts +120 -89
- package/src/process-lock.ts +730 -687
- package/src/protocol.ts +320 -290
- package/src/registry.ts +730 -632
- package/src/routing-policy.ts +268 -0
- package/src/routing-types.ts +217 -0
- package/src/runner.ts +1299 -850
- package/src/schema.ts +10 -10
- package/src/startup-check.ts +481 -0
- package/src/types.ts +208 -198
- package/src/usage.ts +316 -274
- package/src/model-policy.ts +0 -169
package/src/model-policy.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
|
-
import * as os from "node:os";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { isThinkingLevel, type ThinkingLevel } from "./thinking.js";
|
|
5
|
-
|
|
6
|
-
export const MODEL_POLICY_CONFIG_FILE = path.join(os.homedir(), ".pi", "subagent.json");
|
|
7
|
-
|
|
8
|
-
export interface ModelRoute {
|
|
9
|
-
model: string;
|
|
10
|
-
/** Immutable order owned by the user configuration. */
|
|
11
|
-
fallbackModels: readonly string[];
|
|
12
|
-
/** Optional route default; callers may override it with task/agent/profile settings. */
|
|
13
|
-
thinking?: ThinkingLevel;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ModelPolicySnapshot {
|
|
17
|
-
default: ModelRoute;
|
|
18
|
-
agents: ReadonlyMap<string, ModelRoute>;
|
|
19
|
-
source: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ModelPolicyValidation {
|
|
23
|
-
route?: ModelRoute;
|
|
24
|
-
error?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const AGENT_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
28
|
-
|
|
29
|
-
function invalid(source: string, message: string): never {
|
|
30
|
-
throw new Error(`Invalid modelPolicy in ${source}: ${message}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function modelId(value: unknown, pathName: string, source: string): string {
|
|
34
|
-
if (typeof value !== "string") invalid(source, `${pathName}.model must be a string in provider/model-id form`);
|
|
35
|
-
const model = value.trim();
|
|
36
|
-
const slash = model.indexOf("/");
|
|
37
|
-
if (!model || slash <= 0 || slash === model.length - 1 || /[\x00-\x1f\s]/.test(model)) {
|
|
38
|
-
invalid(source, `${pathName}.model must be a non-empty provider/model-id without whitespace`);
|
|
39
|
-
}
|
|
40
|
-
return model;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function route(value: unknown, pathName: string, source: string): ModelRoute {
|
|
44
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
45
|
-
invalid(source, `${pathName} must be an object`);
|
|
46
|
-
}
|
|
47
|
-
const record = value as Record<string, unknown>;
|
|
48
|
-
const unknown = Object.keys(record).filter((key) => key !== "model" && key !== "fallbackModels" && key !== "thinking");
|
|
49
|
-
if (unknown.length) invalid(source, `${pathName} has unknown field(s): ${unknown.join(", ")}`);
|
|
50
|
-
const model = modelId(record.model, pathName, source);
|
|
51
|
-
const rawFallbacks = record.fallbackModels === undefined ? [] : record.fallbackModels;
|
|
52
|
-
if (!Array.isArray(rawFallbacks)) invalid(source, `${pathName}.fallbackModels must be an array`);
|
|
53
|
-
const fallbackModels = rawFallbacks.map((value, index) => modelId(value, `${pathName}.fallbackModels[${index}]`, source));
|
|
54
|
-
if (new Set(fallbackModels).size !== fallbackModels.length) invalid(source, `${pathName}.fallbackModels must not contain duplicates`);
|
|
55
|
-
if (fallbackModels.includes(model)) invalid(source, `${pathName}.fallbackModels must not repeat the primary model`);
|
|
56
|
-
const thinking = record.thinking;
|
|
57
|
-
if (thinking !== undefined && !isThinkingLevel(thinking)) {
|
|
58
|
-
invalid(source, `${pathName}.thinking must be a non-empty Pi thinking level string without whitespace or control characters`);
|
|
59
|
-
}
|
|
60
|
-
return Object.freeze({
|
|
61
|
-
model,
|
|
62
|
-
fallbackModels: Object.freeze([...fallbackModels]),
|
|
63
|
-
...(thinking === undefined ? {} : { thinking }),
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Parse only the modelPolicy subtree; no provider catalog or credentials are read. */
|
|
68
|
-
export function parseModelPolicy(raw: unknown, source = MODEL_POLICY_CONFIG_FILE): ModelPolicySnapshot {
|
|
69
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
70
|
-
invalid(source, "modelPolicy must be an object");
|
|
71
|
-
}
|
|
72
|
-
const record = raw as Record<string, unknown>;
|
|
73
|
-
const unknown = Object.keys(record).filter((key) => key !== "default" && key !== "agents");
|
|
74
|
-
if (unknown.length) invalid(source, `modelPolicy has unknown field(s): ${unknown.join(", ")}`);
|
|
75
|
-
if (!("default" in record)) invalid(source, "modelPolicy.default is required");
|
|
76
|
-
const defaultRoute = route(record.default, "modelPolicy.default", source);
|
|
77
|
-
const agents = new Map<string, ModelRoute>();
|
|
78
|
-
const rawAgents = record.agents === undefined ? {} : record.agents;
|
|
79
|
-
if (!rawAgents || typeof rawAgents !== "object" || Array.isArray(rawAgents)) {
|
|
80
|
-
invalid(source, "modelPolicy.agents must be an object");
|
|
81
|
-
}
|
|
82
|
-
for (const [name, value] of Object.entries(rawAgents as Record<string, unknown>)) {
|
|
83
|
-
const normalized = name.trim().toLowerCase();
|
|
84
|
-
if (!AGENT_NAME.test(normalized)) invalid(source, `modelPolicy.agents key ${JSON.stringify(name)} is not a valid agent name`);
|
|
85
|
-
if (agents.has(normalized)) invalid(source, `modelPolicy.agents contains duplicate agent ${normalized}`);
|
|
86
|
-
agents.set(normalized, route(value, `modelPolicy.agents.${normalized}`, source));
|
|
87
|
-
}
|
|
88
|
-
return Object.freeze({ default: defaultRoute, agents, source });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Read the raw config only far enough to validate modelPolicy. */
|
|
92
|
-
export async function readModelPolicyFile(file = MODEL_POLICY_CONFIG_FILE): Promise<ModelPolicySnapshot> {
|
|
93
|
-
let raw: unknown;
|
|
94
|
-
try {
|
|
95
|
-
raw = JSON.parse(await fs.readFile(file, "utf8"));
|
|
96
|
-
} catch (error: any) {
|
|
97
|
-
if (error?.code === "ENOENT") throw new Error(`Model policy is not configured. Create ${file} using the model policy template.`);
|
|
98
|
-
throw new Error(`Could not read ${file} as JSON; model policy was not loaded.`);
|
|
99
|
-
}
|
|
100
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw) || !("modelPolicy" in (raw as Record<string, unknown>))) {
|
|
101
|
-
throw new Error(`Model policy is missing from ${file}. Add modelPolicy.default using the model policy template.`);
|
|
102
|
-
}
|
|
103
|
-
return parseModelPolicy((raw as Record<string, unknown>).modelPolicy, file);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function resolveModelRoute(policy: ModelPolicySnapshot, agent?: string): ModelRoute {
|
|
107
|
-
const name = agent?.trim().toLowerCase();
|
|
108
|
-
return (name && policy.agents.get(name)) || policy.default;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Validate the caller-owned fields against the immutable configured route. */
|
|
112
|
-
export function validateModelRequest(
|
|
113
|
-
policy: ModelPolicySnapshot,
|
|
114
|
-
options: { agent?: string; model?: string; fallbackModels?: string[]; fallbackModelsProvided?: boolean },
|
|
115
|
-
): ModelPolicyValidation {
|
|
116
|
-
const route = resolveModelRoute(policy, options.agent);
|
|
117
|
-
const actualModel = options.model?.trim();
|
|
118
|
-
if (!actualModel) return { error: `model is required; expected configured model ${route.model}` };
|
|
119
|
-
if (actualModel !== route.model) {
|
|
120
|
-
return { error: `model ${JSON.stringify(actualModel)} does not match the configured model ${JSON.stringify(route.model)}${options.agent ? ` for agent ${options.agent}` : ""}` };
|
|
121
|
-
}
|
|
122
|
-
if (options.fallbackModelsProvided) {
|
|
123
|
-
const requested = (options.fallbackModels ?? []).map((model) => model.trim());
|
|
124
|
-
if (requested.length !== route.fallbackModels.length || requested.some((model, index) => model !== route.fallbackModels[index])) {
|
|
125
|
-
return {
|
|
126
|
-
error: `fallback_models must exactly match the configured order for ${actualModel}: [${route.fallbackModels.join(", ")}]`,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return { route };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function formatModelPolicyPrompt(policy: ModelPolicySnapshot | undefined, error?: string): string {
|
|
134
|
-
if (!policy) {
|
|
135
|
-
return [
|
|
136
|
-
"## Subagent model policy",
|
|
137
|
-
error || `No valid model policy is configured at ${MODEL_POLICY_CONFIG_FILE}.`,
|
|
138
|
-
"Management actions (status/wait/cancel/steer/diff/apply/discard) remain available, but every new task/tasks[] spawn and plan request is rejected until modelPolicy is configured.",
|
|
139
|
-
"Use the package model-policy template; do not invent model IDs or fallback models.",
|
|
140
|
-
].join("\n");
|
|
141
|
-
}
|
|
142
|
-
const lines = [
|
|
143
|
-
"## Subagent model policy",
|
|
144
|
-
"Every new task/tasks[] spawn must pass model explicitly and it must exactly match this mapping. Management actions do not need model.",
|
|
145
|
-
`default (agentless and unmapped agents): model=${policy.default.model}; fallback_models=[${policy.default.fallbackModels.join(", ")}]; thinking=${policy.default.thinking ?? "(unset)"}`,
|
|
146
|
-
];
|
|
147
|
-
for (const [agent, route] of [...policy.agents.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
148
|
-
lines.push(`agent:${agent}: model=${route.model}; fallback_models=[${route.fallbackModels.join(", ")}]; thinking=${route.thinking ?? "(unset)"}`);
|
|
149
|
-
}
|
|
150
|
-
lines.push(
|
|
151
|
-
"If fallback_models is omitted, the configured list is used. If it is supplied, it must match the configured list exactly, including order.",
|
|
152
|
-
"thinking is a route default: explicit task thinking, agent thinking, and profile taskDefaults.thinking override it; parent thinking is used only when the route is unset.",
|
|
153
|
-
"Do not use model/fallback_models from agent frontmatter, taskDefaults, or the parent session; those legacy model fields are ignored.",
|
|
154
|
-
);
|
|
155
|
-
return lines.join("\n");
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function modelPolicyTemplate(): string {
|
|
159
|
-
return JSON.stringify({
|
|
160
|
-
modelPolicy: {
|
|
161
|
-
default: { model: "<provider/model-id>", fallbackModels: [], thinking: "medium" },
|
|
162
|
-
agents: { "<agent-name>": { model: "<provider/model-id>", fallbackModels: [], thinking: "high" } },
|
|
163
|
-
},
|
|
164
|
-
}, null, 2);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function modelPolicySource(file = MODEL_POLICY_CONFIG_FILE): string {
|
|
168
|
-
return path.normalize(file);
|
|
169
|
-
}
|