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.
Files changed (57) hide show
  1. package/README.md +11 -2
  2. package/docs/dag/training-inference-map.html +575 -0
  3. package/docs/design/memory-system.md +193 -295
  4. package/docs/design/orpo-dynamic-lambda.md +179 -0
  5. package/docs/design/orpo-future-enhancements.md +42 -0
  6. package/docs/design/orpo-training.md +13 -0
  7. package/docs/design/parity-tier-dag.md +143 -0
  8. package/docs/design/w4a16-compute-precision-spike.md +232 -0
  9. package/docs/investigations/orpo-base-uf-experiment-and-directions.md +120 -0
  10. package/docs/investigations/orpo-flash-cce-pin-leak.md +167 -0
  11. package/docs/investigations/orpo-uf-testing-handoff.md +126 -0
  12. package/docs/investigations/steel-flash-cce-handoff.md +58 -0
  13. package/docs/investigations/trainer-validation-experiment.md +173 -0
  14. package/docs/reference/memory.md +77 -0
  15. package/docs/reference/orpo-quickstart.md +44 -5
  16. package/docs/reference/training.md +9 -5
  17. package/package.json +1 -1
  18. package/src/cli.ts +651 -18
  19. package/src/eval/runner.ts +76 -6
  20. package/src/eval/tasks/bfcl.ts +49 -15
  21. package/src/eval/tasks/gsm8k.ts +21 -9
  22. package/src/eval/tasks/hashhop.ts +33 -4
  23. package/src/eval/tasks/humaneval.ts +30 -8
  24. package/src/eval/tasks/ifeval.ts +52 -18
  25. package/src/eval/tasks/mmlu.ts +85 -74
  26. package/src/jobs/runner.ts +6 -0
  27. package/src/memory/chunk.ts +25 -0
  28. package/src/memory/cluster.ts +24 -0
  29. package/src/memory/db.ts +114 -0
  30. package/src/memory/ingest.ts +22 -0
  31. package/src/memory/pipeline.ts +63 -0
  32. package/src/memory/prompts.ts +23 -0
  33. package/src/memory/schedule.ts +163 -0
  34. package/src/memory/synthesize.ts +29 -0
  35. package/src/memory/tools.ts +399 -0
  36. package/src/memory/vault.ts +694 -0
  37. package/src/memory/wikify.ts +23 -0
  38. package/src/mlx/metal-kernel.ts +13 -0
  39. package/src/model/flash-attention.ts +5 -3
  40. package/src/pi-provider.ts +29 -13
  41. package/src/pi-session.ts +81 -0
  42. package/src/pi-terminal.ts +13 -24
  43. package/src/pi-web.ts +246 -166
  44. package/src/quantize/job.ts +17 -2
  45. package/src/quantize/quantizer.ts +26 -2
  46. package/src/server.ts +213 -44
  47. package/src/tokenizer.ts +6 -2
  48. package/src/train/dataset.ts +11 -3
  49. package/src/train/flash-cce.ts +18 -5
  50. package/src/train/job.ts +41 -4
  51. package/src/train/loss.ts +10 -4
  52. package/src/train/trainer.ts +62 -3
  53. package/src/train/watch.ts +542 -0
  54. package/src/web/app.html +264 -14
  55. package/src/web/skills/memory/SKILL.md +97 -0
  56. package/src/web/skills/web-research/SKILL.md +3 -1
  57. package/src/web/skills.ts +46 -8
