mlx-bun 0.0.5 → 0.0.6
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/README.md +11 -2
- package/docs/dag/training-inference-map.html +575 -0
- package/docs/design/memory-system.md +193 -295
- package/docs/design/orpo-dynamic-lambda.md +179 -0
- package/docs/design/orpo-future-enhancements.md +42 -0
- package/docs/design/orpo-training.md +13 -0
- package/docs/design/parity-tier-dag.md +143 -0
- package/docs/design/w4a16-compute-precision-spike.md +232 -0
- package/docs/investigations/orpo-base-uf-experiment-and-directions.md +120 -0
- package/docs/investigations/orpo-flash-cce-pin-leak.md +167 -0
- package/docs/investigations/orpo-uf-testing-handoff.md +126 -0
- package/docs/investigations/steel-flash-cce-handoff.md +58 -0
- package/docs/investigations/trainer-validation-experiment.md +173 -0
- package/docs/reference/memory.md +77 -0
- package/docs/reference/orpo-quickstart.md +44 -5
- package/docs/reference/training.md +9 -5
- package/package.json +1 -1
- package/src/cli.ts +651 -18
- package/src/eval/runner.ts +76 -6
- package/src/eval/tasks/bfcl.ts +49 -15
- package/src/eval/tasks/gsm8k.ts +21 -9
- package/src/eval/tasks/hashhop.ts +33 -4
- package/src/eval/tasks/humaneval.ts +30 -8
- package/src/eval/tasks/ifeval.ts +52 -18
- package/src/eval/tasks/mmlu.ts +85 -74
- package/src/jobs/runner.ts +6 -0
- package/src/memory/chunk.ts +25 -0
- package/src/memory/cluster.ts +24 -0
- package/src/memory/db.ts +114 -0
- package/src/memory/ingest.ts +22 -0
- package/src/memory/pipeline.ts +63 -0
- package/src/memory/prompts.ts +23 -0
- package/src/memory/schedule.ts +163 -0
- package/src/memory/synthesize.ts +29 -0
- package/src/memory/tools.ts +399 -0
- package/src/memory/vault.ts +694 -0
- package/src/memory/wikify.ts +23 -0
- package/src/mlx/metal-kernel.ts +13 -0
- package/src/model/flash-attention.ts +5 -3
- package/src/pi-provider.ts +29 -13
- package/src/pi-session.ts +81 -0
- package/src/pi-terminal.ts +13 -24
- package/src/pi-web.ts +246 -166
- package/src/quantize/job.ts +17 -2
- package/src/quantize/quantizer.ts +26 -2
- package/src/server.ts +213 -44
- package/src/tokenizer.ts +6 -2
- package/src/train/dataset.ts +11 -3
- package/src/train/flash-cce.ts +18 -5
- package/src/train/job.ts +41 -4
- package/src/train/loss.ts +10 -4
- package/src/train/trainer.ts +62 -3
- package/src/train/watch.ts +542 -0
- package/src/web/app.html +264 -14
- package/src/web/skills/memory/SKILL.md +97 -0
- package/src/web/skills/web-research/SKILL.md +3 -1
- package/src/web/skills.ts +46 -8
package/src/pi-web.ts
CHANGED
|
@@ -23,43 +23,50 @@
|
|
|
23
23
|
// reason } to deny. Read-only tools (read/grep/find/ls) auto-allow.
|
|
24
24
|
// tool_execution_* events still drive the tool cards (start/update/end).
|
|
25
25
|
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
26
27
|
import { mkdirSync, rmSync } from "node:fs";
|
|
27
28
|
import { homedir } from "node:os";
|
|
28
29
|
import { join, resolve } from "node:path";
|
|
29
30
|
import type { ServerWebSocket, WebSocketHandler } from "bun";
|
|
30
31
|
import {
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
createAgentSessionFromServices,
|
|
33
|
+
createAgentSessionRuntime,
|
|
34
|
+
createAgentSessionServices,
|
|
33
35
|
SessionManager,
|
|
34
36
|
type AgentSession,
|
|
35
37
|
type AgentSessionEvent,
|
|
38
|
+
type AgentSessionRuntime,
|
|
39
|
+
type CreateAgentSessionRuntimeFactory,
|
|
36
40
|
type ExtensionAPI,
|
|
37
41
|
type SessionEntry,
|
|
38
42
|
type SessionInfo,
|
|
39
43
|
type ToolCallEvent,
|
|
40
44
|
} from "@earendil-works/pi-coding-agent";
|
|
41
45
|
import type { ImageContent } from "@earendil-works/pi-ai";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
46
|
+
import { WEB_TOOL_NAMES } from "./web-tools";
|
|
47
|
+
import { MEMORY_TOOL_NAMES, REFERENCE_TOOL_NAMES } from "./memory/tools";
|
|
48
|
+
import { buildPiAgentSurface } from "./pi-session";
|
|
44
49
|
import { buildPiProvider, DEFAULT_CONTEXT_WINDOW, PI_LOCAL_MODEL_ID } from "./pi-provider";
|
|
45
50
|
import { downloadsSnapshot } from "./download";
|
|
46
51
|
|
|
47
|
-
/**
|
|
48
|
-
* Full toolset the web agent is offered. The built-in coding tools plus our
|
|
49
|
-
* outward-facing web tools (web_search/web_fetch/weather). NOTE: pi treats
|
|
50
|
-
* this list as an allowlist (createAgentSession `tools`), so a custom tool's
|
|
51
|
-
* name MUST appear here or it gets filtered out before the model ever sees it.
|
|
52
|
-
*/
|
|
53
|
-
const ALL_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls", ...WEB_TOOL_NAMES];
|
|
54
52
|
/**
|
|
55
53
|
* Tools that never mutate the user's machine; auto-allowed without a browser
|
|
56
54
|
* round-trip. The web tools make outbound network requests but change nothing
|
|
57
55
|
* locally, so they're auto-allowed too (and remain usable in read-only mode).
|
|
58
56
|
*/
|
|
59
|
-
const READ_ONLY_TOOLS = new Set(["read", "grep", "find", "ls", ...WEB_TOOL_NAMES]);
|
|
57
|
+
const READ_ONLY_TOOLS = new Set(["read", "grep", "find", "ls", ...WEB_TOOL_NAMES, ...MEMORY_TOOL_NAMES, ...REFERENCE_TOOL_NAMES]);
|
|
60
58
|
/** Tools that require explicit per-call browser approval. */
|
|
61
59
|
const GATED_TOOLS = new Set(["bash", "edit", "write"]);
|
|
62
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The welcome assistant's tool allowlist: exactly `read` (a local file the user
|
|
63
|
+
* points to) and `web_search` (current/external facts). Both are in
|
|
64
|
+
* READ_ONLY_TOOLS, so neither triggers the approval gate. Kept to two because a
|
|
65
|
+
* 1B model over-calls a larger toolset; widen this list (not the system prompt)
|
|
66
|
+
* to grant more.
|
|
67
|
+
*/
|
|
68
|
+
const WELCOME_TOOLS = ["read", "web_search"] as const;
|
|
69
|
+
|
|
63
70
|
/** Auto-deny a pending approval after this long with no browser decision. */
|
|
64
71
|
const APPROVAL_TIMEOUT_MS = 120_000;
|
|
65
72
|
|
|
@@ -67,72 +74,51 @@ const APPROVAL_TIMEOUT_MS = 120_000;
|
|
|
67
74
|
* System prompt for the web chat assistant.
|
|
68
75
|
*
|
|
69
76
|
* This fully REPLACES pi's default coding-agent prompt (it flows to
|
|
70
|
-
* buildSystemPrompt's `customPrompt` via DefaultResourceLoader.systemPrompt)
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* current date and working directory, so we don't repeat them here.
|
|
77
|
+
* buildSystemPrompt's `customPrompt` via DefaultResourceLoader.systemPrompt),
|
|
78
|
+
* dropping pi's "operating inside a coding-agent harness" framing and its block
|
|
79
|
+
* of internal documentation paths. pi still auto-appends the current date and
|
|
80
|
+
* working directory, so we don't repeat them here.
|
|
75
81
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
82
|
+
* Deliberately SHORT. The default served model is a ~1B local model; a long,
|
|
83
|
+
* "make the user feel welcome / here is everything mlx-bun does" prompt made it
|
|
84
|
+
* fixate on greeting and ignore the user's actual message (verified against the
|
|
85
|
+
* live server: the same model answers correctly with a short prompt and drowns
|
|
86
|
+
* with the long one). So: state identity + privacy in one breath, then tell it
|
|
87
|
+
* plainly to answer what was asked and not to greet or recite capabilities.
|
|
79
88
|
*/
|
|
89
|
+
export const WEB_CHAT_PROMPT_VERSION = "2026-06-21-minimal-v1";
|
|
90
|
+
|
|
91
|
+
export function webChatPromptFingerprint(prompt: string): string {
|
|
92
|
+
return createHash("sha256").update(prompt).digest("hex").slice(0, 12);
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
export function buildWebChatSystemPrompt(
|
|
81
96
|
readOnly: boolean,
|
|
82
97
|
about?: { modelId?: string; downloadingModel?: string | null },
|
|
98
|
+
opts?: { hasTools?: boolean },
|
|
83
99
|
): string {
|
|
84
|
-
const capabilities = readOnly
|
|
85
|
-
? `You have these tools (all used freely, no confirmation needed):
|
|
86
|
-
- web_search — search the web for current, real-world information
|
|
87
|
-
- web_fetch — fetch a URL and read the page or data behind it
|
|
88
|
-
- weather — get current conditions and a short forecast for any place
|
|
89
|
-
- read — open and read any file
|
|
90
|
-
- ls, find, grep — list directories and search the filesystem by name or content
|
|
91
|
-
|
|
92
|
-
This is a read-only session: you can look things up and inspect files, but cannot run commands or change anything on disk.`
|
|
93
|
-
: `You have a real toolset — use it.
|
|
94
|
-
|
|
95
|
-
Used freely, no confirmation needed:
|
|
96
|
-
- web_search — search the web for current, real-world information (news, prices, docs, anything that changes or that you don't already know)
|
|
97
|
-
- web_fetch — fetch a URL and read the page or data behind it (great as a follow-up to web_search, or when the user gives you a link)
|
|
98
|
-
- weather — get current conditions and a short forecast for any place
|
|
99
|
-
- read, ls, find, grep — open files and search the filesystem
|
|
100
|
-
|
|
101
|
-
Asks the user for approval first (they stay in control):
|
|
102
|
-
- bash — run shell commands
|
|
103
|
-
- edit, write — modify existing files or create new ones
|
|
104
|
-
|
|
105
|
-
Prefer doing the work over describing it — run the search, fetch the page, propose the concrete command or edit and let the user approve it.`;
|
|
106
|
-
|
|
107
100
|
const servedModel =
|
|
108
101
|
about?.modelId && about.modelId !== PI_LOCAL_MODEL_ID ? about.modelId : null;
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
${aboutMlxBun}
|
|
130
|
-
|
|
131
|
-
${capabilities}
|
|
132
|
-
|
|
133
|
-
When you don't know something — especially anything current, factual, or time-sensitive — look it up with web_search and web_fetch rather than speculating or relying on stale memory (questions about mlx-bun itself are the exception — use the facts above). If a request is genuinely ambiguous, ask one brief clarifying question; otherwise make a sensible choice and proceed. Be honest about what you did and what you found — including mistakes, dead ends, and things you couldn't do.
|
|
134
|
-
|
|
135
|
-
Keep your responses clear and to the point. Format with Markdown, cite links when you used the web, and show file paths and commands plainly so they're easy to read.`;
|
|
102
|
+
const modelLine = servedModel ? ` You are running on the local model \`${servedModel}\`.` : "";
|
|
103
|
+
|
|
104
|
+
// A concise mlx-bun blurb so the welcome assistant can answer product
|
|
105
|
+
// questions from its own knowledge (no tool, no fragile cwd-relative reads).
|
|
106
|
+
// Kept SHORT on purpose — the old multi-paragraph product wall drowned the 1B
|
|
107
|
+
// model. The blurb + the two key commands cover "what is it / how do I start".
|
|
108
|
+
const aboutLine = ` mlx-bun runs open LLMs locally with MLX + Bun: a built-in chat, an OpenAI/Anthropic-compatible API, model download and serving, quantization, LoRA fine-tuning, and adapters — all on-device and private. Key commands: \`mlx-bun serve <model>\` to serve one, \`mlx-bun get <repo-id>\` to download one. You already know this, so answer questions about mlx-bun directly.`;
|
|
109
|
+
|
|
110
|
+
// The tool guidance MUST match the session's actual surface (read +
|
|
111
|
+
// web_search), or the model promises actions it can't take. Naming exactly
|
|
112
|
+
// those two — and telling it to answer from knowledge first — is what keeps a
|
|
113
|
+
// small model from reaching for a tool on math/writing/general questions.
|
|
114
|
+
const hasTools = opts?.hasTools ?? true;
|
|
115
|
+
const toolsLine = hasTools
|
|
116
|
+
? ` Answer directly from your own knowledge whenever you can. Only call a tool when you truly need information you don't have: \`web_search\` for current or external facts (news, current events, prices, latest docs), \`read\` for a specific local file the user points you to. Never use a tool for general questions, explanations, math, or writing — just answer. When a tool returns results, pull the answer out of the result text and state it directly — never tell the user to go open the links or check the sources themselves; reading them is your job.`
|
|
117
|
+
: ` You have no tools in this session, so answer from your own knowledge; if something needs current or external data you can't reach, say so briefly instead of pretending to look it up.`;
|
|
118
|
+
|
|
119
|
+
return `You are mlx-bun's built-in assistant, running entirely on the user's own Apple-silicon Mac — nothing they type leaves the machine.${modelLine}${aboutLine}${toolsLine}
|
|
120
|
+
|
|
121
|
+
Respond to what the user actually said, concisely. Don't open with a generic greeting or recite your capabilities unless asked — just answer. If a request is genuinely ambiguous, ask one short clarifying question. Format with Markdown when it helps.`;
|
|
136
122
|
}
|
|
137
123
|
|
|
138
124
|
/** Per-connection data the server attaches at upgrade time. */
|
|
@@ -156,7 +142,6 @@ interface ImageAttachment {
|
|
|
156
142
|
/** Client -> server frames. */
|
|
157
143
|
type ClientMessage =
|
|
158
144
|
| { type: "prompt"; text: string; images?: ImageAttachment[] }
|
|
159
|
-
| { type: "steer"; text: string; images?: ImageAttachment[] }
|
|
160
145
|
| { type: "abort" }
|
|
161
146
|
| { type: "approval"; callId: string; decision: ApprovalDecision }
|
|
162
147
|
// Toggle the model's reasoning channel on/off (only meaningful when the
|
|
@@ -167,6 +152,12 @@ type ClientMessage =
|
|
|
167
152
|
// app.html mounts it (POST /v1/adapters) before sending this; the
|
|
168
153
|
// before_provider_request hook injects it into the provider payload.
|
|
169
154
|
| { type: "set_adapter"; id: string | null }
|
|
155
|
+
// Per-request sampling overrides for subsequent turns. Each field is
|
|
156
|
+
// optional; null/undefined means "leave it to the server default"
|
|
157
|
+
// (the mode-aware recommended value resolved in toOptions). A present
|
|
158
|
+
// numeric value is injected into the provider payload and always wins.
|
|
159
|
+
// The before_provider_request hook injects whatever is set here.
|
|
160
|
+
| { type: "set_sampling"; temperature?: number | null; top_p?: number | null; top_k?: number | null }
|
|
170
161
|
// Session management (recent-chats sidebar + new chat).
|
|
171
162
|
| { type: "new_session" }
|
|
172
163
|
| { type: "list_sessions" }
|
|
@@ -411,11 +402,49 @@ export function injectAdapter(
|
|
|
411
402
|
return { ...payload, adapter: selected };
|
|
412
403
|
}
|
|
413
404
|
|
|
405
|
+
/** Per-request sampling overrides carried on the session and injected into the
|
|
406
|
+
* provider payload by the before_provider_request hook. Each field is either a
|
|
407
|
+
* user-set override or null/undefined ("use the server's mode-aware default"). */
|
|
408
|
+
export interface SamplingOverrides {
|
|
409
|
+
temperature?: number | null;
|
|
410
|
+
top_p?: number | null;
|
|
411
|
+
top_k?: number | null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Inject the user's sampling overrides into the outgoing chat-completions
|
|
415
|
+
* payload (which reaches the server's toOptions, where an explicit
|
|
416
|
+
* temperature/top_p/top_k always wins). Only finite numbers are injected; a
|
|
417
|
+
* null/undefined/unset field is left off so the server falls back to its
|
|
418
|
+
* mode-aware recommended default. Returns undefined when nothing is set so the
|
|
419
|
+
* hook can keep the payload unchanged. Pure + exported for unit testing. */
|
|
420
|
+
export function injectSampling(
|
|
421
|
+
payload: Record<string, unknown>,
|
|
422
|
+
s: SamplingOverrides | undefined,
|
|
423
|
+
): Record<string, unknown> | undefined {
|
|
424
|
+
if (!s) return undefined;
|
|
425
|
+
const out: Record<string, unknown> = { ...payload };
|
|
426
|
+
let changed = false;
|
|
427
|
+
if (typeof s.temperature === "number" && Number.isFinite(s.temperature)) {
|
|
428
|
+
out.temperature = s.temperature;
|
|
429
|
+
changed = true;
|
|
430
|
+
}
|
|
431
|
+
if (typeof s.top_p === "number" && Number.isFinite(s.top_p)) {
|
|
432
|
+
out.top_p = s.top_p;
|
|
433
|
+
changed = true;
|
|
434
|
+
}
|
|
435
|
+
if (typeof s.top_k === "number" && Number.isFinite(s.top_k)) {
|
|
436
|
+
out.top_k = s.top_k;
|
|
437
|
+
changed = true;
|
|
438
|
+
}
|
|
439
|
+
return changed ? out : undefined;
|
|
440
|
+
}
|
|
441
|
+
|
|
414
442
|
/**
|
|
415
443
|
* One browser connection's pi agent. Owns the AgentSession, the event
|
|
416
444
|
* subscription, and the pending tool-approval handshakes.
|
|
417
445
|
*/
|
|
418
446
|
class PiWebSession {
|
|
447
|
+
private runtime?: AgentSessionRuntime;
|
|
419
448
|
private session?: AgentSession;
|
|
420
449
|
/** SessionManager backing the active AgentSession (disk-persisted). */
|
|
421
450
|
private sessionManager?: SessionManager;
|
|
@@ -424,6 +453,10 @@ class PiWebSession {
|
|
|
424
453
|
/** Active LoRA adapter id for this connection (null = none/base model).
|
|
425
454
|
* Read by the before_provider_request hook; set via the set_adapter msg. */
|
|
426
455
|
private selectedAdapter: string | null = null;
|
|
456
|
+
/** Per-request sampling overrides for this connection (set via set_sampling).
|
|
457
|
+
* Read by the before_provider_request hook; unset fields fall back to the
|
|
458
|
+
* server's mode-aware recommended defaults. */
|
|
459
|
+
private sampling: SamplingOverrides = {};
|
|
427
460
|
|
|
428
461
|
/** Per-connection invariants, built once in start() and reused across
|
|
429
462
|
* session switches (new chat / resume / fork). */
|
|
@@ -458,6 +491,7 @@ class PiWebSession {
|
|
|
458
491
|
this.provider = buildPiProvider(baseUrl, {
|
|
459
492
|
contextWindow: this.opts.contextWindow,
|
|
460
493
|
reasoning: this.opts.thinking,
|
|
494
|
+
vision: this.opts.vision,
|
|
461
495
|
});
|
|
462
496
|
mkdirSync(this.sessionDir, { recursive: true });
|
|
463
497
|
|
|
@@ -470,7 +504,7 @@ class PiWebSession {
|
|
|
470
504
|
// its own session on a transient reconnect so a blip doesn't strand it on
|
|
471
505
|
// a blank backend session. Sessions persist to disk in pi's own format —
|
|
472
506
|
// the substrate for the recent-chats sidebar and the nightly memory pipeline.
|
|
473
|
-
await this.
|
|
507
|
+
await this.replaceRuntime(SessionManager.create(this.cwd, this.sessionDir));
|
|
474
508
|
if (this.disposed) return;
|
|
475
509
|
|
|
476
510
|
this.send({ type: "ready", model: this.opts.modelId, vision: this.opts.vision, thinking: this.opts.thinking });
|
|
@@ -478,97 +512,116 @@ class PiWebSession {
|
|
|
478
512
|
await this.sendSessions();
|
|
479
513
|
}
|
|
480
514
|
|
|
481
|
-
/**
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
// AGENTS.md in the launch directory out of the chat.
|
|
497
|
-
noExtensions: true,
|
|
498
|
-
noSkills: true,
|
|
499
|
-
noPromptTemplates: true,
|
|
500
|
-
noThemes: true,
|
|
501
|
-
noContextFiles: true,
|
|
502
|
-
// Our curated skills only (not the user's). materializeBundledSkills is
|
|
503
|
-
// memoized, so calling it per build just returns the path. See web/skills.ts.
|
|
504
|
-
additionalSkillPaths: [materializeBundledSkills()],
|
|
505
|
-
// Replace pi's default coding-agent prompt with our own helpful,
|
|
506
|
-
// eager-assistant persona (see buildWebChatSystemPrompt). Pass the
|
|
507
|
-
// served model id + any in-progress background download so the
|
|
508
|
-
// loading-screen assistant knows what it is and what's coming.
|
|
509
|
-
systemPrompt: buildWebChatSystemPrompt(this.opts.readOnly, {
|
|
515
|
+
/** Runtime factory used for initial session creation and SDK-managed replacements. */
|
|
516
|
+
private createRuntimeFactory(): CreateAgentSessionRuntimeFactory {
|
|
517
|
+
return async ({ cwd, sessionManager, sessionStartEvent }) => {
|
|
518
|
+
const provider = this.provider;
|
|
519
|
+
if (!provider) throw new Error("provider not initialized");
|
|
520
|
+
|
|
521
|
+
// Welcome-assistant tool surface: exactly two read-only tools —
|
|
522
|
+
// `web_search` (current/external facts) and `read` (a local file the user
|
|
523
|
+
// points to). Both auto-allow (no approval round-trip). We deliberately
|
|
524
|
+
// do NOT expose web_fetch/weather/bash/edit/write/grep/find/ls: a 1B model
|
|
525
|
+
// over-calls a big toolset. With thinking ON (the web chat default) it
|
|
526
|
+
// uses these two appropriately; build the web tools (for the web_search
|
|
527
|
+
// definition) then restrict the allowlist to the two names below.
|
|
528
|
+
const surface = await buildPiAgentSurface({ webTools: true, codingTools: false });
|
|
529
|
+
const webPrompt = buildWebChatSystemPrompt(this.opts.readOnly, {
|
|
510
530
|
modelId: this.opts.modelId,
|
|
511
531
|
downloadingModel: downloadsSnapshot().find(
|
|
512
532
|
(d) => d.state === "active" && d.repoId !== this.opts.modelId,
|
|
513
533
|
)?.repoId ?? null,
|
|
514
|
-
})
|
|
515
|
-
|
|
516
|
-
(pi
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
534
|
+
}, { hasTools: WELCOME_TOOLS.length > 0 }) + surface.memoryHint;
|
|
535
|
+
if (process.env.MLX_BUN_PI_DEBUG) {
|
|
536
|
+
console.error(`[pi-web] prompt ${WEB_CHAT_PROMPT_VERSION} sha=${webChatPromptFingerprint(webPrompt)} memory=${surface.memoryEnabled ? "on" : "off"}`);
|
|
537
|
+
}
|
|
538
|
+
const services = await createAgentSessionServices({
|
|
539
|
+
cwd,
|
|
540
|
+
agentDir: this.agentDir,
|
|
541
|
+
authStorage: provider.authStorage,
|
|
542
|
+
modelRegistry: provider.modelRegistry,
|
|
543
|
+
resourceLoaderOptions: {
|
|
544
|
+
noExtensions: true,
|
|
545
|
+
noSkills: true,
|
|
546
|
+
noPromptTemplates: true,
|
|
547
|
+
noThemes: true,
|
|
548
|
+
noContextFiles: true,
|
|
549
|
+
additionalSkillPaths: surface.skillPaths,
|
|
550
|
+
systemPrompt: webPrompt,
|
|
551
|
+
extensionFactories: [
|
|
552
|
+
(pi) => this.installApprovalGate(pi),
|
|
553
|
+
(pi) => this.installAdapterHook(pi),
|
|
554
|
+
],
|
|
555
|
+
},
|
|
556
|
+
});
|
|
521
557
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
558
|
+
return {
|
|
559
|
+
...(await createAgentSessionFromServices({
|
|
560
|
+
services,
|
|
561
|
+
sessionManager,
|
|
562
|
+
sessionStartEvent,
|
|
563
|
+
model: provider.model,
|
|
564
|
+
// Allowlist (not surface.tools): `read` is a pi built-in enabled by
|
|
565
|
+
// name; `web_search` is the custom tool from surface.customTools. pi
|
|
566
|
+
// exposes only names in this list, so web_fetch/weather stay defined
|
|
567
|
+
// but hidden.
|
|
568
|
+
tools: [...WELCOME_TOOLS],
|
|
569
|
+
customTools: surface.customTools,
|
|
570
|
+
})),
|
|
571
|
+
services,
|
|
572
|
+
diagnostics: services.diagnostics,
|
|
573
|
+
};
|
|
574
|
+
};
|
|
534
575
|
}
|
|
535
576
|
|
|
536
|
-
/** Tear down
|
|
537
|
-
|
|
577
|
+
/** Tear down UI/session bindings owned by this WebSocket. Runtime.dispose()
|
|
578
|
+
* owns the AgentSession itself; this method only detaches browser state. */
|
|
579
|
+
private teardownBindings(): void {
|
|
538
580
|
for (const settle of this.pendingApprovals.values()) settle("deny");
|
|
539
581
|
this.pendingApprovals.clear();
|
|
540
582
|
for (const timer of this.approvalTimers.values()) clearTimeout(timer);
|
|
541
583
|
this.approvalTimers.clear();
|
|
542
584
|
this.unsubscribe?.();
|
|
543
585
|
this.unsubscribe = undefined;
|
|
544
|
-
try {
|
|
545
|
-
this.session?.dispose();
|
|
546
|
-
} catch {
|
|
547
|
-
// Never let a dispose error escape teardown.
|
|
548
|
-
}
|
|
549
586
|
this.session = undefined;
|
|
550
587
|
this.sessionManager = undefined;
|
|
551
588
|
}
|
|
552
589
|
|
|
553
|
-
/**
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
590
|
+
/** Bind browser event plumbing to the current runtime.session. */
|
|
591
|
+
private async bindRuntimeSession(): Promise<void> {
|
|
592
|
+
const session = this.runtime?.session;
|
|
593
|
+
if (!session) return;
|
|
594
|
+
this.unsubscribe?.();
|
|
595
|
+
this.session = session;
|
|
596
|
+
this.sessionManager = session.sessionManager;
|
|
597
|
+
await session.bindExtensions({ mode: "rpc" });
|
|
598
|
+
this.unsubscribe = session.subscribe((event) => this.onSessionEvent(event));
|
|
599
|
+
this.sendContextUsage();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Replace the whole pi runtime with a target SessionManager. Used for the
|
|
603
|
+
* initial session and for file-level fork, while open/new use runtime APIs. */
|
|
604
|
+
private async replaceRuntime(sm: SessionManager): Promise<void> {
|
|
557
605
|
try {
|
|
558
606
|
await this.session?.abort();
|
|
559
607
|
} catch {
|
|
560
608
|
// Old turn may already be done; ignore.
|
|
561
609
|
}
|
|
562
|
-
const
|
|
610
|
+
const next = await createAgentSessionRuntime(this.createRuntimeFactory(), {
|
|
611
|
+
cwd: this.cwd,
|
|
612
|
+
agentDir: this.agentDir,
|
|
613
|
+
sessionManager: sm,
|
|
614
|
+
});
|
|
615
|
+
next.setRebindSession(async () => this.bindRuntimeSession());
|
|
563
616
|
if (this.disposed) {
|
|
564
|
-
|
|
617
|
+
await next.dispose();
|
|
565
618
|
return;
|
|
566
619
|
}
|
|
567
|
-
this.
|
|
568
|
-
this.
|
|
569
|
-
this.
|
|
570
|
-
|
|
571
|
-
|
|
620
|
+
const previous = this.runtime;
|
|
621
|
+
this.teardownBindings();
|
|
622
|
+
this.runtime = next;
|
|
623
|
+
await this.bindRuntimeSession();
|
|
624
|
+
if (previous) await previous.dispose();
|
|
572
625
|
}
|
|
573
626
|
|
|
574
627
|
/** Replay the active session's transcript to the browser (rebuilds thread). */
|
|
@@ -609,7 +662,11 @@ class PiWebSession {
|
|
|
609
662
|
|
|
610
663
|
/** Start a fresh chat (old one stays on disk, resumable from the sidebar). */
|
|
611
664
|
private async newSession(): Promise<void> {
|
|
612
|
-
|
|
665
|
+
if (!this.runtime) {
|
|
666
|
+
await this.replaceRuntime(SessionManager.create(this.cwd, this.sessionDir));
|
|
667
|
+
} else {
|
|
668
|
+
await this.runtime.newSession();
|
|
669
|
+
}
|
|
613
670
|
this.sendHistory();
|
|
614
671
|
await this.sendSessions();
|
|
615
672
|
}
|
|
@@ -620,7 +677,8 @@ class PiWebSession {
|
|
|
620
677
|
this.send({ type: "error", message: "invalid session path" });
|
|
621
678
|
return;
|
|
622
679
|
}
|
|
623
|
-
await this.
|
|
680
|
+
if (!this.runtime) await this.replaceRuntime(SessionManager.open(path, this.sessionDir));
|
|
681
|
+
else await this.runtime.switchSession(path);
|
|
624
682
|
this.sendHistory();
|
|
625
683
|
await this.sendSessions();
|
|
626
684
|
}
|
|
@@ -631,7 +689,9 @@ class PiWebSession {
|
|
|
631
689
|
this.send({ type: "error", message: "invalid session path" });
|
|
632
690
|
return;
|
|
633
691
|
}
|
|
634
|
-
|
|
692
|
+
// File-level fork is not a runtime primitive, so create the target
|
|
693
|
+
// SessionManager then replace the runtime through the same SDK factory.
|
|
694
|
+
await this.replaceRuntime(SessionManager.forkFrom(path, this.cwd, this.sessionDir));
|
|
635
695
|
this.sendHistory();
|
|
636
696
|
await this.sendSessions();
|
|
637
697
|
}
|
|
@@ -657,15 +717,24 @@ class PiWebSession {
|
|
|
657
717
|
* adapter into every provider request (Pi-native adapter control, mirrors the
|
|
658
718
|
* CLI extension). Default none = no injection (base model). */
|
|
659
719
|
private installAdapterHook(pi: ExtensionAPI): void {
|
|
660
|
-
pi.on("before_provider_request", (event) =>
|
|
661
|
-
|
|
662
|
-
|
|
720
|
+
pi.on("before_provider_request", (event) => {
|
|
721
|
+
let payload = event.payload as Record<string, unknown>;
|
|
722
|
+
// Layer both injections: adapter selection, then sampling overrides.
|
|
723
|
+
// Each returns undefined when it has nothing to change, so we keep the
|
|
724
|
+
// prior payload in that case.
|
|
725
|
+
payload = injectAdapter(payload, this.selectedAdapter) ?? payload;
|
|
726
|
+
payload = injectSampling(payload, this.sampling) ?? payload;
|
|
727
|
+
return payload;
|
|
728
|
+
});
|
|
663
729
|
}
|
|
664
730
|
|
|
665
731
|
/** Register the tool_call approval gate on the inline extension. */
|
|
666
732
|
private installApprovalGate(pi: ExtensionAPI): void {
|
|
667
733
|
pi.on("tool_call", async (event: ToolCallEvent) => {
|
|
668
734
|
const tool = event.toolName;
|
|
735
|
+
if (process.env.MLX_BUN_PI_DEBUG) {
|
|
736
|
+
console.error(`[pi-web] tool_call ${tool} args=${JSON.stringify(event.input)}`);
|
|
737
|
+
}
|
|
669
738
|
|
|
670
739
|
// Read-only tools never need approval.
|
|
671
740
|
if (READ_ONLY_TOOLS.has(tool)) return undefined;
|
|
@@ -675,8 +744,8 @@ class PiWebSession {
|
|
|
675
744
|
return { block: true, reason: "Read-only session: mutating tools are disabled." };
|
|
676
745
|
}
|
|
677
746
|
|
|
678
|
-
// Non-gated, non-read-only tools (shouldn't happen with
|
|
679
|
-
// but be safe): allow.
|
|
747
|
+
// Non-gated, non-read-only tools (shouldn't happen with the shared pi
|
|
748
|
+
// surface, but be safe): allow.
|
|
680
749
|
if (!GATED_TOOLS.has(tool)) return undefined;
|
|
681
750
|
|
|
682
751
|
const decision = await this.requestApproval(event);
|
|
@@ -768,26 +837,24 @@ class PiWebSession {
|
|
|
768
837
|
}
|
|
769
838
|
|
|
770
839
|
switch (msg.type) {
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
case "prompt":
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
// the live turn; if that turn just ended (same race), pi falls back to
|
|
788
|
-
// a normal prompt instead of dropping the message.
|
|
789
|
-
await session.prompt(msg.text, { streamingBehavior: "steer", images: toPiImages(msg.images) });
|
|
840
|
+
// The whole chat is just this: hand the user's message to pi's
|
|
841
|
+
// AgentSession and let pi run the turn (reply or tool calls, which pi
|
|
842
|
+
// executes). The ONLY decision we make is the canonical idle-vs-streaming
|
|
843
|
+
// branch from pi's own prompt() contract (core/agent-session.ts): an idle
|
|
844
|
+
// session runs a normal turn; if a turn is already streaming (the user
|
|
845
|
+
// typed again before it finished), the message is queued as a follow-up
|
|
846
|
+
// so it becomes the next turn instead of being dropped. pi re-checks
|
|
847
|
+
// isStreaming atomically inside prompt(), so this read can't strand a
|
|
848
|
+
// message at the turn boundary.
|
|
849
|
+
case "prompt": {
|
|
850
|
+
if (process.env.MLX_BUN_PI_DEBUG) {
|
|
851
|
+
console.error(`[pi-web] prompt text=${JSON.stringify(msg.text.slice(0, 500))} images=${msg.images?.length ?? 0} streaming=${session.isStreaming} adapter=${this.selectedAdapter ?? "base"}`);
|
|
852
|
+
}
|
|
853
|
+
const images = toPiImages(msg.images);
|
|
854
|
+
if (session.isStreaming) await session.prompt(msg.text, { streamingBehavior: "followUp", images });
|
|
855
|
+
else await session.prompt(msg.text, { images });
|
|
790
856
|
return;
|
|
857
|
+
}
|
|
791
858
|
case "abort":
|
|
792
859
|
await session.abort();
|
|
793
860
|
return;
|
|
@@ -805,13 +872,26 @@ class PiWebSession {
|
|
|
805
872
|
// app.html has already mounted it server-side (POST /v1/adapters).
|
|
806
873
|
this.selectedAdapter = msg.id;
|
|
807
874
|
return;
|
|
875
|
+
case "set_sampling":
|
|
876
|
+
// Record the per-request sampling overrides; the
|
|
877
|
+
// before_provider_request hook injects any set fields into the
|
|
878
|
+
// outgoing payload. A null/undefined field clears the override so the
|
|
879
|
+
// server's mode-aware default applies again.
|
|
880
|
+
this.sampling = {
|
|
881
|
+
temperature: msg.temperature ?? null,
|
|
882
|
+
top_p: msg.top_p ?? null,
|
|
883
|
+
top_k: msg.top_k ?? null,
|
|
884
|
+
};
|
|
885
|
+
return;
|
|
808
886
|
}
|
|
809
887
|
}
|
|
810
888
|
|
|
811
889
|
/** Tear down the session and reject any in-flight approvals. */
|
|
812
890
|
dispose(): void {
|
|
813
891
|
this.disposed = true;
|
|
814
|
-
this.
|
|
892
|
+
this.teardownBindings();
|
|
893
|
+
void this.runtime?.dispose();
|
|
894
|
+
this.runtime = undefined;
|
|
815
895
|
}
|
|
816
896
|
|
|
817
897
|
/** Send a frame, swallowing errors on a closed socket. */
|
package/src/quantize/job.ts
CHANGED
|
@@ -52,14 +52,29 @@ export const quantizeRunner: JobRunner = async (emit: Emit, config) => {
|
|
|
52
52
|
|
|
53
53
|
const srcDir = resolveSrcDir(config);
|
|
54
54
|
|
|
55
|
+
// Mixed-precision (OptiQ sensitivity sweep + knapsack) is triggered by
|
|
56
|
+
// targetBpw. The server/CLI send these as snake_case in the job config — they
|
|
57
|
+
// MUST be forwarded into opts or quantizeModelDir silently runs uniform.
|
|
58
|
+
const targetBpw = config.target_bpw != null ? Number(config.target_bpw) : undefined;
|
|
59
|
+
const mixed = targetBpw !== undefined;
|
|
60
|
+
|
|
55
61
|
emit({
|
|
56
62
|
type: "stage",
|
|
57
63
|
stage: "starting",
|
|
58
64
|
progress: 0.01,
|
|
59
|
-
message:
|
|
65
|
+
message: mixed
|
|
66
|
+
? `Quantizing ${srcDir} → mixed ${targetBpw} bpw (OptiQ sensitivity sweep, ~minutes)`
|
|
67
|
+
: `Quantizing ${srcDir} → ${bits}-bit (g${groupSize})`,
|
|
60
68
|
});
|
|
61
69
|
|
|
62
|
-
const opts: QuantizeOptions = {
|
|
70
|
+
const opts: QuantizeOptions = {
|
|
71
|
+
bits, groupSize, mode: String(config.mode ?? "affine"),
|
|
72
|
+
...(targetBpw !== undefined ? { targetBpw } : {}),
|
|
73
|
+
...(Array.isArray(config.candidate_bits) ? { candidateBits: (config.candidate_bits as number[]).map(Number) } : {}),
|
|
74
|
+
...(config.reference ? { reference: String(config.reference) } : {}),
|
|
75
|
+
...(config.calibration_mix ? { calibrationMix: String(config.calibration_mix) } : {}),
|
|
76
|
+
...(config.n_calibration != null ? { nCalibration: Number(config.n_calibration) } : {}),
|
|
77
|
+
};
|
|
63
78
|
|
|
64
79
|
const r = await quantizeModelDir(srcDir, outDir, opts, (e) =>
|
|
65
80
|
emit({ type: "stage", stage: e.stage, progress: e.progress, message: e.message }),
|