@fiale-plus/pi-rogue 0.2.2 → 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-core/src/context-broker.ts +4 -0
- 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-context-broker/README.md +20 -2
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +81 -3
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +72 -10
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.test.ts +32 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +32 -1
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +37 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +39 -2
- 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 +46 -5
- 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/checkpoints.ts +9 -1
- 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 +145 -6
- package/node_modules/@fiale-plus/pi-rogue-router/src/config.ts +51 -11
- package/node_modules/@fiale-plus/pi-rogue-router/src/extension.ts +67 -7
- package/node_modules/@fiale-plus/pi-rogue-router/src/git-features.ts +27 -12
- package/node_modules/@fiale-plus/pi-rogue-router/src/index.ts +4 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/observe.ts +87 -9
- 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 +59 -2
- package/package.json +1 -1
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { decideRoute, readCheckpointJsonl, selectCheckpoint } from "./decision.js";
|
|
5
|
+
import { writeBinaryGateTraining } from "./binary-gate.js";
|
|
5
6
|
import { writeSessionCheckpointsJsonl } from "./checkpoints.js";
|
|
6
7
|
import { appendRouteEvent, buildRouteEvent } from "./ledger.js";
|
|
7
8
|
import { writeCapabilityCards, writeShadowEval, writeTeacherPromptRequests, writeTeacherReflection } from "./learning.js";
|
|
8
9
|
import { writeTrainingRows } from "./dataset.js";
|
|
9
|
-
import { writeInferredOutcomes } from "./outcomes.js";
|
|
10
|
+
import { writeEnrichedOutcomes, writeInferredOutcomes } from "./outcomes.js";
|
|
11
|
+
import { writeRouterReport } from "./reports.js";
|
|
12
|
+
import { writeSharpeningHints } from "./sharpening.js";
|
|
13
|
+
import { runTeacherLabeling } from "./teacher-runner.js";
|
|
10
14
|
|
|
11
15
|
interface Args {
|
|
12
16
|
command?: string;
|
|
@@ -19,12 +23,22 @@ interface Args {
|
|
|
19
23
|
events?: string;
|
|
20
24
|
labels?: string;
|
|
21
25
|
reflection?: string;
|
|
26
|
+
artifact?: string;
|
|
27
|
+
report?: string;
|
|
28
|
+
dataset?: string;
|
|
29
|
+
evalDataset?: string;
|
|
30
|
+
markdown?: string;
|
|
31
|
+
gateReport?: string;
|
|
22
32
|
teacher?: string;
|
|
23
33
|
teacherOutput?: string;
|
|
24
34
|
teacherPrompts?: string;
|
|
35
|
+
requests?: string;
|
|
36
|
+
decisionsOutput?: string;
|
|
25
37
|
outcomes?: string;
|
|
38
|
+
cards?: string;
|
|
26
39
|
includeLocalRuleLabels?: boolean;
|
|
27
40
|
workspaceDiff?: boolean;
|
|
41
|
+
dryRun?: boolean;
|
|
28
42
|
pretty: boolean;
|
|
29
43
|
}
|
|
30
44
|
|
|
@@ -35,9 +49,14 @@ function usage(): never {
|
|
|
35
49
|
npm run router:decide -- --checkpoint-file <checkpoints.jsonl> [--checkpoint-id <id>] [--ledger <events.jsonl>] [--pretty]
|
|
36
50
|
npm run router:cards -- --events <events.jsonl> --output <model-cards.jsonl> [--outcomes <outcomes.jsonl>] [--pretty]
|
|
37
51
|
npm run router:outcomes -- --checkpoint-file <checkpoints.jsonl> --events <events.jsonl> --output <outcomes.jsonl> [--pretty]
|
|
52
|
+
npm run router:outcome-enrich -- --outcomes <outcomes.jsonl> --output <enriched-outcomes.jsonl> [--checkpoint-file <checkpoints.jsonl>] [--events <events.jsonl>] [--pretty]
|
|
38
53
|
npm run router:teacher-requests -- --checkpoint-file <checkpoints.jsonl> --output <requests.jsonl> [--teacher openai-codex/gpt-5.5] [--pretty]
|
|
54
|
+
npm run router:teacher-label -- --requests <requests.jsonl> --teacher-output <decisions.jsonl> --labels <labels.jsonl> [--teacher openai-codex/gpt-5.5] [--dry-run] [--pretty]
|
|
39
55
|
npm run router:reflect -- --checkpoint-file <checkpoints.jsonl> --labels <labels.jsonl> --reflection <reflection.md> [--teacher local-rule] [--teacher-output <decisions.jsonl>] [--teacher-prompts <requests.jsonl>] [--pretty]
|
|
40
56
|
npm run router:dataset -- --checkpoint-file <checkpoints.jsonl> --output <training.jsonl> [--events <events.jsonl>] [--outcomes <outcomes.jsonl>] [--labels <labels.jsonl>] [--include-local-rule-labels] [--pretty]
|
|
57
|
+
npm run router:gate-train -- --dataset <training.jsonl> --eval-dataset <eval.jsonl> --artifact <gate.json> --report <gate-report.json> [--pretty]
|
|
58
|
+
npm run router:report -- --output <report.json> [--markdown <report.md>] [--events <events.jsonl>] [--outcomes <outcomes.jsonl>] [--dataset <training.jsonl>] [--gate-report <gate-report.json>] [--pretty]
|
|
59
|
+
npm run router:sharpen -- --events <events.jsonl> --output <hints.json> [--outcomes <outcomes.jsonl>] [--cards <model-cards.jsonl>] [--pretty]
|
|
41
60
|
npm run router:shadow -- --checkpoint-file <checkpoints.jsonl> --output <report.json> [--ledger <events.jsonl>] [--pretty]
|
|
42
61
|
|
|
43
62
|
Commands:
|
|
@@ -45,9 +64,14 @@ Commands:
|
|
|
45
64
|
decide Emit a strict JSON route decision for a checkpoint and optionally append a route event.
|
|
46
65
|
cards Generate local observed model capability cards from route events and optional outcomes.
|
|
47
66
|
outcomes Infer conservative outcome skeletons that can be manually enriched.
|
|
67
|
+
outcome-enrich Enrich outcome records from checkpoints and route events.
|
|
48
68
|
teacher-requests Generate local JSONL prompt requests for explicit teacher labeling.
|
|
69
|
+
teacher-label Run explicit teacher model labeling over request JSONL.
|
|
49
70
|
reflect Generate command-triggered soft routing labels and a reflection artifact.
|
|
50
71
|
dataset Export trainable rows for a conservative continue-vs-intervene gate.
|
|
72
|
+
gate-train Train/evaluate a local binary continue-vs-intervene gate artifact.
|
|
73
|
+
report Summarize route events, outcomes, labels, and gate eval metrics.
|
|
74
|
+
sharpen Generate local, provenance-backed router sharpening hints without mutating policy.
|
|
51
75
|
shadow Shadow-evaluate the current rule policy over historical checkpoints.
|
|
52
76
|
`);
|
|
53
77
|
process.exit(2);
|
|
@@ -104,6 +128,36 @@ function parseArgs(argv: string[]): Args {
|
|
|
104
128
|
index++;
|
|
105
129
|
continue;
|
|
106
130
|
}
|
|
131
|
+
if (arg === "--artifact" && next) {
|
|
132
|
+
args.artifact = next;
|
|
133
|
+
index++;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (arg === "--report" && next) {
|
|
137
|
+
args.report = next;
|
|
138
|
+
index++;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (arg === "--dataset" && next) {
|
|
142
|
+
args.dataset = next;
|
|
143
|
+
index++;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (arg === "--eval-dataset" && next) {
|
|
147
|
+
args.evalDataset = next;
|
|
148
|
+
index++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (arg === "--markdown" && next) {
|
|
152
|
+
args.markdown = next;
|
|
153
|
+
index++;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (arg === "--gate-report" && next) {
|
|
157
|
+
args.gateReport = next;
|
|
158
|
+
index++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
107
161
|
if (arg === "--teacher" && next) {
|
|
108
162
|
args.teacher = next;
|
|
109
163
|
index++;
|
|
@@ -119,11 +173,26 @@ function parseArgs(argv: string[]): Args {
|
|
|
119
173
|
index++;
|
|
120
174
|
continue;
|
|
121
175
|
}
|
|
176
|
+
if (arg === "--requests" && next) {
|
|
177
|
+
args.requests = next;
|
|
178
|
+
index++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (arg === "--decisions-output" && next) {
|
|
182
|
+
args.decisionsOutput = next;
|
|
183
|
+
index++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
122
186
|
if (arg === "--outcomes" && next) {
|
|
123
187
|
args.outcomes = next;
|
|
124
188
|
index++;
|
|
125
189
|
continue;
|
|
126
190
|
}
|
|
191
|
+
if (arg === "--cards" && next) {
|
|
192
|
+
args.cards = next;
|
|
193
|
+
index++;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
127
196
|
if (arg === "--include-local-rule-labels") {
|
|
128
197
|
args.includeLocalRuleLabels = true;
|
|
129
198
|
continue;
|
|
@@ -132,6 +201,10 @@ function parseArgs(argv: string[]): Args {
|
|
|
132
201
|
args.workspaceDiff = true;
|
|
133
202
|
continue;
|
|
134
203
|
}
|
|
204
|
+
if (arg === "--dry-run") {
|
|
205
|
+
args.dryRun = true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
135
208
|
if (arg === "--pretty") {
|
|
136
209
|
args.pretty = true;
|
|
137
210
|
continue;
|
|
@@ -207,12 +280,28 @@ function outcomes(args: Args): unknown {
|
|
|
207
280
|
return writeInferredOutcomes({ checkpointPath: args.checkpointFile, eventsPath: args.events, outputPath: args.output });
|
|
208
281
|
}
|
|
209
282
|
|
|
283
|
+
function outcomeEnrich(args: Args): unknown {
|
|
284
|
+
if (!args.outcomes || !args.output) usage();
|
|
285
|
+
return writeEnrichedOutcomes({ outcomesPath: args.outcomes, outputPath: args.output, checkpointPath: args.checkpointFile, eventsPath: args.events });
|
|
286
|
+
}
|
|
287
|
+
|
|
210
288
|
function teacherRequests(args: Args): unknown {
|
|
211
289
|
if (!args.checkpointFile || !args.output) usage();
|
|
212
290
|
const requests = writeTeacherPromptRequests(args.checkpointFile, args.output, args.teacher ?? "openai-codex/gpt-5.5");
|
|
213
291
|
return { schema: "pi-router.teacher-requests-summary.v1", output: resolve(args.output), requests: requests.length, teacher: args.teacher ?? "openai-codex/gpt-5.5" };
|
|
214
292
|
}
|
|
215
293
|
|
|
294
|
+
async function teacherLabel(args: Args): Promise<unknown> {
|
|
295
|
+
if (!args.requests || !args.teacherOutput || !args.labels) usage();
|
|
296
|
+
return runTeacherLabeling({
|
|
297
|
+
requestsPath: args.requests,
|
|
298
|
+
decisionsOutputPath: args.teacherOutput,
|
|
299
|
+
labelsOutputPath: args.labels,
|
|
300
|
+
teacher: args.teacher,
|
|
301
|
+
dryRun: args.dryRun,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
216
305
|
function reflect(args: Args): unknown {
|
|
217
306
|
if (!args.checkpointFile || !args.labels || !args.reflection) usage();
|
|
218
307
|
const result = writeTeacherReflection({
|
|
@@ -244,6 +333,21 @@ function dataset(args: Args): unknown {
|
|
|
244
333
|
});
|
|
245
334
|
}
|
|
246
335
|
|
|
336
|
+
function gateTrain(args: Args): unknown {
|
|
337
|
+
if (!args.dataset || !args.evalDataset || !args.artifact || !args.report) usage();
|
|
338
|
+
return writeBinaryGateTraining({ trainingRowsPath: args.dataset, evalRowsPath: args.evalDataset, artifactPath: args.artifact, reportPath: args.report });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function report(args: Args): unknown {
|
|
342
|
+
if (!args.output) usage();
|
|
343
|
+
return writeRouterReport({ outputPath: args.output, markdownPath: args.markdown, eventsPath: args.events, outcomesPath: args.outcomes, trainingRowsPath: args.dataset, gateReportPath: args.gateReport });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function sharpen(args: Args): unknown {
|
|
347
|
+
if (!args.events || !args.output) usage();
|
|
348
|
+
return writeSharpeningHints({ eventsPath: args.events, outputPath: args.output, outcomesPath: args.outcomes, cardsPath: args.cards });
|
|
349
|
+
}
|
|
350
|
+
|
|
247
351
|
function shadow(args: Args): unknown {
|
|
248
352
|
if (!args.checkpointFile || !args.output) usage();
|
|
249
353
|
return writeShadowEval(args.checkpointFile, args.output, args.ledger);
|
|
@@ -259,15 +363,25 @@ async function main(): Promise<void> {
|
|
|
259
363
|
? cards(args)
|
|
260
364
|
: args.command === "outcomes"
|
|
261
365
|
? outcomes(args)
|
|
262
|
-
: args.command === "
|
|
366
|
+
: args.command === "outcome-enrich"
|
|
367
|
+
? outcomeEnrich(args)
|
|
368
|
+
: args.command === "teacher-requests"
|
|
263
369
|
? teacherRequests(args)
|
|
264
|
-
: args.command === "
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
370
|
+
: args.command === "teacher-label"
|
|
371
|
+
? await teacherLabel(args)
|
|
372
|
+
: args.command === "reflect"
|
|
373
|
+
? reflect(args)
|
|
374
|
+
: args.command === "dataset"
|
|
375
|
+
? dataset(args)
|
|
376
|
+
: args.command === "gate-train"
|
|
377
|
+
? gateTrain(args)
|
|
378
|
+
: args.command === "report"
|
|
379
|
+
? report(args)
|
|
380
|
+
: args.command === "sharpen"
|
|
381
|
+
? sharpen(args)
|
|
382
|
+
: args.command === "shadow"
|
|
383
|
+
? shadow(args)
|
|
384
|
+
: usage();
|
|
271
385
|
console.log(args.pretty ? JSON.stringify(result, null, 2) : JSON.stringify(result));
|
|
272
386
|
}
|
|
273
387
|
|
|
@@ -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
|
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
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, 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 { summarizeRouterDecision } from "./observe.js";
|
|
9
|
+
import { applyModelRouting, modelsMatch, observeRouterTurn, summarizeRouterDecision } from "./observe.js";
|
|
10
10
|
import type { RouterCheckpoint } from "./types.js";
|
|
11
11
|
|
|
12
|
-
function ctxMock() {
|
|
12
|
+
function ctxMock(sessionPath?: string) {
|
|
13
13
|
const cwd = mkdtempSync(join(tmpdir(), "pi-router-ext-"));
|
|
14
14
|
const notifications: Array<{ text: string; level: string }> = [];
|
|
15
15
|
return {
|
|
16
16
|
cwd,
|
|
17
17
|
notifications,
|
|
18
|
+
sessionManager: sessionPath ? { getSessionFile: () => sessionPath } : undefined,
|
|
18
19
|
ui: {
|
|
19
20
|
notify(text: string, level: string) {
|
|
20
21
|
notifications.push({ text, level });
|
|
@@ -23,11 +24,23 @@ function ctxMock() {
|
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
function writeSessionFixture(dir: string, name: string): string {
|
|
28
|
+
const path = join(dir, name);
|
|
29
|
+
writeFileSync(path, [
|
|
30
|
+
JSON.stringify({ type: "session", id: name, cwd: dir }),
|
|
31
|
+
JSON.stringify({ type: "message", id: `${name}-user`, message: { role: "user", content: [{ type: "text", text: "please implement a small fix" }] } }),
|
|
32
|
+
].join("\n") + "\n");
|
|
33
|
+
return path;
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
function piMock() {
|
|
27
37
|
const commands = new Map<string, any>();
|
|
28
38
|
const shortcuts = new Map<string, any>();
|
|
29
39
|
const handlers = new Map<string, any[]>();
|
|
40
|
+
const selectedModels: any[] = [];
|
|
30
41
|
const pi: any = {
|
|
42
|
+
selectedModels,
|
|
43
|
+
async setModel(model: any) { selectedModels.push(model); return true; },
|
|
31
44
|
registerCommand(name: string, options: any) { commands.set(name, options); },
|
|
32
45
|
registerShortcut(key: string, options: any) { shortcuts.set(key, options); },
|
|
33
46
|
on(name: string, handler: any) { handlers.set(name, [...(handlers.get(name) ?? []), handler]); },
|
|
@@ -81,6 +94,7 @@ describe("router config profiles", () => {
|
|
|
81
94
|
|
|
82
95
|
expect(config.activeProfile).toBe("all-smart");
|
|
83
96
|
expect(config.profileOrder).toEqual(["all-smart", "spark-smart", "local-smart"]);
|
|
97
|
+
expect(config.mode).toBe("observe");
|
|
84
98
|
expect(activeProfile(config).worker).toBe("openai-codex/gpt-5.5");
|
|
85
99
|
expect(readFileSync(routerConfigPath(ctx), "utf8")).toContain("spark-smart");
|
|
86
100
|
});
|
|
@@ -92,11 +106,43 @@ describe("router config profiles", () => {
|
|
|
92
106
|
expect(spark?.activeProfile).toBe("spark-smart");
|
|
93
107
|
expect(cycleRouterProfile(spark!, 1).activeProfile).toBe("local-smart");
|
|
94
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();
|
|
95
114
|
});
|
|
96
115
|
|
|
97
|
-
it("completes router commands
|
|
98
|
-
|
|
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 …");
|
|
99
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"]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("keeps config repo-global while state and live events are session-scoped", async () => {
|
|
127
|
+
const cwd = mkdtempSync(join(tmpdir(), "pi-router-sessions-"));
|
|
128
|
+
const firstSession = writeSessionFixture(cwd, "session-a.jsonl");
|
|
129
|
+
const secondSession = writeSessionFixture(cwd, "session-b.jsonl");
|
|
130
|
+
const firstCtx = { ...ctxMock(firstSession), cwd };
|
|
131
|
+
const secondCtx = { ...ctxMock(secondSession), cwd };
|
|
132
|
+
saveRouterConfig(firstCtx, { ...loadRouterConfig(firstCtx), enabled: true, print: "all" });
|
|
133
|
+
|
|
134
|
+
expect(routerConfigPath(firstCtx)).toBe(routerConfigPath(secondCtx));
|
|
135
|
+
expect(routerStatePath(firstCtx, firstSession)).not.toBe(routerStatePath(secondCtx, secondSession));
|
|
136
|
+
expect(routerEventsPath(firstCtx, firstSession)).not.toBe(routerEventsPath(secondCtx, secondSession));
|
|
137
|
+
expect(routerSessionDir(firstCtx, firstSession)).toContain("session-a");
|
|
138
|
+
|
|
139
|
+
await observeRouterTurn(firstCtx);
|
|
140
|
+
await observeRouterTurn(secondCtx);
|
|
141
|
+
|
|
142
|
+
expect(existsSync(routerStatePath(firstCtx, firstSession))).toBe(true);
|
|
143
|
+
expect(existsSync(routerStatePath(secondCtx, secondSession))).toBe(true);
|
|
144
|
+
expect(readFileSync(routerEventsPath(firstCtx, firstSession), "utf8").trim().split("\n")).toHaveLength(1);
|
|
145
|
+
expect(readFileSync(routerEventsPath(secondCtx, secondSession), "utf8").trim().split("\n")).toHaveLength(1);
|
|
100
146
|
});
|
|
101
147
|
});
|
|
102
148
|
|
|
@@ -117,6 +163,18 @@ describe("router extension", () => {
|
|
|
117
163
|
await commands.get("router").handler("profile spark-smart", ctx);
|
|
118
164
|
expect(loadRouterConfig(ctx).activeProfile).toBe("spark-smart");
|
|
119
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
|
+
|
|
120
178
|
await shortcuts.get("ctrl+alt+p").handler(ctx);
|
|
121
179
|
expect(loadRouterConfig(ctx).activeProfile).toBe("local-smart");
|
|
122
180
|
});
|
|
@@ -130,4 +188,85 @@ describe("router extension", () => {
|
|
|
130
188
|
expect(summary.text).toContain("smart(openai-codex/gpt-5.5)");
|
|
131
189
|
expect(summary.text).toContain("current=gpt-5.3-codex-spark");
|
|
132
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
|
+
});
|
|
133
272
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { hashText } from "./hash.js";
|
|
3
4
|
|
|
4
|
-
export type RouterMode = "observe";
|
|
5
|
+
export type RouterMode = "observe" | "auto_model";
|
|
5
6
|
export type RouterPrintMode = "all" | "mismatch_only" | "off";
|
|
6
7
|
|
|
7
8
|
export interface RouterProfile {
|
|
@@ -83,12 +84,36 @@ export function routerConfigPath(ctx: any): string {
|
|
|
83
84
|
return join(routerDir(ctx), "config.json");
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
function sessionPathFromCtx(ctx: any): string | undefined {
|
|
88
|
+
const value = ctx?.sessionManager?.getSessionFile?.();
|
|
89
|
+
return value ? String(value) : undefined;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
return
|
|
92
|
+
function safeSegment(value: string): string {
|
|
93
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 96) || "session";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function routerSessionKey(sessionPath: string): string {
|
|
97
|
+
const resolved = resolve(sessionPath);
|
|
98
|
+
const name = safeSegment(basename(resolved).replace(/\.jsonl$/i, ""));
|
|
99
|
+
return `${name}-${hashText(resolved).slice(0, 8)}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function routerSessionsDir(ctx: any): string {
|
|
103
|
+
return join(routerDir(ctx), "sessions");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function routerSessionDir(ctx: any, sessionPath = sessionPathFromCtx(ctx)): string {
|
|
107
|
+
const key = sessionPath ? routerSessionKey(sessionPath) : "no-session";
|
|
108
|
+
return join(routerSessionsDir(ctx), key);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function routerStatePath(ctx: any, sessionPath = sessionPathFromCtx(ctx)): string {
|
|
112
|
+
return join(routerSessionDir(ctx, sessionPath), "state.json");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function routerEventsPath(ctx: any, sessionPath = sessionPathFromCtx(ctx)): string {
|
|
116
|
+
return join(routerSessionDir(ctx, sessionPath), "events.jsonl");
|
|
92
117
|
}
|
|
93
118
|
|
|
94
119
|
function readJson<T>(path: string, fallback: T): T {
|
|
@@ -99,6 +124,10 @@ function readJson<T>(path: string, fallback: T): T {
|
|
|
99
124
|
}
|
|
100
125
|
}
|
|
101
126
|
|
|
127
|
+
function normalizeRouterMode(value: unknown): RouterMode {
|
|
128
|
+
return value === "auto_model" || value === "auto" ? "auto_model" : "observe";
|
|
129
|
+
}
|
|
130
|
+
|
|
102
131
|
export function normalizeRouterConfig(raw: Partial<RouterConfig> | null | undefined): RouterConfig {
|
|
103
132
|
const mergedProfiles = { ...DEFAULT_ROUTER_CONFIG.profiles, ...(raw?.profiles ?? {}) };
|
|
104
133
|
const profileOrder = Array.isArray(raw?.profileOrder) && raw.profileOrder.length > 0
|
|
@@ -110,7 +139,7 @@ export function normalizeRouterConfig(raw: Partial<RouterConfig> | null | undefi
|
|
|
110
139
|
const print = raw?.print === "all" || raw?.print === "off" || raw?.print === "mismatch_only" ? raw.print : DEFAULT_ROUTER_CONFIG.print;
|
|
111
140
|
return {
|
|
112
141
|
enabled: Boolean(raw?.enabled ?? DEFAULT_ROUTER_CONFIG.enabled),
|
|
113
|
-
mode:
|
|
142
|
+
mode: normalizeRouterMode(raw?.mode),
|
|
114
143
|
print,
|
|
115
144
|
activeProfile,
|
|
116
145
|
profileOrder,
|
|
@@ -135,12 +164,12 @@ export function ensureRouterConfig(ctx: any): RouterConfig {
|
|
|
135
164
|
return config;
|
|
136
165
|
}
|
|
137
166
|
|
|
138
|
-
export function loadRouterState(ctx: any): RouterState {
|
|
139
|
-
return readJson<RouterState>(routerStatePath(ctx), {});
|
|
167
|
+
export function loadRouterState(ctx: any, sessionPath?: string): RouterState {
|
|
168
|
+
return readJson<RouterState>(routerStatePath(ctx, sessionPath), {});
|
|
140
169
|
}
|
|
141
170
|
|
|
142
|
-
export function saveRouterState(ctx: any, state: RouterState): void {
|
|
143
|
-
const path = routerStatePath(ctx);
|
|
171
|
+
export function saveRouterState(ctx: any, state: RouterState, sessionPath?: string): void {
|
|
172
|
+
const path = routerStatePath(ctx, sessionPath);
|
|
144
173
|
mkdirSync(dirname(path), { recursive: true });
|
|
145
174
|
writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`);
|
|
146
175
|
}
|
|
@@ -162,6 +191,17 @@ export function setRouterProfile(config: RouterConfig, name: string): RouterConf
|
|
|
162
191
|
return { ...config, activeProfile: name, profileOrder: config.profileOrder.includes(name) ? config.profileOrder : [...config.profileOrder, name] };
|
|
163
192
|
}
|
|
164
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
|
+
|
|
165
205
|
export function formatProfile(name: string, profile: RouterProfile): string {
|
|
166
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(" ");
|
|
167
207
|
return `${name}: worker=${profile.worker} smart=${profile.smart} teacher=${profile.teacher} reviewer=${profile.reviewer} ${subagents}`;
|