pi-laya-mlx 0.1.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.
@@ -0,0 +1,440 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { clampThinkingLevel } from "@earendil-works/pi-ai";
3
+ import { Key } from "@earendil-works/pi-tui";
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import * as os from "node:os";
7
+
8
+ interface RouteConfig {
9
+ provider: string;
10
+ model: string;
11
+ thinking: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
12
+ description: string;
13
+ /** Focused yes/no question for this route; enables the more reliable noul probe mode. */
14
+ noul?: string;
15
+ }
16
+
17
+ interface RouterSettings {
18
+ enabled: boolean;
19
+ endpoint?: string;
20
+ /** Minimum confidence (0-1) required before switching models. Below this, keep current model. */
21
+ threshold?: number;
22
+ /** Use per-route yes/no probes instead of a single choice question (default true when all routes define `noul`). */
23
+ noul_mode?: boolean;
24
+ routes: Record<string, RouteConfig>;
25
+ }
26
+
27
+ const CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "laya-router.json");
28
+
29
+ function loadConfig(): RouterSettings | null {
30
+ try {
31
+ if (!fs.existsSync(CONFIG_PATH)) return null;
32
+ const raw = fs.readFileSync(CONFIG_PATH, "utf8");
33
+ return JSON.parse(raw);
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ export default function (pi: ExtensionAPI) {
40
+ let lastPrompt: string | undefined;
41
+ let lastPredictedCategory: string | undefined;
42
+
43
+ const toggleLaya = (ctx: ExtensionContext | ExtensionCommandContext, forceState?: boolean) => {
44
+ const config = loadConfig();
45
+ if (!config) {
46
+ if (ctx.hasUI) ctx.ui.notify("Config file not found at ~/.pi/agent/laya-router.json. Run 'laya init' in terminal.", "error");
47
+ return;
48
+ }
49
+
50
+ config.enabled = forceState !== undefined ? forceState : !config.enabled;
51
+ try {
52
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
53
+ } catch (err: any) {
54
+ if (ctx.hasUI) ctx.ui.notify(`Failed to save config: ${err.message}`, "error");
55
+ return;
56
+ }
57
+
58
+ const stateLabel = config.enabled ? "ENABLED" : "DISABLED";
59
+ if (ctx.hasUI) {
60
+ ctx.ui.notify(`⚡ Laya auto-router is now ${stateLabel}`, "info");
61
+ }
62
+ pi.events.emit("laya:toggle", { enabled: config.enabled });
63
+ };
64
+
65
+ const handleCommand = async (args: string | undefined, ctx: ExtensionCommandContext) => {
66
+ const config = loadConfig();
67
+ if (!config) {
68
+ ctx.ui.notify("Config file not found at ~/.pi/agent/laya-router.json. Run 'laya init' in terminal.", "error");
69
+ return;
70
+ }
71
+
72
+ const trimmed = args?.trim().toLowerCase();
73
+ if (trimmed === "on") {
74
+ toggleLaya(ctx, true);
75
+ return;
76
+ }
77
+ if (trimmed === "off") {
78
+ toggleLaya(ctx, false);
79
+ return;
80
+ }
81
+ if (!trimmed || trimmed === "toggle") {
82
+ toggleLaya(ctx);
83
+ return;
84
+ }
85
+
86
+ if (trimmed === "status" || trimmed === "list") {
87
+ const status = config.enabled ? "ENABLED" : "DISABLED";
88
+ const endpoint = config.endpoint || "http://127.0.0.1:4141/predict";
89
+
90
+ // Check daemon health
91
+ let daemon = "unknown";
92
+ try {
93
+ const healthUrl = new URL(endpoint);
94
+ healthUrl.pathname = "/health";
95
+ const controller = new AbortController();
96
+ const t = setTimeout(() => controller.abort(), 800);
97
+ const res = await fetch(healthUrl.toString(), { signal: controller.signal });
98
+ clearTimeout(t);
99
+ daemon = res.ok ? "running" : `error ${res.status}`;
100
+ } catch {
101
+ daemon = "not running";
102
+ }
103
+
104
+ const routesList = Object.entries(config.routes)
105
+ .map(([key, val]) => `• ${key}: ${val.provider}/${val.model} (thinking: ${val.thinking})`)
106
+ .join("\n");
107
+
108
+ ctx.ui.notify(
109
+ `[Laya Auto-Router: ${status}]\nDaemon: ${daemon} (${endpoint})\nThreshold: ${config.threshold ?? 0.25}\n\n${routesList}\n\nToggle: /laya (or Ctrl+Shift+J / Alt+J)\nRecent logs: /laya logs`,
110
+ "info"
111
+ );
112
+ return;
113
+ }
114
+
115
+ if (trimmed?.startsWith("log")) {
116
+ const endpoint = config.endpoint || "http://127.0.0.1:4141/predict";
117
+ try {
118
+ const logsUrl = new URL(endpoint);
119
+ logsUrl.pathname = "/logs";
120
+ logsUrl.searchParams.set("limit", "10");
121
+ const res = await fetch(logsUrl.toString());
122
+ if (!res.ok) {
123
+ ctx.ui.notify(`[Laya] Failed to fetch logs (${res.status})`, "error");
124
+ return;
125
+ }
126
+ const data = await res.json() as { logs: Array<{ timestamp: string; prompt: string; choice: string; confidence: number; latency_ms: number }> };
127
+ if (!data.logs || data.logs.length === 0) {
128
+ ctx.ui.notify("[Laya] No prediction logs recorded yet.", "info");
129
+ return;
130
+ }
131
+ const logLines = data.logs.map((item) => {
132
+ const timeStr = item.timestamp.split("T")[1]?.slice(0, 8) || "";
133
+ const conf = Math.round((item.confidence || 0) * 100);
134
+ const p = item.prompt.length > 35 ? item.prompt.slice(0, 32) + "..." : item.prompt;
135
+ return `[${timeStr}] ${item.choice} (${conf}%) [${item.latency_ms}ms] <- "${p}"`;
136
+ }).join("\n");
137
+
138
+ ctx.ui.notify(`[Laya Recent Logs]\n${logLines}`, "info");
139
+ } catch (err: any) {
140
+ ctx.ui.notify(`[Laya] Error fetching logs: ${err.message}`, "error");
141
+ }
142
+ return;
143
+ }
144
+
145
+ if (trimmed?.startsWith("flag") || trimmed?.startsWith("correct")) {
146
+ const parts = trimmed.split(/\s+/);
147
+ const targetInput = parts[1]?.toLowerCase();
148
+ if (!lastPrompt) {
149
+ ctx.ui.notify("[Laya] No recent prompt recorded to flag.", "warning");
150
+ return;
151
+ }
152
+ const catMap: Record<string, string> = {
153
+ chat: "CHAT_OR_TRIVIAL",
154
+ trivial: "CHAT_OR_TRIVIAL",
155
+ review: "CODE_REVIEW",
156
+ code_review: "CODE_REVIEW",
157
+ coding: "CODING_STANDARD",
158
+ code: "CODING_STANDARD",
159
+ explore: "RESEARCH_AND_EXPLORE",
160
+ research: "RESEARCH_AND_EXPLORE",
161
+ arch: "HARD_ARCHITECTURE",
162
+ architecture: "HARD_ARCHITECTURE",
163
+ };
164
+ const targetCat = catMap[targetInput] || Object.keys(config.routes).find((k) => k.toLowerCase() === targetInput);
165
+ if (!targetCat) {
166
+ ctx.ui.notify(
167
+ `Usage: /laya flag [chat|review|coding|explore|arch]\nLast prompt: "${lastPrompt.slice(0, 50)}" (was: ${lastPredictedCategory || "unknown"})`,
168
+ "warning"
169
+ );
170
+ return;
171
+ }
172
+
173
+ const endpoint = config.endpoint || "http://127.0.0.1:4141/predict";
174
+ const corrUrl = new URL(endpoint);
175
+ corrUrl.pathname = "/corrections";
176
+ try {
177
+ await fetch(corrUrl.toString(), {
178
+ method: "POST",
179
+ headers: { "Content-Type": "application/json" },
180
+ body: JSON.stringify({
181
+ prompt: lastPrompt,
182
+ target: targetCat,
183
+ was: lastPredictedCategory,
184
+ }),
185
+ });
186
+ ctx.ui.notify(
187
+ `⚡ [Laya] Flagged: "${lastPrompt.slice(0, 45)}" → ${targetCat}\nSaved to corrections.jsonl. Run /skill:laya-tune to optimize anchors.`,
188
+ "info"
189
+ );
190
+ } catch (err: any) {
191
+ ctx.ui.notify(`[Laya] Error saving flag: ${err.message}`, "error");
192
+ }
193
+ return;
194
+ }
195
+
196
+ ctx.ui.notify("Usage: /laya [on|off|toggle|status|logs|flag <cat>] or press Ctrl+Shift+J / Alt+J", "warning");
197
+ };
198
+
199
+ pi.registerCommand("laya-router", {
200
+ description: "Inspect or toggle Laya local model routing",
201
+ handler: handleCommand,
202
+ });
203
+
204
+ pi.registerCommand("laya", {
205
+ description: "Toggle or inspect Laya local model routing",
206
+ handler: handleCommand,
207
+ });
208
+
209
+ pi.registerShortcut(Key.ctrlShift("j"), {
210
+ description: "Toggle Laya auto-router ON/OFF",
211
+ handler: async (ctx) => {
212
+ toggleLaya(ctx);
213
+ },
214
+ });
215
+
216
+ pi.registerShortcut(Key.alt("j"), {
217
+ description: "Toggle Laya auto-router ON/OFF",
218
+ handler: async (ctx) => {
219
+ toggleLaya(ctx);
220
+ },
221
+ });
222
+
223
+ pi.on("before_agent_start", async (event, ctx) => {
224
+ const config = loadConfig();
225
+ if (!config || !config.enabled) return;
226
+
227
+ let promptText = event.prompt?.trim();
228
+ if (!promptText || promptText.length < 3) return;
229
+
230
+ // Fast path: skip slash commands
231
+ if (promptText.startsWith("/")) return;
232
+
233
+ // Strip injected XML blocks (<memory>...</memory>, <context>...</context>, etc.)
234
+ promptText = promptText.replace(/<[a-zA-Z0-9_-]+[\s\S]*?<\/[a-zA-Z0-9_-]+>/gi, "").replace(/<[^>]+>/g, "").trim();
235
+ if (!promptText || promptText.length < 3) return;
236
+
237
+ const routeKeys = Object.keys(config.routes);
238
+ if (routeKeys.length === 0) return;
239
+
240
+ const criteria: Record<string, string> = {};
241
+ const noulProbes: Record<string, string> = {};
242
+ for (const [key, val] of Object.entries(config.routes)) {
243
+ criteria[key] = val.description;
244
+ if (val.noul) noulProbes[key] = val.noul;
245
+ }
246
+ const useNoul = config.noul_mode !== false && Object.keys(noulProbes).length === Object.keys(config.routes).length;
247
+
248
+ const stateSnippet = promptText.length > 400 ? promptText.slice(0, 400) : promptText;
249
+ lastPrompt = stateSnippet;
250
+
251
+ const payload = {
252
+ state: stateSnippet,
253
+ questions: {
254
+ task_category: {
255
+ type: "choice",
256
+ instructions: "Classify the developer's intent into the single most appropriate task category.",
257
+ criteria,
258
+ ...(useNoul ? { noul: noulProbes } : {}),
259
+ },
260
+ },
261
+ };
262
+
263
+ if (ctx.hasUI && ctx.ui.setStatus) {
264
+ ctx.ui.setStatus("laya-router", "⚡ [Laya] Classifying intent...");
265
+ }
266
+
267
+ try {
268
+ const endpoint = config.endpoint || "http://127.0.0.1:4141/predict";
269
+ const controller = new AbortController();
270
+ const timeout = setTimeout(() => controller.abort(), 1500);
271
+
272
+ const res = await fetch(endpoint, {
273
+ method: "POST",
274
+ headers: { "Content-Type": "application/json" },
275
+ body: JSON.stringify(payload),
276
+ signal: controller.signal,
277
+ });
278
+
279
+ clearTimeout(timeout);
280
+ if (ctx.hasUI && ctx.ui.setStatus) {
281
+ ctx.ui.setStatus("laya-router", undefined);
282
+ }
283
+
284
+ if (!res.ok) {
285
+ throw new Error(`daemon status ${res.status}`);
286
+ }
287
+
288
+ const data = await res.json() as any;
289
+ const taskAnswer = data?.answers?.task_category;
290
+ const selectedCategory = taskAnswer?.choice || taskAnswer?.value;
291
+ lastPredictedCategory = selectedCategory;
292
+ const probabilities: Record<string, number> | undefined = taskAnswer?.probabilities;
293
+ const confidence = taskAnswer?.confidence ?? (probabilities ? probabilities[selectedCategory] : undefined);
294
+ const confPercent = confidence !== undefined ? ` (${Math.round(confidence * 100)}%)` : "";
295
+
296
+ if (!selectedCategory || !config.routes[selectedCategory]) return;
297
+
298
+ const threshold = config.threshold ?? 0.25;
299
+ const topProb = confidence ?? (probabilities ? Math.max(...Object.values(probabilities)) : 1);
300
+
301
+ const targetCategory = topProb >= threshold ? selectedCategory : "CHAT_OR_TRIVIAL";
302
+ const targetRoute = config.routes[targetCategory];
303
+ if (!targetRoute) return;
304
+
305
+ const targetModel = ctx.modelRegistry.find(targetRoute.provider, targetRoute.model);
306
+ if (!targetModel) {
307
+ if (ctx.hasUI) {
308
+ ctx.ui.notify(
309
+ `⚡ [Laya] Model not found in registry: ${targetRoute.provider}/${targetRoute.model}`,
310
+ "warning"
311
+ );
312
+ }
313
+ return;
314
+ }
315
+
316
+ const currentModel = ctx.model || (pi as any).activeModel;
317
+ const isSame = currentModel && currentModel.id === targetRoute.model && currentModel.provider === targetRoute.provider;
318
+
319
+ const effectiveThinking = targetRoute.thinking
320
+ ? clampThinkingLevel(targetModel, targetRoute.thinking)
321
+ : pi.getThinkingLevel();
322
+
323
+ const switchNote = topProb >= threshold
324
+ ? `${targetCategory}${confPercent}`
325
+ : `CHAT_OR_TRIVIAL (fallback from ${selectedCategory} ${Math.round(topProb * 100)}%)`;
326
+
327
+ if (!isSame) {
328
+ const switched = await pi.setModel(targetModel);
329
+ if (switched) {
330
+ if (effectiveThinking) {
331
+ pi.setThinkingLevel(effectiveThinking);
332
+ }
333
+ const thinkingNote = targetRoute.thinking !== effectiveThinking
334
+ ? `${effectiveThinking} (clamped from ${targetRoute.thinking})`
335
+ : effectiveThinking;
336
+ if (ctx.hasUI) {
337
+ ctx.ui.notify(
338
+ `⚡ [Laya] Switched → ${switchNote} [${targetRoute.provider}/${targetRoute.model} | thinking: ${thinkingNote}]`,
339
+ "info"
340
+ );
341
+ }
342
+ }
343
+ } else {
344
+ const currentThinking = pi.getThinkingLevel();
345
+ if (effectiveThinking && currentThinking !== effectiveThinking) {
346
+ pi.setThinkingLevel(effectiveThinking);
347
+ }
348
+ if (ctx.hasUI && topProb >= threshold) {
349
+ ctx.ui.notify(
350
+ `⚡ [Laya] Kept ${selectedCategory}${confPercent} [${targetRoute.model} | thinking: ${effectiveThinking}]`,
351
+ "info"
352
+ );
353
+ }
354
+ }
355
+ } catch (layaErr: any) {
356
+ if (ctx.hasUI && ctx.ui.setStatus) {
357
+ ctx.ui.setStatus("laya-router", undefined);
358
+ }
359
+
360
+ // Fallback to Jev Cloud Router if configured
361
+ const apiKey = process.env.TYPESAFE_API_KEY;
362
+ if (apiKey) {
363
+ try {
364
+ if (ctx.hasUI && ctx.ui.setStatus) {
365
+ ctx.ui.setStatus("laya-router", "⚡ [Jev Fallback] Classifying...");
366
+ }
367
+
368
+ const jevEndpoint = (config as any).jev_endpoint || "https://api.typesafe.ai/v1/systemone";
369
+ const jevController = new AbortController();
370
+ const jevTimeout = setTimeout(() => jevController.abort(), 2000);
371
+
372
+ const jevRes = await fetch(jevEndpoint, {
373
+ method: "POST",
374
+ headers: {
375
+ "Content-Type": "application/json",
376
+ Authorization: `Bearer ${apiKey}`,
377
+ },
378
+ body: JSON.stringify({
379
+ model: (config as any).jev_model || "jev-latest",
380
+ state: stateSnippet,
381
+ questions: {
382
+ task_category: {
383
+ type: "choice",
384
+ instructions: "Classify the developer's intent into the single most appropriate task category.",
385
+ criteria,
386
+ },
387
+ },
388
+ }),
389
+ signal: jevController.signal,
390
+ });
391
+
392
+ clearTimeout(jevTimeout);
393
+ if (ctx.hasUI && ctx.ui.setStatus) {
394
+ ctx.ui.setStatus("laya-router", undefined);
395
+ }
396
+
397
+ if (jevRes.ok) {
398
+ const jevData = await jevRes.json() as any;
399
+ const taskAnswer = jevData?.answers?.task_category;
400
+ const selectedCategory = taskAnswer?.choice || taskAnswer?.value;
401
+ lastPredictedCategory = selectedCategory;
402
+
403
+ if (selectedCategory && config.routes[selectedCategory]) {
404
+ const targetRoute = config.routes[selectedCategory];
405
+ const targetModel = ctx.modelRegistry.find(targetRoute.provider, targetRoute.model);
406
+ if (targetModel) {
407
+ const currentModel = ctx.model || (pi as any).activeModel;
408
+ const isSame = currentModel && currentModel.id === targetRoute.model && currentModel.provider === targetRoute.provider;
409
+ const effectiveThinking = targetRoute.thinking
410
+ ? clampThinkingLevel(targetModel, targetRoute.thinking)
411
+ : pi.getThinkingLevel();
412
+
413
+ if (!isSame) {
414
+ const switched = await pi.setModel(targetModel);
415
+ if (switched) {
416
+ if (effectiveThinking) pi.setThinkingLevel(effectiveThinking);
417
+ if (ctx.hasUI) {
418
+ ctx.ui.notify(
419
+ `⚡ [Jev Fallback] Switched → ${selectedCategory} [${targetRoute.provider}/${targetRoute.model}]`,
420
+ "info"
421
+ );
422
+ }
423
+ }
424
+ }
425
+ }
426
+ }
427
+ return;
428
+ }
429
+ } catch {}
430
+ }
431
+
432
+ if (ctx.hasUI && !apiKey) {
433
+ ctx.ui.notify(
434
+ "⚡ [Router] Laya daemon offline. Run 'laya start' in terminal or check 'laya status'.",
435
+ "warning"
436
+ );
437
+ }
438
+ }
439
+ });
440
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "pi-laya-mlx",
3
+ "version": "0.1.0",
4
+ "description": "Laya MLX local semantic auto-router & optimizer for Pi coding agent",
5
+ "type": "module",
6
+ "bin": {
7
+ "laya": "./bin/laya.mjs"
8
+ },
9
+ "keywords": [
10
+ "pi-package",
11
+ "pi-extension",
12
+ "laya",
13
+ "mlx",
14
+ "router",
15
+ "apple-silicon"
16
+ ],
17
+ "pi": {
18
+ "extensions": [
19
+ "./extensions/laya-router.ts"
20
+ ],
21
+ "skills": [
22
+ "./skills/laya-tune"
23
+ ],
24
+ "image": "https://raw.githubusercontent.com/itzmail/pi-laya-mlx/main/assets/banner.png"
25
+ },
26
+ "scripts": {
27
+ "start": "./bin/laya.mjs start",
28
+ "stop": "./bin/laya.mjs stop",
29
+ "status": "./bin/laya.mjs status",
30
+ "reload": "./bin/laya.mjs reload"
31
+ },
32
+ "peerDependencies": {
33
+ "@earendil-works/pi-agent-core": "*",
34
+ "@earendil-works/pi-ai": "*",
35
+ "@earendil-works/pi-coding-agent": "*",
36
+ "@earendil-works/pi-tui": "*",
37
+ "typebox": "*"
38
+ },
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "license": "MIT"
43
+ }
@@ -0,0 +1,47 @@
1
+ # Laya Router Tuning & Evaluation Skill
2
+
3
+ When invoked, spawn an isolated background subagent via `Agent` tool to inspect logs, resolve flagged misclassifications, update semantic anchors, and trigger daemon hot-reload.
4
+
5
+ ---
6
+
7
+ ## 1. Spawn Tuning Subagent
8
+
9
+ The Main Agent should immediately spawn a subagent:
10
+
11
+ ```javascript
12
+ Agent({
13
+ subagent_type: "general-purpose",
14
+ description: "Tune Laya semantic anchors",
15
+ run_in_background: false,
16
+ prompt: `
17
+ You are the Laya Tuning Subagent. Perform semantic anchor optimization:
18
+
19
+ 1. INSPECT DATA:
20
+ - Read user corrections: ~/.pi/agent/laya/corrections.jsonl or daemon/corrections.jsonl
21
+ - Read latest 40-50 lines: ~/.pi/agent/laya/predictions.jsonl or daemon/predictions.jsonl
22
+ - Read current anchors: ~/.pi/agent/laya/anchors.json or daemon/anchors.json
23
+
24
+ 2. ANALYZE & TUNE:
25
+ - Find misclassified prompts and low-confidence items (< 0.35).
26
+ - Determine the best category for each flagged prompt.
27
+ - Append relevant new exemplar phrases into anchors.json under the appropriate category.
28
+
29
+ 3. HOT-RELOAD & CLEANUP:
30
+ - Trigger in-memory hot-reload via HTTP POST:
31
+ curl -s -X POST http://127.0.0.1:4141/reload-anchors
32
+ - Clear/archive resolved entries in corrections.jsonl (truncate to empty file or clear handled lines).
33
+ - Run sample prediction probes via curl to verify resolution.
34
+
35
+ 4. RETURN CONCISE SUMMARY:
36
+ - Number of corrections resolved.
37
+ - Categories updated & new exemplars added.
38
+ - Verification status.
39
+ `
40
+ })
41
+ ```
42
+
43
+ ---
44
+
45
+ ## 2. Report to User
46
+
47
+ When the subagent completes, display a concise summary showing what anchors were updated and verified.