@@ -0,0 +1,23 @@
1
+ // mlx-bun memory — synthesis stage 5: WIKIFY (deterministic). ⚠️ M1 STUB.
2
+ //
3
+ // Non-model editorial cleanup after synthesis: canonicalize [[wikilinks]]
4
+ // (spaces↔underscores, case, resolve to existing stems), repair footnotes, and
5
+ // run the deterministic editorial gate. Non-fatal — a failure here logs and
6
+ // continues; it never corrupts an article. Mirrors lucien's wikify.ts +
7
+ // normalize-wikilinks.ts + normalize-footnotes.ts.
8
+ //
9
+ // See docs/design/memory-system.md → "The nightly pipeline" (wikify/normalize).
10
+
11
+ import type { SynthesisEvent } from "./pipeline";
12
+
13
+ export interface WikifyResult {
14
+ articlesNormalized: number;
15
+ linksFixed: number;
16
+ }
17
+
18
+ /** Deterministic editorial gate + link/footnote normalize. STUB. */
19
+ export async function wikify(_onEvent?: (e: SynthesisEvent) => void): Promise<WikifyResult> {
20
+ // TODO(M1): resolveWikilinkToStem across touched articles (vault.ts already
21
+ // has the resolver), footnote renumber/repair, {{stub}} marking.
22
+ throw new Error("memory.wikify is not implemented yet (M1)");
23
+ }
@@ -67,6 +67,11 @@ export class MetalKernel {
67
67
  apply(inputs: MlxArray[], call: MetalKernelCall): MlxArray[] {
68
68
  if (this.#disposed) throw new Error("MetalKernel used after dispose");
69
69
  const cfg = C.mlx_fast_metal_kernel_config_new();
70
+ // bun:ffi ptr() does NOT retain its argument, so any JS buffer whose pointer
71
+ // crosses an FFI call below is anchored here until that call returns —
72
+ // otherwise GC could free it mid-call (the lifetime class behind the
73
+ // flash-CCE pin bug). Pure-additive: holds references, changes no logic.
74
+ const keepAlive: unknown[] = [];
70
75
  try {
71
76
  // Derive outputs + grid from the (current) input shapes when a function is
72
77
  // given — so the SAME kernel object answers correctly for any input shape
@@ -76,6 +81,7 @@ export class MetalKernel {
76
81
  const grid = typeof call.grid === "function" ? call.grid(inputs) : call.grid;
77
82
  for (const o of outputs) {
78
83
  const shape = new Int32Array(o.shape);
84
+ keepAlive.push(shape);
79
85
  if (C.mlx_fast_metal_kernel_config_add_output_arg(cfg, ptr(shape), BigInt(o.shape.length), o.dtype) !== 0)
80
86
  throw new Error(`metal_kernel add_output_arg failed: ${takeMlxError() ?? ""}`);
81
87
  }
@@ -85,19 +91,26 @@ export class MetalKernel {
85
91
  C.mlx_fast_metal_kernel_config_set_init_value(cfg, call.initValue);
86
92
  for (const [k, v] of Object.entries(call.templateInts ?? {})) {
87
93
  const kb = Buffer.from(k + "\0", "utf8");
94
+ keepAlive.push(kb);
88
95
  C.mlx_fast_metal_kernel_config_add_template_arg_int(cfg, ptr(kb), v);
89
96
  }
90
97
  for (const [k, v] of Object.entries(call.templateDtypes ?? {})) {
91
98
  const kb = Buffer.from(k + "\0", "utf8");
99
+ keepAlive.push(kb);
92
100
  C.mlx_fast_metal_kernel_config_add_template_arg_dtype(cfg, ptr(kb), v);
93
101
  }
94
102
 
95
103
  const handles = new BigUint64Array(inputs.map((a) => a.handle));
104
+ keepAlive.push(handles);
96
105
  const inVec = C.mlx_vector_array_new_data(ptr(handles), BigInt(inputs.length));
97
106
  const slot = new BigUint64Array([C.mlx_vector_array_new()]);
107
+ keepAlive.push(slot);
98
108
  const slotPtr = ptr(slot);
99
109
  const status = C.mlx_fast_metal_kernel_apply(slotPtr, this.#kernel, inVec, cfg, call.stream ?? gpuStream);
100
110
  const outVec = read.u64(slotPtr, 0);
111
+ // anchor: all ptr()'d JS buffers above are done being read by native code by
112
+ // this point; touching keepAlive keeps the JIT from freeing them earlier.
113
+ if (keepAlive.length < 0) throw new Error("unreachable");
101
114
  C.mlx_vector_array_free(inVec);
102
115
  if (status !== 0) {
103
116
  C.mlx_vector_array_free(outVec);
@@ -29,10 +29,12 @@ function forwardTiles(headDim: number): [number, number] {
29
29
  throw new Error(`unsupported head_dim=${headDim}`);
30
30
  }
31
31
 
32
- /** uint32 [n] array (matches optiq's shape/causal input dtype). */
32
+ /** uint32 [n] array (matches optiq's shape/causal input dtype). COPY into an
33
+ * mlx-owned array (not the pinning fromView) — these tiny kernel-arg arrays are
34
+ * disposed before the lazy kernel evaluates; pinning leaks the host buffer per
35
+ * call (see flash-cce.ts u32 for the full diagnosis). */
33
36
  function u32(values: number[]): MlxArray {
34
- const u = new Uint32Array(values);
35
- return MlxArray.fromView(new Uint8Array(u.buffer.slice(0)), [values.length], Dtype.uint32);
37
+ return MlxArray.fromBytesCopy(new Uint8Array(new Uint32Array(values).buffer), [values.length], Dtype.uint32);
36
38
  }
37
39
 
38
40
  // Forward shader — verbatim from optiq flash_attention_metal.py _KERNEL_SOURCE.
@@ -40,6 +40,13 @@ export interface PiProviderOptions {
40
40
  * reasoning and sends the on/off switch the server maps to enable_thinking.
41
41
  * Default false. */
42
42
  reasoning?: boolean;
43
+ /** The loaded model can accept images (server's `ctx.vision != null`).
44
+ * This MUST match the real model: pi-ai serializes user content as OpenAI
45
+ * multimodal content-parts (`[{type:"text",…}]`) for image-capable models
46
+ * and as a plain string otherwise. A text-only chat template renders nothing
47
+ * for a parts array, so declaring a non-vision model as image-capable makes
48
+ * the user's turn vanish ("I don't see any message"). Default false. */
49
+ vision?: boolean;
43
50
  }
44
51
 
45
52
  export interface PiProviderWiring {
@@ -69,19 +76,28 @@ export function buildPiProvider(baseUrl: string, opts: PiProviderOptions = {}):
69
76
  name: opts.name ?? "mlx-bun (local)",
70
77
  api: PI_API,
71
78
  reasoning: opts.reasoning ?? false,
72
- // Our local thinking models (Qwen3.5/MiniCPM5) are BINARY: the chat
73
- // template's enable_thinking is a boolean. "qwen-chat-template" makes
74
- // Pi send chat_template_kwargs.enable_thinking; pi-ai computes that
75
- // boolean from whether the selected level is off or not, so no level
76
- // map is needed here (null in thinkingLevelMap means "unsupported").
77
- ...(opts.reasoning
78
- ? { compat: { thinkingFormat: "qwen-chat-template" as const } }
79
- : {}),
80
- // Declare image capability so pi will carry image content to /v1.
81
- // Whether the *loaded* model can actually see images is gated at the
82
- // server (non-vision models 400) and surfaced to the web UI via the
83
- // `ready` frame's `vision` flag.
84
- input: ["text", "image"],
79
+ // Compat flags that make pi emit the wire format our local server's
80
+ // chat templates expect (docs/custom-provider.md "Model Definition
81
+ // Reference"):
82
+ // - supportsDeveloperRole:false → pi sends the system prompt as the
83
+ // `system` role, not `developer` (our templates only know
84
+ // system/user/assistant/tool).
85
+ // - thinkingFormat:"qwen-chat-template" (reasoning models only) → pi
86
+ // sends chat_template_kwargs.enable_thinking, the boolean our
87
+ // Qwen3.5/MiniCPM5 templates gate on. pi computes the boolean from
88
+ // the selected thinking level (off → false), so no thinkingLevelMap.
89
+ compat: {
90
+ supportsDeveloperRole: false,
91
+ ...(opts.reasoning ? { thinkingFormat: "qwen-chat-template" as const } : {}),
92
+ },
93
+ // input MUST match the loaded model. pi-ai serializes user content as a
94
+ // plain string for text-only models and as OpenAI content-parts
95
+ // (`[{type:"text",…}]`) once a model declares image input — and a
96
+ // text-only chat template renders NOTHING for a parts array, dropping
97
+ // the user's turn ("I don't see any message"). So declare image input
98
+ // only for an actual vision model; the web `ready` frame's `vision`
99
+ // flag is the same signal that drives the UI's image affordance.
100
+ input: opts.vision ? ["text", "image"] : ["text"],
85
101
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
86
102
  contextWindow: opts.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
87
103
  maxTokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
@@ -0,0 +1,81 @@
1
+ // Shared pi front-door assembly for mlx-bun's embedded web + terminal agents.
2
+ //
3
+ // This is the single place that decides which custom tools and bundled skills
4
+ // are visible to pi. Keeping that policy here prevents the browser and TUI from
5
+ // drifting — especially for personal memory, whose prompt surface must be
6
+ // relevant-but-not-dominant.
7
+
8
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
9
+ import { createWebTools, WEB_TOOL_NAMES } from "./web-tools";
10
+ import { materializeBundledSkillPaths } from "./web/skills";
11
+ import {
12
+ createMemoryTools,
13
+ MEMORY_TOOL_NAMES,
14
+ memoryIndexHint,
15
+ isMemoryEnabled,
16
+ createReferenceTools,
17
+ REFERENCE_TOOL_NAMES,
18
+ } from "./memory/tools";
19
+
20
+ /** Built-in pi coding tools plus mlx-bun's non-mutating web tools. */
21
+ export const PI_BASE_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls", ...WEB_TOOL_NAMES] as const;
22
+
23
+ export interface PiAgentSurfaceOptions {
24
+ /** Force memory on/off for tests. Default: auto-detect vault existence. */
25
+ memory?: "auto" | "on" | "off";
26
+ /** Include web_search/web_fetch/weather. Default true. */
27
+ webTools?: boolean;
28
+ /** Include read/bash/edit/write/grep/find/ls. Default true. */
29
+ codingTools?: boolean;
30
+ }
31
+
32
+ export interface PiAgentSurface {
33
+ memoryEnabled: boolean;
34
+ /** Tool allowlist to pass to createAgentSession/createAgentSessionFromServices. */
35
+ tools: string[];
36
+ /** Custom tools to pass to pi. Built-ins are selected by name in `tools`. */
37
+ customTools: ToolDefinition[];
38
+ /** Skill paths for DefaultResourceLoader.additionalSkillPaths. */
39
+ skillPaths: string[];
40
+ /** Short prompt addendum. Empty unless memory is enabled. */
41
+ memoryHint: string;
42
+ }
43
+
44
+ async function resolveMemoryEnabled(mode: PiAgentSurfaceOptions["memory"]): Promise<boolean> {
45
+ if (mode === "on") return true;
46
+ if (mode === "off") return false;
47
+ return isMemoryEnabled();
48
+ }
49
+
50
+ /**
51
+ * Build the shared tool + skill surface for both pi front doors.
52
+ *
53
+ * Memory policy: if the user enabled memory (vault exists), memory is available.
54
+ * It is still a scoped retrieval capability, not a default first step for every
55
+ * prompt. If memory is absent, no memory skill/tool schemas are exposed at all.
56
+ */
57
+ export async function buildPiAgentSurface(opts: PiAgentSurfaceOptions = {}): Promise<PiAgentSurface> {
58
+ const memoryEnabled = await resolveMemoryEnabled(opts.memory ?? "auto");
59
+ const includeWeb = opts.webTools ?? true;
60
+ const includeCoding = opts.codingTools ?? true;
61
+
62
+ const tools: string[] = [];
63
+ if (includeCoding) tools.push("read", "bash", "edit", "write", "grep", "find", "ls");
64
+ const customTools: ToolDefinition[] = [];
65
+ if (includeWeb) {
66
+ tools.push(...WEB_TOOL_NAMES);
67
+ customTools.push(...createWebTools());
68
+ }
69
+ if (memoryEnabled) {
70
+ tools.push(...MEMORY_TOOL_NAMES, ...REFERENCE_TOOL_NAMES);
71
+ customTools.push(...createMemoryTools(), ...createReferenceTools());
72
+ }
73
+
74
+ return {
75
+ memoryEnabled,
76
+ tools,
77
+ customTools,
78
+ skillPaths: materializeBundledSkillPaths({ includeMemory: memoryEnabled }),
79
+ memoryHint: memoryEnabled ? await memoryIndexHint() : "",
80
+ };
81
+ }
@@ -35,8 +35,7 @@ import {
35
35
  runRpcMode,
36
36
  type CreateAgentSessionRuntimeFactory,
37
37
  } from "@earendil-works/pi-coding-agent";
38
- import { createWebTools, WEB_TOOL_NAMES } from "./web-tools";
39
- import { materializeBundledSkills } from "./web/skills";
38
+ import { buildPiAgentSurface } from "./pi-session";
40
39
  import { buildPiProvider, DEFAULT_CONTEXT_WINDOW } from "./pi-provider";
41
40
 
42
41
  /** How the embedded agent runs. Mirrors pi's CLI appMode. */
@@ -49,8 +48,6 @@ export type PiTerminalMode = "interactive" | "print" | "rpc";
49
48
  * sees it. Mutating tools (bash/edit/write) surface pi's own TUI approval
50
49
  * prompt at call time — we don't add a gate.
51
50
  */
52
- const ALL_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls", ...WEB_TOOL_NAMES];
53
-
54
51
  /**
55
52
  * System prompt for the embedded terminal coding agent.
56
53
  *
@@ -70,20 +67,11 @@ export function buildTerminalSystemPrompt(modelLabel?: string): string {
70
67
  const model = modelLabel ? `the local model \`${modelLabel}\`` : "a local model";
71
68
  return `You are mlx-bun's terminal coding agent — a capable, hands-on pair programmer working alongside the user in their terminal. You run on ${model} entirely on the user's own Apple-silicon Mac via mlx-bun, so the whole session stays local and private; nothing leaves the machine except the explicit web tools below.
72
69
 
73
- You operate inside the user's current project directory and have a real toolset — use it. Prefer doing the work over describing it: open the files, search the tree, run the command, make the edit. Investigate before you answer, and follow through to a working result.
74
-
75
- Tools used freely (no confirmation needed):
76
- - read — open and read any file
77
- - ls, find, grep — list directories and search the tree by name or content
78
- - web_search — search the web for current, real-world information (docs, APIs, errors, anything that changes or that you don't already know)
79
- - web_fetch — fetch a URL and read the page or data behind it (a natural follow-up to web_search, or when the user gives you a link)
80
- - weather — current conditions and a short forecast for any place
70
+ You operate inside the user's current project directory. Available capabilities when relevant: read-only file inspection (read, ls, find, grep), web search/fetch (web_search, web_fetch), weather, and user-approved local actions (bash, edit, write).
81
71
 
82
- Tools that ask the user for approval first (they stay in control — pi shows the prompt, you don't have to ask in prose):
83
- - bash — run shell commands (build, test, git, scripts)
84
- - edit, write — modify existing files or create new ones
72
+ Use tools in service of helping the user, not as a performance. For ordinary social conversation, answer naturally yourself. For real-world facts, current events, schedules, releases, local information, recommendations, restaurants, movies, prices, travel, docs, or anything likely to have changed, look it up with web_search/web_fetch or weather instead of relying on memory or guessing. For coding tasks, inspect the files, investigate before you answer, and follow through to a working result. When you need to change code or run something, propose the concrete edit or command and let the approval prompt handle consent — don't ask permission in prose first.
85
73
 
86
- When you need to change code or run something, just propose the concrete edit or command and let the approval prompt handle consent — don't narrate "I would run…" or ask permission in text. When you hit something you don't know — especially current or version-specific facts — look it up with web_search and web_fetch instead of guessing. If a request is genuinely ambiguous, ask one short clarifying question; otherwise make a sensible choice and proceed.
74
+ Environment metadata such as current date, working directory, model name, and available tools may be provided for context; do not report or summarize that metadata unless the user asks for it or it is directly relevant. Do not narrate tool policies or internal workflows.
87
75
 
88
76
  Be honest about what you did and what you found, including mistakes, dead ends, and anything you couldn't do. Keep responses tight and skimmable: format with Markdown, show file paths and commands plainly, and cite links when you used the web.`;
89
77
  }
@@ -121,6 +109,7 @@ export async function runEmbeddedPi(opts: RunEmbeddedPiOptions): Promise<number>
121
109
  const cwd = process.cwd();
122
110
  // Own session/settings dir, isolated from any standalone pi install.
123
111
  const agentDir = join(homedir(), ".mlx-bun", "pi");
112
+ const sessionDir = join(agentDir, "sessions", `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`);
124
113
 
125
114
  const { authStorage, modelRegistry, model } = buildPiProvider(opts.baseUrl, {
126
115
  contextWindow: opts.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
@@ -128,10 +117,10 @@ export async function runEmbeddedPi(opts: RunEmbeddedPiOptions): Promise<number>
128
117
  reasoning: opts.reasoning ?? false,
129
118
  });
130
119
 
131
- // Curated skills (web-research, …) written to ~/.mlx-bun/skills and loaded
132
- // on top of an otherwise-isolated session. See src/web/skills.ts.
133
- const skillsRoot = materializeBundledSkills();
134
- const systemPrompt = buildTerminalSystemPrompt(opts.modelLabel);
120
+ // Shared pi tool/skill surface. Memory is included only when the user has
121
+ // enabled a vault; if present it remains scoped to user-specific continuity.
122
+ const surface = await buildPiAgentSurface();
123
+ const systemPrompt = buildTerminalSystemPrompt(opts.modelLabel) + surface.memoryHint;
135
124
 
136
125
  // The runtime factory: same shape as pi's own CLI (dist/main.js) and the
137
126
  // shipped examples/sdk/13-session-runtime.ts. The same factory is reused by
@@ -154,7 +143,7 @@ export async function runEmbeddedPi(opts: RunEmbeddedPiOptions): Promise<number>
154
143
  // AGENTS.md / CLAUDE.md context files (unlike the web chat).
155
144
  noContextFiles: false,
156
145
  // Our curated skills on top (noSkills above only disables discovery).
157
- additionalSkillPaths: [skillsRoot],
146
+ additionalSkillPaths: surface.skillPaths,
158
147
  // Replace pi's default coding-agent prompt with our mlx-bun persona.
159
148
  systemPrompt,
160
149
  },
@@ -165,15 +154,15 @@ export async function runEmbeddedPi(opts: RunEmbeddedPiOptions): Promise<number>
165
154
  sessionManager,
166
155
  sessionStartEvent,
167
156
  model,
168
- tools: ALL_TOOLS,
169
- customTools: createWebTools(),
157
+ tools: surface.tools,
158
+ customTools: surface.customTools,
170
159
  })),
171
160
  services,
172
161
  diagnostics: services.diagnostics,
173
162
  };
174
163
  };
175
164
 
176
- const sessionManager = SessionManager.create(cwd);
165
+ const sessionManager = SessionManager.create(cwd, sessionDir);
177
166
  const runtime = await createAgentSessionRuntime(createRuntime, { cwd, agentDir, sessionManager });
178
167
 
179
168
  try {