@fiale-plus/pi-rogue 0.2.3 → 0.2.4
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/node_modules/@fiale-plus/pi-rogue-advisor/README.md +1 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.test.ts +8 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.ts +7 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +26 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +10 -1
- package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +3 -3
- package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +3 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +3 -2
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +65 -2
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +84 -4
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +3 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +43 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +96 -11
- package/node_modules/@fiale-plus/pi-rogue-router/README.md +45 -6
- package/node_modules/@fiale-plus/pi-rogue-router/src/binary-gate.test.ts +88 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/binary-gate.ts +232 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/cli.ts +123 -9
- package/node_modules/@fiale-plus/pi-rogue-router/src/completions.ts +39 -16
- package/node_modules/@fiale-plus/pi-rogue-router/src/config-extension.test.ts +111 -4
- package/node_modules/@fiale-plus/pi-rogue-router/src/config.ts +17 -2
- package/node_modules/@fiale-plus/pi-rogue-router/src/extension.ts +67 -7
- package/node_modules/@fiale-plus/pi-rogue-router/src/index.ts +4 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/observe.ts +76 -5
- package/node_modules/@fiale-plus/pi-rogue-router/src/outcomes.ts +130 -6
- package/node_modules/@fiale-plus/pi-rogue-router/src/reports.test.ts +92 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/reports.ts +116 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/sharpening.test.ts +223 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/sharpening.ts +344 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/teacher-runner.test.ts +126 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/teacher-runner.ts +238 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/v1-telemetry.test.ts +54 -1
- package/package.json +1 -1
|
@@ -1,34 +1,57 @@
|
|
|
1
|
-
import { DEFAULT_ROUTER_CONFIG, loadRouterConfig } from "./config.js";
|
|
1
|
+
import { DEFAULT_ROUTER_CONFIG, formatProfile, loadRouterConfig } from "./config.js";
|
|
2
2
|
|
|
3
3
|
type CompletionItem = { value: string; label: string; description?: string };
|
|
4
4
|
|
|
5
|
-
function item(value: string, description?: string): CompletionItem {
|
|
6
|
-
return { value, label
|
|
5
|
+
function item(value: string, description?: string, label = value.trimEnd()): CompletionItem {
|
|
6
|
+
return { value, label, ...(description ? { description } : {}) };
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
function filter(items: CompletionItem[], prefix: string): CompletionItem[] | null {
|
|
10
10
|
const q = prefix.trimStart().toLowerCase();
|
|
11
|
-
const out = q ? items.filter((entry) => entry.value.toLowerCase().startsWith(q)) : items;
|
|
11
|
+
const out = q ? items.filter((entry) => entry.value.toLowerCase().startsWith(q) || entry.label.toLowerCase().startsWith(q)) : items;
|
|
12
12
|
return out.length ? out : null;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function topLevelItems(): CompletionItem[] {
|
|
16
|
+
return [
|
|
17
|
+
item("status", "show current router state"),
|
|
18
|
+
item("help", "show command tree and safety notes"),
|
|
19
|
+
item("on", "enable router using the current explicit mode"),
|
|
20
|
+
item("off", "disable router"),
|
|
21
|
+
item("mode ", "choose observe or auto_model", "mode …"),
|
|
22
|
+
item("profile ", "choose active router profile", "profile …"),
|
|
23
|
+
item("print ", "choose router notification verbosity", "print …"),
|
|
24
|
+
item("models", "show active role → model mapping"),
|
|
25
|
+
item("profiles", "list all configured profiles"),
|
|
26
|
+
item("cycle", "cycle to the next router profile"),
|
|
27
|
+
item("configure", "create/show config and suggested next commands"),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
export function routerArgumentCompletions(prefix: string, ctx?: any): CompletionItem[] | null {
|
|
16
32
|
const trimmed = prefix.trimStart();
|
|
17
33
|
const [cmd, rest = ""] = trimmed.split(/\s+/, 2);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
item("off", "disable router summaries"),
|
|
21
|
-
item("status", "show router state and active profile"),
|
|
22
|
-
item("profile", "show or set active router profile"),
|
|
23
|
-
item("profiles", "list router profiles"),
|
|
24
|
-
item("models", "show active role to model mapping"),
|
|
25
|
-
item("configure", "write default local config if missing"),
|
|
26
|
-
item("cycle", "cycle to the next router profile"),
|
|
27
|
-
];
|
|
28
|
-
if (!cmd || !trimmed.includes(" ")) return filter(top, trimmed);
|
|
34
|
+
if (!cmd || !trimmed.includes(" ")) return filter(topLevelItems(), trimmed);
|
|
35
|
+
|
|
29
36
|
if (cmd === "profile") {
|
|
30
37
|
const config = ctx ? loadRouterConfig(ctx) : DEFAULT_ROUTER_CONFIG;
|
|
31
|
-
return filter(config.profileOrder.map((name) =>
|
|
38
|
+
return filter(config.profileOrder.map((name) => {
|
|
39
|
+
const marker = name === config.activeProfile ? "active" : "profile";
|
|
40
|
+
return item(`profile ${name}`, `${marker}: ${formatProfile(name, config.profiles[name])}`, name);
|
|
41
|
+
}), `profile ${rest}`);
|
|
42
|
+
}
|
|
43
|
+
if (cmd === "mode") {
|
|
44
|
+
return filter([
|
|
45
|
+
item("mode observe", "recommendations only", "observe"),
|
|
46
|
+
item("mode auto_model", "apply model switches only", "auto_model"),
|
|
47
|
+
], `mode ${rest}`);
|
|
48
|
+
}
|
|
49
|
+
if (cmd === "print") {
|
|
50
|
+
return filter([
|
|
51
|
+
item("print mismatch_only", "notify only route/model mismatches", "mismatch_only"),
|
|
52
|
+
item("print all", "notify every router decision", "all"),
|
|
53
|
+
item("print off", "suppress observe notifications", "off"),
|
|
54
|
+
], `print ${rest}`);
|
|
32
55
|
}
|
|
33
56
|
return null;
|
|
34
57
|
}
|
|
@@ -3,10 +3,10 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
5
5
|
import { routerArgumentCompletions } from "./completions.js";
|
|
6
|
-
import { activeProfile, cycleRouterProfile, ensureRouterConfig, loadRouterConfig, routerConfigPath, routerEventsPath, routerSessionDir, routerStatePath, saveRouterConfig, setRouterProfile } from "./config.js";
|
|
6
|
+
import { activeProfile, cycleRouterProfile, ensureRouterConfig, loadRouterConfig, routerConfigPath, routerEventsPath, routerSessionDir, routerStatePath, saveRouterConfig, setRouterMode, setRouterPrint, setRouterProfile } from "./config.js";
|
|
7
7
|
import { registerRouter } from "./extension.js";
|
|
8
8
|
import { decideRoute } from "./decision.js";
|
|
9
|
-
import { observeRouterTurn, summarizeRouterDecision } from "./observe.js";
|
|
9
|
+
import { applyModelRouting, modelsMatch, observeRouterTurn, summarizeRouterDecision } from "./observe.js";
|
|
10
10
|
import type { RouterCheckpoint } from "./types.js";
|
|
11
11
|
|
|
12
12
|
function ctxMock(sessionPath?: string) {
|
|
@@ -37,7 +37,10 @@ function piMock() {
|
|
|
37
37
|
const commands = new Map<string, any>();
|
|
38
38
|
const shortcuts = new Map<string, any>();
|
|
39
39
|
const handlers = new Map<string, any[]>();
|
|
40
|
+
const selectedModels: any[] = [];
|
|
40
41
|
const pi: any = {
|
|
42
|
+
selectedModels,
|
|
43
|
+
async setModel(model: any) { selectedModels.push(model); return true; },
|
|
41
44
|
registerCommand(name: string, options: any) { commands.set(name, options); },
|
|
42
45
|
registerShortcut(key: string, options: any) { shortcuts.set(key, options); },
|
|
43
46
|
on(name: string, handler: any) { handlers.set(name, [...(handlers.get(name) ?? []), handler]); },
|
|
@@ -91,6 +94,7 @@ describe("router config profiles", () => {
|
|
|
91
94
|
|
|
92
95
|
expect(config.activeProfile).toBe("all-smart");
|
|
93
96
|
expect(config.profileOrder).toEqual(["all-smart", "spark-smart", "local-smart"]);
|
|
97
|
+
expect(config.mode).toBe("observe");
|
|
94
98
|
expect(activeProfile(config).worker).toBe("openai-codex/gpt-5.5");
|
|
95
99
|
expect(readFileSync(routerConfigPath(ctx), "utf8")).toContain("spark-smart");
|
|
96
100
|
});
|
|
@@ -102,11 +106,21 @@ describe("router config profiles", () => {
|
|
|
102
106
|
expect(spark?.activeProfile).toBe("spark-smart");
|
|
103
107
|
expect(cycleRouterProfile(spark!, 1).activeProfile).toBe("local-smart");
|
|
104
108
|
expect(setRouterProfile(config, "missing")).toBeNull();
|
|
109
|
+
expect(setRouterMode(config, "auto")?.mode).toBe("auto_model");
|
|
110
|
+
expect(setRouterMode(config, "auto_model")?.mode).toBe("auto_model");
|
|
111
|
+
expect(setRouterMode(config, "agent-auto")).toBeNull();
|
|
112
|
+
expect(setRouterPrint(config, "all")?.print).toBe("all");
|
|
113
|
+
expect(setRouterPrint(config, "noisy")).toBeNull();
|
|
105
114
|
});
|
|
106
115
|
|
|
107
|
-
it("completes router commands
|
|
108
|
-
|
|
116
|
+
it("completes router commands as a nested slash-menu tree", () => {
|
|
117
|
+
const top = routerArgumentCompletions("") ?? [];
|
|
118
|
+
|
|
119
|
+
expect(top.map((item) => item.value)).toEqual(["status", "help", "on", "off", "mode ", "profile ", "print ", "models", "profiles", "cycle", "configure"]);
|
|
120
|
+
expect(top.find((item) => item.value === "mode ")?.label).toBe("mode …");
|
|
109
121
|
expect(routerArgumentCompletions("profile s")?.map((item) => item.value)).toEqual(["profile spark-smart"]);
|
|
122
|
+
expect(routerArgumentCompletions("mode a")?.map((item) => item.value)).toEqual(["mode auto_model"]);
|
|
123
|
+
expect(routerArgumentCompletions("print ")?.map((item) => item.value)).toEqual(["print mismatch_only", "print all", "print off"]);
|
|
110
124
|
});
|
|
111
125
|
|
|
112
126
|
it("keeps config repo-global while state and live events are session-scoped", async () => {
|
|
@@ -149,6 +163,18 @@ describe("router extension", () => {
|
|
|
149
163
|
await commands.get("router").handler("profile spark-smart", ctx);
|
|
150
164
|
expect(loadRouterConfig(ctx).activeProfile).toBe("spark-smart");
|
|
151
165
|
|
|
166
|
+
await commands.get("router").handler("mode auto_model", ctx);
|
|
167
|
+
expect(loadRouterConfig(ctx).mode).toBe("auto_model");
|
|
168
|
+
await commands.get("router").handler("print all", ctx);
|
|
169
|
+
expect(loadRouterConfig(ctx).print).toBe("all");
|
|
170
|
+
await commands.get("router").handler("help", ctx);
|
|
171
|
+
expect(ctx.notifications.at(-1)?.text).toContain("router command tree:");
|
|
172
|
+
await commands.get("router").handler("off", ctx);
|
|
173
|
+
await commands.get("router").handler("on", ctx);
|
|
174
|
+
expect(ctx.notifications.at(-1)?.text).toContain("auto_model applies model switches only");
|
|
175
|
+
await commands.get("router").handler("status", ctx);
|
|
176
|
+
expect(ctx.notifications.at(-1)?.text).toContain("model routing: auto_model");
|
|
177
|
+
|
|
152
178
|
await shortcuts.get("ctrl+alt+p").handler(ctx);
|
|
153
179
|
expect(loadRouterConfig(ctx).activeProfile).toBe("local-smart");
|
|
154
180
|
});
|
|
@@ -162,4 +188,85 @@ describe("router extension", () => {
|
|
|
162
188
|
expect(summary.text).toContain("smart(openai-codex/gpt-5.5)");
|
|
163
189
|
expect(summary.text).toContain("current=gpt-5.3-codex-spark");
|
|
164
190
|
});
|
|
191
|
+
|
|
192
|
+
it("auto_model applies only model switches for explicit target mismatches", async () => {
|
|
193
|
+
const { pi } = piMock();
|
|
194
|
+
const ctx = {
|
|
195
|
+
...ctxMock(),
|
|
196
|
+
modelRegistry: {
|
|
197
|
+
find: (provider: string, id: string) => provider === "openai-codex" && id === "gpt-5.5" ? { provider, id } : undefined,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
const config = { ...loadRouterConfig(ctx), enabled: true, mode: "auto_model" as const, activeProfile: "spark-smart" };
|
|
201
|
+
const item = checkpoint();
|
|
202
|
+
const summary = summarizeRouterDecision(item, decideRoute(item), config);
|
|
203
|
+
|
|
204
|
+
const applied = await applyModelRouting(pi, ctx, summary);
|
|
205
|
+
|
|
206
|
+
expect(applied).toMatchObject({ applied: true, fromModel: "gpt-5.3-codex-spark", toModel: "openai-codex/gpt-5.5" });
|
|
207
|
+
expect(pi.selectedModels).toEqual([{ provider: "openai-codex", id: "gpt-5.5" }]);
|
|
208
|
+
|
|
209
|
+
const none = await applyModelRouting(pi, ctx, { ...summary, role: "none", targetModel: undefined, match: null });
|
|
210
|
+
expect(none.applied).toBe(false);
|
|
211
|
+
expect(pi.selectedModels).toHaveLength(1);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("does not treat provider-qualified target as matched when only leaf model id matches", async () => {
|
|
215
|
+
const { pi } = piMock();
|
|
216
|
+
const ctx = {
|
|
217
|
+
...ctxMock(),
|
|
218
|
+
modelRegistry: {
|
|
219
|
+
find: (provider: string, id: string) => provider === "openai-codex" && id === "gpt-5.5" ? { provider, id } : undefined,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
const config = { ...loadRouterConfig(ctx), enabled: true, mode: "auto_model" as const, activeProfile: "spark-smart" };
|
|
223
|
+
const item = checkpoint({ activeModel: "gpt-5.5", provider: "custom" });
|
|
224
|
+
const summary = summarizeRouterDecision(item, decideRoute(item), config);
|
|
225
|
+
const qualifiedWithoutProvider = summarizeRouterDecision(checkpoint({ activeModel: "custom/gpt-5.5", provider: undefined }), decideRoute(item), config);
|
|
226
|
+
const leafWithoutProvider = summarizeRouterDecision(checkpoint({ activeModel: "gpt-5.5", provider: undefined }), decideRoute(item), config);
|
|
227
|
+
|
|
228
|
+
expect(summary.match).toBe(false);
|
|
229
|
+
expect(qualifiedWithoutProvider.match).toBe(false);
|
|
230
|
+
expect(leafWithoutProvider.match).toBe(false);
|
|
231
|
+
expect(modelsMatch("zai/kimi-k2.6", "openrouter/moonshotai/kimi-k2.6", "openrouter")).toBe(false);
|
|
232
|
+
expect(modelsMatch("moonshotai/kimi-k2.6", "openrouter/moonshotai/kimi-k2.6", "openrouter")).toBe(true);
|
|
233
|
+
const applied = await applyModelRouting(pi, ctx, summary);
|
|
234
|
+
|
|
235
|
+
expect(applied.applied).toBe(true);
|
|
236
|
+
expect(pi.selectedModels).toEqual([{ provider: "openai-codex", id: "gpt-5.5" }]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("resolves bare slash-containing model ids from the registry", async () => {
|
|
240
|
+
const { pi } = piMock();
|
|
241
|
+
const ctx = {
|
|
242
|
+
...ctxMock(),
|
|
243
|
+
modelRegistry: {
|
|
244
|
+
getAll: () => [{ provider: "openrouter", id: "moonshotai/kimi-k2.6" }],
|
|
245
|
+
find: (provider: string, id: string) => ({ provider, id }),
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const applied = await applyModelRouting(pi, ctx, { checkpointId: "c", action: "ask_micro_hint", role: "smart", currentModel: "qwen", currentProvider: "openrouter", targetModel: "moonshotai/kimi-k2.6", match: false, confidence: 0.8, reason: "test", text: "test" });
|
|
250
|
+
const skipped = await applyModelRouting(pi, ctx, { checkpointId: "c", action: "ask_micro_hint", role: "smart", currentModel: "moonshotai/kimi-k2.6", currentProvider: "openrouter", targetModel: "moonshotai/kimi-k2.6", match: false, confidence: 0.8, reason: "test", text: "test" });
|
|
251
|
+
const duplicateProviderCtx = {
|
|
252
|
+
...ctxMock(),
|
|
253
|
+
modelRegistry: {
|
|
254
|
+
getAll: () => [{ provider: "first", id: "same-model" }, { provider: "current", id: "same-model" }],
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
const duplicateSkipped = await applyModelRouting(pi, duplicateProviderCtx, { checkpointId: "c", action: "ask_micro_hint", role: "smart", currentModel: "same-model", currentProvider: "current", targetModel: "same-model", match: true, confidence: 0.8, reason: "test", text: "test" });
|
|
258
|
+
|
|
259
|
+
const ambiguousCtx = {
|
|
260
|
+
...ctxMock(),
|
|
261
|
+
modelRegistry: { getAll: () => [{ provider: "first", id: "ambiguous" }, { provider: "second", id: "ambiguous" }] },
|
|
262
|
+
};
|
|
263
|
+
const ambiguous = await applyModelRouting(pi, ambiguousCtx, { checkpointId: "c", action: "ask_micro_hint", role: "smart", currentModel: "other", targetModel: "ambiguous", match: false, confidence: 0.8, reason: "test", text: "test" });
|
|
264
|
+
|
|
265
|
+
expect(applied.applied).toBe(true);
|
|
266
|
+
expect(skipped.applied).toBe(false);
|
|
267
|
+
expect(duplicateSkipped.applied).toBe(false);
|
|
268
|
+
expect(ambiguous.applied).toBe(false);
|
|
269
|
+
expect(ambiguous.reason).toContain("target model not configured");
|
|
270
|
+
expect(pi.selectedModels).toEqual([{ provider: "openrouter", id: "moonshotai/kimi-k2.6" }]);
|
|
271
|
+
});
|
|
165
272
|
});
|
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { hashText } from "./hash.js";
|
|
4
4
|
|
|
5
|
-
export type RouterMode = "observe";
|
|
5
|
+
export type RouterMode = "observe" | "auto_model";
|
|
6
6
|
export type RouterPrintMode = "all" | "mismatch_only" | "off";
|
|
7
7
|
|
|
8
8
|
export interface RouterProfile {
|
|
@@ -124,6 +124,10 @@ function readJson<T>(path: string, fallback: T): T {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function normalizeRouterMode(value: unknown): RouterMode {
|
|
128
|
+
return value === "auto_model" || value === "auto" ? "auto_model" : "observe";
|
|
129
|
+
}
|
|
130
|
+
|
|
127
131
|
export function normalizeRouterConfig(raw: Partial<RouterConfig> | null | undefined): RouterConfig {
|
|
128
132
|
const mergedProfiles = { ...DEFAULT_ROUTER_CONFIG.profiles, ...(raw?.profiles ?? {}) };
|
|
129
133
|
const profileOrder = Array.isArray(raw?.profileOrder) && raw.profileOrder.length > 0
|
|
@@ -135,7 +139,7 @@ export function normalizeRouterConfig(raw: Partial<RouterConfig> | null | undefi
|
|
|
135
139
|
const print = raw?.print === "all" || raw?.print === "off" || raw?.print === "mismatch_only" ? raw.print : DEFAULT_ROUTER_CONFIG.print;
|
|
136
140
|
return {
|
|
137
141
|
enabled: Boolean(raw?.enabled ?? DEFAULT_ROUTER_CONFIG.enabled),
|
|
138
|
-
mode:
|
|
142
|
+
mode: normalizeRouterMode(raw?.mode),
|
|
139
143
|
print,
|
|
140
144
|
activeProfile,
|
|
141
145
|
profileOrder,
|
|
@@ -187,6 +191,17 @@ export function setRouterProfile(config: RouterConfig, name: string): RouterConf
|
|
|
187
191
|
return { ...config, activeProfile: name, profileOrder: config.profileOrder.includes(name) ? config.profileOrder : [...config.profileOrder, name] };
|
|
188
192
|
}
|
|
189
193
|
|
|
194
|
+
export function setRouterMode(config: RouterConfig, mode: string): RouterConfig | null {
|
|
195
|
+
if (mode === "observe") return { ...config, mode: "observe" };
|
|
196
|
+
if (mode === "auto" || mode === "auto_model") return { ...config, mode: "auto_model" };
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function setRouterPrint(config: RouterConfig, print: string): RouterConfig | null {
|
|
201
|
+
if (print === "all" || print === "mismatch_only" || print === "off") return { ...config, print };
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
190
205
|
export function formatProfile(name: string, profile: RouterProfile): string {
|
|
191
206
|
const subagents = [`explore=${profile.explore ?? profile.worker}`, `debug=${profile.debug_diagnose ?? profile.smart}`, `review=${profile.review ?? profile.reviewer}`, `verify=${profile.verify ?? profile.worker}`].join(" ");
|
|
192
207
|
return `${name}: worker=${profile.worker} smart=${profile.smart} teacher=${profile.teacher} reviewer=${profile.reviewer} ${subagents}`;
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
routerConfigPath,
|
|
9
9
|
routerEventsPath,
|
|
10
10
|
saveRouterConfig,
|
|
11
|
+
setRouterMode,
|
|
12
|
+
setRouterPrint,
|
|
11
13
|
setRouterProfile,
|
|
12
14
|
type RouterConfig,
|
|
13
15
|
} from "./config.js";
|
|
@@ -17,8 +19,8 @@ import { routerArgumentCompletions } from "./completions.js";
|
|
|
17
19
|
function statusText(ctx: any, config: RouterConfig): string {
|
|
18
20
|
const profile = activeProfile(config);
|
|
19
21
|
return [
|
|
20
|
-
`router: ${config.enabled ? "
|
|
21
|
-
`
|
|
22
|
+
`router: ${config.enabled ? "on" : "off"}`,
|
|
23
|
+
`model routing: ${config.mode === "auto_model" ? "auto_model (applies model switches only)" : "observe (recommendations only)"}`,
|
|
22
24
|
`print: ${config.print}`,
|
|
23
25
|
`profile: ${config.activeProfile}`,
|
|
24
26
|
`worker: ${profile.worker}`,
|
|
@@ -35,11 +37,35 @@ function notifyProfile(ctx: any, config: RouterConfig, prefix = "router profile"
|
|
|
35
37
|
ctx.ui.notify(`${prefix}: ${config.activeProfile}\nworker: ${profile.worker}\nsmart: ${profile.smart}\nteacher: ${profile.teacher}\nreviewer: ${profile.reviewer}`, "info");
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
function helpText(ctx: any, config: RouterConfig): string {
|
|
41
|
+
return [
|
|
42
|
+
"router command tree:",
|
|
43
|
+
" /router status show current router state",
|
|
44
|
+
" /router help show this help",
|
|
45
|
+
" /router on enable router using current explicit mode",
|
|
46
|
+
" /router off disable router",
|
|
47
|
+
" /router mode observe recommendations only",
|
|
48
|
+
" /router mode auto_model apply model switches only",
|
|
49
|
+
" /router profile <name> choose active profile",
|
|
50
|
+
" /router print mismatch_only notify only mismatches",
|
|
51
|
+
" /router print all notify every router decision",
|
|
52
|
+
" /router print off suppress observe notifications",
|
|
53
|
+
" /router models show active role → model mapping",
|
|
54
|
+
" /router profiles list configured profiles",
|
|
55
|
+
" /router cycle cycle to next profile",
|
|
56
|
+
" /router configure create/show config",
|
|
57
|
+
"",
|
|
58
|
+
"safety: observe is recommendations only; auto_model applies model switches only, never agent/subagent/tool routing.",
|
|
59
|
+
"",
|
|
60
|
+
statusText(ctx, config),
|
|
61
|
+
].join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
38
64
|
function setEnabled(ctx: any, enabled: boolean): void {
|
|
39
65
|
const config = ensureRouterConfig(ctx);
|
|
40
66
|
const next = { ...config, enabled };
|
|
41
67
|
saveRouterConfig(ctx, next);
|
|
42
|
-
ctx.ui.notify(enabled ?
|
|
68
|
+
ctx.ui.notify(enabled ? `router enabled: ${next.mode === "auto_model" ? "auto_model applies model switches only" : "observe recommendations only"}` : "router disabled", "info");
|
|
43
69
|
}
|
|
44
70
|
|
|
45
71
|
export function registerRouter(pi: ExtensionAPI): void {
|
|
@@ -48,7 +74,7 @@ export function registerRouter(pi: ExtensionAPI): void {
|
|
|
48
74
|
p.__piRogueRouterRegistered = true;
|
|
49
75
|
|
|
50
76
|
pi.registerCommand("router", {
|
|
51
|
-
description: "
|
|
77
|
+
description: "Trajectory router. Usage: /router status|help|on|off|mode|profile|print|profiles|models|configure|cycle. Default observe-only; auto_model applies model switches only.",
|
|
52
78
|
getArgumentCompletions: (prefix: string, ctx?: any) => routerArgumentCompletions(prefix, ctx),
|
|
53
79
|
handler: async (args, ctx) => {
|
|
54
80
|
const input = String(args ?? "").trim();
|
|
@@ -65,11 +91,15 @@ export function registerRouter(pi: ExtensionAPI): void {
|
|
|
65
91
|
}
|
|
66
92
|
if (cmd === "configure" || cmd === "config") {
|
|
67
93
|
const config = ensureRouterConfig(ctx);
|
|
68
|
-
ctx.ui.notify(["router config ready", "", statusText(ctx, config)].join("\n"), "info");
|
|
94
|
+
ctx.ui.notify(["router config ready", "", "next: /router mode …, /router profile …, /router print …", "", statusText(ctx, config)].join("\n"), "info");
|
|
69
95
|
return;
|
|
70
96
|
}
|
|
71
97
|
|
|
72
98
|
const config = ensureRouterConfig(ctx);
|
|
99
|
+
if (cmd === "help") {
|
|
100
|
+
ctx.ui.notify(helpText(ctx, config), "info");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
73
103
|
if (cmd === "status" || cmd === "show") {
|
|
74
104
|
ctx.ui.notify(statusText(ctx, config), "info");
|
|
75
105
|
return;
|
|
@@ -78,6 +108,36 @@ export function registerRouter(pi: ExtensionAPI): void {
|
|
|
78
108
|
notifyProfile(ctx, config, "router models");
|
|
79
109
|
return;
|
|
80
110
|
}
|
|
111
|
+
if (cmd === "mode") {
|
|
112
|
+
const mode = rest[0];
|
|
113
|
+
if (!mode) {
|
|
114
|
+
ctx.ui.notify(statusText(ctx, config), "info");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const next = setRouterMode(config, mode);
|
|
118
|
+
if (!next) {
|
|
119
|
+
ctx.ui.notify("unknown router mode: use observe or auto_model", "error");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
saveRouterConfig(ctx, next);
|
|
123
|
+
ctx.ui.notify(`router model routing mode set: ${next.mode === "auto_model" ? "auto_model (model switches only)" : "observe (recommendations only)"}`, "info");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (cmd === "print") {
|
|
127
|
+
const print = rest[0];
|
|
128
|
+
if (!print) {
|
|
129
|
+
ctx.ui.notify(statusText(ctx, config), "info");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const next = setRouterPrint(config, print);
|
|
133
|
+
if (!next) {
|
|
134
|
+
ctx.ui.notify("unknown router print mode: use mismatch_only, all, or off", "error");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
saveRouterConfig(ctx, next);
|
|
138
|
+
ctx.ui.notify(`router print mode set: ${next.print}`, "info");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
81
141
|
if (cmd === "profiles") {
|
|
82
142
|
ctx.ui.notify(config.profileOrder.map((name) => {
|
|
83
143
|
const marker = name === config.activeProfile ? "*" : " ";
|
|
@@ -108,7 +168,7 @@ export function registerRouter(pi: ExtensionAPI): void {
|
|
|
108
168
|
return;
|
|
109
169
|
}
|
|
110
170
|
|
|
111
|
-
ctx.ui.notify("Usage: /router on|off|
|
|
171
|
+
ctx.ui.notify("Usage: /router status|help|on|off|mode [observe|auto_model]|profile [name]|print [mismatch_only|all|off]|profiles|models|configure|cycle", "error");
|
|
112
172
|
},
|
|
113
173
|
});
|
|
114
174
|
|
|
@@ -127,7 +187,7 @@ export function registerRouter(pi: ExtensionAPI): void {
|
|
|
127
187
|
|
|
128
188
|
pi.on("turn_end", async (_event: any, ctx: any) => {
|
|
129
189
|
try {
|
|
130
|
-
await observeRouterTurn(ctx);
|
|
190
|
+
await observeRouterTurn(ctx, pi);
|
|
131
191
|
} catch (error) {
|
|
132
192
|
ctx.ui?.notify?.(`router observe failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
133
193
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from "./binary-gate.js";
|
|
1
2
|
export * from "./checkpoints.js";
|
|
2
3
|
export * from "./completions.js";
|
|
3
4
|
export * from "./config.js";
|
|
@@ -10,6 +11,9 @@ export * from "./ledger.js";
|
|
|
10
11
|
export * from "./observe.js";
|
|
11
12
|
export * from "./outcomes.js";
|
|
12
13
|
export * from "./progress.js";
|
|
14
|
+
export * from "./reports.js";
|
|
15
|
+
export * from "./sharpening.js";
|
|
13
16
|
export * from "./session-reader.js";
|
|
14
17
|
export * from "./subagents.js";
|
|
18
|
+
export * from "./teacher-runner.js";
|
|
15
19
|
export * from "./types.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import { appendRouteEvent, buildRouteEvent } from "./ledger.js";
|
|
2
3
|
import { decideRoute } from "./decision.js";
|
|
3
4
|
import { checkpointWithDiffStats, streamCheckpointsFromSessionPath } from "./checkpoints.js";
|
|
@@ -21,12 +22,20 @@ export interface RouterObserveSummary {
|
|
|
21
22
|
role: keyof RouterProfile | "none" | "current";
|
|
22
23
|
targetModel?: string;
|
|
23
24
|
currentModel?: string;
|
|
25
|
+
currentProvider?: string;
|
|
24
26
|
match: boolean | null;
|
|
25
27
|
confidence: number;
|
|
26
28
|
reason: string;
|
|
27
29
|
text: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
export interface RouterModelApplySummary {
|
|
33
|
+
applied: boolean;
|
|
34
|
+
reason: string;
|
|
35
|
+
fromModel?: string;
|
|
36
|
+
toModel?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
function squish(text: unknown, max = 140): string {
|
|
31
40
|
const value = String(text ?? "").replace(/\s+/g, " ").trim();
|
|
32
41
|
return value.length <= max ? value : `${value.slice(0, max - 1).trimEnd()}…`;
|
|
@@ -53,10 +62,22 @@ function modelLeaf(model: string): string {
|
|
|
53
62
|
return model.split("/").at(-1)?.toLowerCase() ?? model.toLowerCase();
|
|
54
63
|
}
|
|
55
64
|
|
|
56
|
-
export function modelsMatch(current: string | undefined, target: string | undefined): boolean | null {
|
|
65
|
+
export function modelsMatch(current: string | undefined, target: string | undefined, currentProvider?: string): boolean | null {
|
|
57
66
|
if (!current || !target) return null;
|
|
58
67
|
const c = current.toLowerCase();
|
|
59
68
|
const t = target.toLowerCase();
|
|
69
|
+
const provider = currentProvider?.toLowerCase();
|
|
70
|
+
const [targetProvider, ...targetModelParts] = t.split("/");
|
|
71
|
+
if (targetModelParts.length > 0) {
|
|
72
|
+
const targetModel = targetModelParts.join("/");
|
|
73
|
+
if (provider) {
|
|
74
|
+
const currentModel = c.startsWith(`${provider}/`) ? c.slice(provider.length + 1) : c;
|
|
75
|
+
if (provider === targetProvider) return currentModel === targetModel;
|
|
76
|
+
return currentModel === t;
|
|
77
|
+
}
|
|
78
|
+
if (c.includes("/")) return c === t;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
60
81
|
return c === t || modelLeaf(c) === modelLeaf(t) || c.endsWith(`/${modelLeaf(t)}`) || t.endsWith(`/${modelLeaf(c)}`);
|
|
61
82
|
}
|
|
62
83
|
|
|
@@ -70,7 +91,7 @@ export function summarizeRouterDecision(checkpoint: RouterCheckpoint, decision:
|
|
|
70
91
|
const profile = activeProfile(config);
|
|
71
92
|
const role = actionRole(decision.action);
|
|
72
93
|
const targetModel = targetForRole(role, profile, checkpoint.activeModel);
|
|
73
|
-
const match = role === "none" ? null : modelsMatch(checkpoint.activeModel, targetModel);
|
|
94
|
+
const match = role === "none" ? null : modelsMatch(checkpoint.activeModel, targetModel, checkpoint.provider);
|
|
74
95
|
const verdict = match === null ? "INFO" : match ? "MATCH" : "MISMATCH";
|
|
75
96
|
const roleText = role === "none" ? "no-model" : role;
|
|
76
97
|
const targetText = targetModel ? `${roleText}(${targetModel})` : roleText;
|
|
@@ -81,6 +102,7 @@ export function summarizeRouterDecision(checkpoint: RouterCheckpoint, decision:
|
|
|
81
102
|
role,
|
|
82
103
|
targetModel,
|
|
83
104
|
currentModel: checkpoint.activeModel,
|
|
105
|
+
currentProvider: checkpoint.provider,
|
|
84
106
|
match,
|
|
85
107
|
confidence: decision.confidence,
|
|
86
108
|
reason: decision.reason,
|
|
@@ -94,9 +116,51 @@ export async function latestCheckpointFromSession(sessionPath: string): Promise<
|
|
|
94
116
|
return latest;
|
|
95
117
|
}
|
|
96
118
|
|
|
97
|
-
|
|
119
|
+
function findConfiguredModel(ctx: any, target: string, currentProvider?: string): { model: any; matchedBy: "qualified" | "id" } | undefined {
|
|
120
|
+
const all = ctx?.modelRegistry?.getAll?.() ?? [];
|
|
121
|
+
const observedProvider = currentProvider?.toLowerCase();
|
|
122
|
+
const byCurrentProviderId = observedProvider ? all.find((model: any) => model.id === target && String(model.provider).toLowerCase() === observedProvider) : undefined;
|
|
123
|
+
if (byCurrentProviderId) return { model: byCurrentProviderId, matchedBy: "id" };
|
|
124
|
+
const [provider, ...modelParts] = target.split("/");
|
|
125
|
+
if (modelParts.length > 0) {
|
|
126
|
+
const found = ctx?.modelRegistry?.find?.(provider, modelParts.join("/"));
|
|
127
|
+
if (found) return { model: found, matchedBy: "qualified" };
|
|
128
|
+
const byQualified = all.find((model: any) => `${model.provider}/${model.id}` === target);
|
|
129
|
+
if (byQualified) return { model: byQualified, matchedBy: "qualified" };
|
|
130
|
+
}
|
|
131
|
+
const byId = all.filter((model: any) => model.id === target);
|
|
132
|
+
return byId.length === 1 ? { model: byId[0], matchedBy: "id" } : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function configuredModelMatches(current: string | undefined, currentProvider: string | undefined, resolved: { model: any; matchedBy: "qualified" | "id" }): boolean {
|
|
136
|
+
const model = resolved.model;
|
|
137
|
+
if (!current || !model?.provider || !model?.id) return false;
|
|
138
|
+
const c = current.toLowerCase();
|
|
139
|
+
const provider = String(model.provider).toLowerCase();
|
|
140
|
+
const id = String(model.id).toLowerCase();
|
|
141
|
+
const observedProvider = currentProvider?.toLowerCase();
|
|
142
|
+
if (observedProvider) {
|
|
143
|
+
const currentModel = c.startsWith(`${observedProvider}/`) ? c.slice(observedProvider.length + 1) : c;
|
|
144
|
+
return observedProvider === provider && currentModel === id;
|
|
145
|
+
}
|
|
146
|
+
return c === `${provider}/${id}` || (resolved.matchedBy === "id" && c === id);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function applyModelRouting(pi: Pick<ExtensionAPI, "setModel"> | undefined, ctx: any, summary: RouterObserveSummary): Promise<RouterModelApplySummary> {
|
|
150
|
+
if (!summary.targetModel || summary.role === "none" || summary.role === "current") return { applied: false, reason: "no model switch for route action" };
|
|
151
|
+
const resolved = findConfiguredModel(ctx, summary.targetModel, summary.currentProvider);
|
|
152
|
+
if (resolved?.matchedBy === "id" && modelsMatch(summary.currentModel, summary.targetModel, summary.currentProvider)) return { applied: false, reason: "current model already matches target", fromModel: summary.currentModel, toModel: summary.targetModel };
|
|
153
|
+
if (resolved && configuredModelMatches(summary.currentModel, summary.currentProvider, resolved)) return { applied: false, reason: "current model already matches target", fromModel: summary.currentModel, toModel: summary.targetModel };
|
|
154
|
+
if (!resolved && modelsMatch(summary.currentModel, summary.targetModel, summary.currentProvider)) return { applied: false, reason: "current model already matches target", fromModel: summary.currentModel, toModel: summary.targetModel };
|
|
155
|
+
if (!resolved) return { applied: false, reason: `target model not configured: ${summary.targetModel}`, fromModel: summary.currentModel, toModel: summary.targetModel };
|
|
156
|
+
const success = await pi?.setModel?.(resolved.model);
|
|
157
|
+
if (!success) return { applied: false, reason: `target model unavailable or missing auth: ${summary.targetModel}`, fromModel: summary.currentModel, toModel: summary.targetModel };
|
|
158
|
+
return { applied: true, reason: summary.reason, fromModel: summary.currentModel, toModel: summary.targetModel };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function observeRouterTurn(ctx: any, pi?: Pick<ExtensionAPI, "setModel">): Promise<RouterObserveSummary | null> {
|
|
98
162
|
const config = loadRouterConfig(ctx);
|
|
99
|
-
if (!config.enabled || config.print === "off") return null;
|
|
163
|
+
if (!config.enabled || (config.print === "off" && config.mode === "observe")) return null;
|
|
100
164
|
const sessionPath = ctx?.sessionManager?.getSessionFile?.();
|
|
101
165
|
if (!sessionPath) return null;
|
|
102
166
|
const checkpoint = await latestCheckpointFromSession(String(sessionPath));
|
|
@@ -120,7 +184,14 @@ export async function observeRouterTurn(ctx: any): Promise<RouterObserveSummary
|
|
|
120
184
|
lastSummary: summary.text,
|
|
121
185
|
}, String(sessionPath));
|
|
122
186
|
|
|
187
|
+
if (config.mode === "auto_model") {
|
|
188
|
+
const applied = await applyModelRouting(pi, ctx, summary);
|
|
189
|
+
if (applied.applied || summary.match === false) {
|
|
190
|
+
ctx.ui?.notify?.(`router auto-model: ${applied.applied ? "APPLIED" : "SKIPPED"} ${applied.fromModel ?? "unknown"} → ${applied.toModel ?? "none"} · ${applied.reason}`, applied.applied ? "info" : "warning");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
123
194
|
if (config.print === "mismatch_only" && summary.match !== false) return summary;
|
|
124
|
-
ctx.ui?.notify?.(summary.text, summary.match === false ? "warning" : "info");
|
|
195
|
+
if (config.print !== "off") ctx.ui?.notify?.(summary.text, summary.match === false ? "warning" : "info");
|
|
125
196
|
return summary;
|
|
126
197
|
}
|