mlx-bun 0.0.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.
Files changed (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +362 -0
  3. package/THIRD_PARTY_LICENSES.md +41 -0
  4. package/bin/mlx-bun.mjs +32 -0
  5. package/docs/curve-designer.html +303 -0
  6. package/docs/design/adapters-end-to-end.md +201 -0
  7. package/docs/design/compat-cli-surface-design.md +227 -0
  8. package/docs/design/docs-reorg-plan.md +152 -0
  9. package/docs/design/hlg-sampling.md +340 -0
  10. package/docs/design/memory-system.md +339 -0
  11. package/docs/design/optimization_plan.md +344 -0
  12. package/docs/design/parallel-slots.md +515 -0
  13. package/docs/design/runtime-isolation.md +247 -0
  14. package/docs/design/segmented-backward-training.md +508 -0
  15. package/docs/investigations/chunk-finetune-experiment.md +84 -0
  16. package/docs/investigations/curve-bisector-routing.md +123 -0
  17. package/docs/investigations/curve-runs/bisector-route.json +98 -0
  18. package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
  19. package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
  20. package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
  21. package/docs/investigations/curve-terrain.html +105 -0
  22. package/docs/investigations/expert-offload-single-user-moe.md +292 -0
  23. package/docs/investigations/hlg-ab.html +34 -0
  24. package/docs/investigations/hlg-explorer.html +238 -0
  25. package/docs/investigations/hlg-figs/cliff.svg +1 -0
  26. package/docs/investigations/hlg-figs/cross.svg +1 -0
  27. package/docs/investigations/hlg-figs/edges.svg +1 -0
  28. package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
  29. package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
  30. package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
  31. package/docs/investigations/hlg-figs/terrain.svg +1 -0
  32. package/docs/investigations/hlg-report.html +262 -0
  33. package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
  34. package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
  35. package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
  36. package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
  37. package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
  38. package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
  39. package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
  40. package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
  41. package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
  42. package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
  43. package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
  44. package/docs/investigations/hlg-runs/hlg-map.json +383 -0
  45. package/docs/investigations/hlg-runs/run0-original.log +159 -0
  46. package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
  47. package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
  48. package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
  49. package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
  50. package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
  51. package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
  52. package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
  53. package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
  54. package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
  55. package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
  56. package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
  57. package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
  58. package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
  59. package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
  60. package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
  61. package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
  62. package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
  63. package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
  64. package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
  65. package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
  66. package/docs/investigations/hlg-runs/runN-verify.log +116 -0
  67. package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
  68. package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
  69. package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
  70. package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
  71. package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
  72. package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
  73. package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
  74. package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
  75. package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
  76. package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
  77. package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
  78. package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
  79. package/docs/investigations/hlg-sampling-investigation.md +970 -0
  80. package/docs/investigations/lab-build-journal.md +157 -0
  81. package/docs/investigations/pi-builtin-investigation.md +308 -0
  82. package/docs/investigations/starter-model-port-handoff.md +323 -0
  83. package/docs/planning/IDEAS.md +26 -0
  84. package/docs/planning/PRODUCT_ROADMAP.md +426 -0
  85. package/docs/planning/ResearchTopics.md +119 -0
  86. package/docs/planning/chunk-task-roadmap.md +77 -0
  87. package/docs/planning/journal.md +155 -0
  88. package/docs/reference/distribution.md +121 -0
  89. package/docs/reference/embedding.md +168 -0
  90. package/docs/reference/library-api.md +153 -0
  91. package/docs/reference/server-api.md +224 -0
  92. package/docs/reference/server-config.md +243 -0
  93. package/docs/segmented-backward-handoff.md +148 -0
  94. package/package.json +68 -0
  95. package/src/anthropic.ts +491 -0
  96. package/src/chat-template.ts +186 -0
  97. package/src/cli.ts +1075 -0
  98. package/src/config.ts +229 -0
  99. package/src/curve-sampler.ts +122 -0
  100. package/src/dataset/generators.ts +1087 -0
  101. package/src/dataset/index.ts +12 -0
  102. package/src/dataset/job.ts +81 -0
  103. package/src/dataset/llm.ts +71 -0
  104. package/src/dataset/registry.ts +528 -0
  105. package/src/download.ts +283 -0
  106. package/src/eval/README.md +86 -0
  107. package/src/eval/kl-prompts.ts +29 -0
  108. package/src/eval/kl.ts +294 -0
  109. package/src/eval/quality-db.ts +92 -0
  110. package/src/eval/runner.ts +102 -0
  111. package/src/eval/score.ts +49 -0
  112. package/src/eval/tasks/bfcl.ts +500 -0
  113. package/src/eval/tasks/gsm8k.ts +85 -0
  114. package/src/eval/tasks/hashhop.ts +109 -0
  115. package/src/eval/tasks/humaneval.ts +252 -0
  116. package/src/eval/tasks/ifeval.ts +354 -0
  117. package/src/eval/tasks/mmlu.ts +145 -0
  118. package/src/evaldb.ts +199 -0
  119. package/src/expert-offload-build.ts +77 -0
  120. package/src/expert-offload.ts +61 -0
  121. package/src/expert-trace.ts +67 -0
  122. package/src/fit.ts +254 -0
  123. package/src/generate.ts +451 -0
  124. package/src/harness-pi.ts +177 -0
  125. package/src/hf-push.ts +628 -0
  126. package/src/index.ts +22 -0
  127. package/src/jobs/db.ts +139 -0
  128. package/src/jobs/index.ts +29 -0
  129. package/src/jobs/job-entry.ts +106 -0
  130. package/src/jobs/runner.ts +304 -0
  131. package/src/jobs/sse.ts +114 -0
  132. package/src/jobs/types.ts +58 -0
  133. package/src/kv-store.ts +185 -0
  134. package/src/lora.ts +314 -0
  135. package/src/md-modules.d.ts +7 -0
  136. package/src/mlx/array.ts +237 -0
  137. package/src/mlx/autograd.ts +267 -0
  138. package/src/mlx/checkpoint.ts +116 -0
  139. package/src/mlx/compile.ts +158 -0
  140. package/src/mlx/custom-vjp.ts +140 -0
  141. package/src/mlx/ffi.ts +433 -0
  142. package/src/mlx/metal-kernel.ts +130 -0
  143. package/src/mlx/ops.ts +707 -0
  144. package/src/mmap.ts +90 -0
  145. package/src/model/batched-mask.ts +204 -0
  146. package/src/model/batched-rotating.ts +300 -0
  147. package/src/model/compiled-decode.ts +681 -0
  148. package/src/model/factory.ts +27 -0
  149. package/src/model/fingerprint.ts +33 -0
  150. package/src/model/flash-attention.ts +674 -0
  151. package/src/model/fused-decode-kernel.ts +305 -0
  152. package/src/model/fused-geglu-kernel.ts +87 -0
  153. package/src/model/gemma4-base.ts +1616 -0
  154. package/src/model/gemma4.ts +1012 -0
  155. package/src/model/generated/gemma4-12b.ts +526 -0
  156. package/src/model/generated/gemma4-26b.ts +500 -0
  157. package/src/model/generated/gemma4-e4b.ts +757 -0
  158. package/src/model/generated/index.ts +18 -0
  159. package/src/model/minicpm5.ts +278 -0
  160. package/src/model/qwen3-delta.ts +224 -0
  161. package/src/model/qwen3_5.ts +429 -0
  162. package/src/model/support.ts +46 -0
  163. package/src/native-pack.ts +152 -0
  164. package/src/pi-provider.ts +95 -0
  165. package/src/pi-terminal.ts +264 -0
  166. package/src/pi-web.ts +924 -0
  167. package/src/preflight.ts +123 -0
  168. package/src/prompt-cache.ts +113 -0
  169. package/src/quantize/allocator.ts +627 -0
  170. package/src/quantize/calibration.ts +180 -0
  171. package/src/quantize/config-writer.ts +132 -0
  172. package/src/quantize/index.ts +57 -0
  173. package/src/quantize/job.ts +136 -0
  174. package/src/quantize/quantizer.ts +462 -0
  175. package/src/quantize/safetensors-writer.ts +171 -0
  176. package/src/quantize/sensitivity.ts +234 -0
  177. package/src/registry.ts +336 -0
  178. package/src/responses.ts +774 -0
  179. package/src/safetensors.ts +158 -0
  180. package/src/sampler.ts +517 -0
  181. package/src/serve/batch-scheduler.ts +362 -0
  182. package/src/serve/generation-gateway.ts +160 -0
  183. package/src/server.ts +1891 -0
  184. package/src/spec/drafter.ts +261 -0
  185. package/src/spec/generate.ts +221 -0
  186. package/src/status-page.html +497 -0
  187. package/src/tokenizer.ts +38 -0
  188. package/src/tool-call.ts +242 -0
  189. package/src/train/dataset.ts +461 -0
  190. package/src/train/export.ts +32 -0
  191. package/src/train/forward.ts +229 -0
  192. package/src/train/index.ts +43 -0
  193. package/src/train/job.ts +148 -0
  194. package/src/train/lora-params.ts +194 -0
  195. package/src/train/loss.ts +342 -0
  196. package/src/train/merge.ts +195 -0
  197. package/src/train/optimizer.ts +176 -0
  198. package/src/train/rank.ts +101 -0
  199. package/src/train/segmented.ts +520 -0
  200. package/src/train/trainer.ts +600 -0
  201. package/src/tui.ts +161 -0
  202. package/src/vision/embedder.ts +168 -0
  203. package/src/vision/preprocess.ts +247 -0
  204. package/src/vision/prompt.ts +158 -0
  205. package/src/vision/siglip.ts +599 -0
  206. package/src/web/app.html +2471 -0
  207. package/src/web/skills/web-research/SKILL.md +44 -0
  208. package/src/web/skills.ts +59 -0
  209. package/src/web-tools.ts +426 -0
  210. package/src/weights.ts +91 -0
@@ -0,0 +1,157 @@
1
+ # Build journal — mlx-bun Lab (web UI + native quantize/train/dataset + pi web chat)
2
+
3
+ *Branch `web-ui-and-native-lab`, 2026-06-13/14. Visual summary:
4
+ [archive/mlx-bun-lab-report.html](../../archive/mlx-bun-lab-report.html). Evidence
5
+ artifact: `benchmarks/lab-verification.json` (regenerable, gitignored — produced by
6
+ `scripts/verify-lab.ts`).*
7
+
8
+ ## Goal
9
+
10
+ Re-implement the whole mlx-bun web surface as one beautiful, keynote-grade SPA
11
+ with five sections — a **full pi-agent chat**, plus **Quantize**, **Fine-tune**,
12
+ and **Build Dataset** ported from the OptIQ "lab" — while keeping the status
13
+ page. Compute is **native Bun/MLX (no Python)** per Josh's decision.
14
+
15
+ ## The decisions that shaped it
16
+
17
+ 1. **The big fork (asked up front).** Quantize + fine-tune compute could be (a)
18
+ native via mlx-c, (b) orchestrate the installed OptIQ Python, or (c) proxy
19
+ the running `optiq lab`. Josh chose **(a) native** — truest to mlx-bun's "the
20
+ buggy layer doesn't need Python" thesis. Chat = **full pi agent** (all tools).
21
+ Verification = **real tiny runs, pausing for go-ahead** before heavy jobs.
22
+
23
+ 2. **Gate the riskiest unknown first.** Native LoRA needs autograd across the
24
+ FFI boundary. Rather than discover mid-build that it's infeasible, the first
25
+ thing built was `spikes/phase-train-vag.ts`: a tiny LoRA-shaped loss
26
+ (`mean((x + (x@A)@B − y)²)`) differentiated by `mlx_value_and_grad` through a
27
+ `bun:ffi` JSCallback closure, checked against central finite differences.
28
+ **PASS (<1e-2), exactly 2 grads returned (base frozen).** Only then was the
29
+ trainer worth writing. This single check de-risked the entire subsystem.
30
+
31
+ 3. **One SPA, hash-routed, text-imported.** Kept the exact single-binary
32
+ embedding (`with { type: "text" }`) the status page already used — no
33
+ bundler, no asset-resolution to patch in `build-binary.sh`. Design tokens
34
+ lifted from `status-page.html` so the five sections feel like one product.
35
+
36
+ 4. **Mixed job-execution model by safety class.** Async Metal command-buffer
37
+ OOM is **uncatchable** (it `std::terminate`s). So GPU jobs (quantize,
38
+ finetune) run as **crash-isolated child `mlx-bun` subprocesses** behind a GPU
39
+ lease; dataset jobs (pure JS + loopback HTTP) run **in-process**. Both write
40
+ the same NDJSON event log + SQLite row, so the SSE/tail path is uniform.
41
+
42
+ 5. **Open-book ports, not inventions.** Each engine follows a readable source:
43
+ dataset → `optiq/lab/dataset_templates.py` (13 templates, field-for-field);
44
+ trainer → `optiq/lora/{trainer,dpo,config}.py` + mlx-lm `default_loss`;
45
+ quantize → mlx-lm `convert.py` rules + the on-disk OptIQ model format; jobs →
46
+ `optiq/lab/{jobs,db}.py`; chat → pi SDK `examples/sdk/12-full-control.ts`.
47
+
48
+ ## How it was built — parallel orchestration
49
+
50
+ After laying the shared job contract (`src/jobs/types.ts`), six subsystems were
51
+ fanned out to concurrent sub-agents on **non-overlapping files**, then
52
+ integrated centrally:
53
+
54
+ | subsystem | files | how verified |
55
+ |---|---|---|
56
+ | shared jobs | `src/jobs/*` | 12 tests (lifecycle, GPU lease, crash isolation, zombie, SSE) |
57
+ | dataset | `src/dataset/*` | 32 tests (generators, CSV, 90/10 split, 13 templates) |
58
+ | quantize | `src/quantize/*` | 6 tests + real-model e2e |
59
+ | trainer | `src/train/*`, `src/mlx/autograd.ts` | 19 fast + real-model e2e |
60
+ | web SPA | `src/web/app.html` | 7 in-process route tests |
61
+ | pi embed | `src/pi-web.ts` | 9 unit + live chat turn |
62
+
63
+ Shared/owned-by-me files were edited centrally to avoid conflicts:
64
+ `src/server.ts` (websocket + Lab routes + serve the SPA), `src/mlx/{ffi,ops}.ts`
65
+ (autograd/random/save bindings), `src/cli.ts`, `package.json` (pinned
66
+ `@earendil-works/pi-coding-agent@0.79.3`).
67
+
68
+ ## Key technical findings
69
+
70
+ - **`mlx_save_safetensors` exists** (mlx-c `io.h`) — the quantizer writes real
71
+ HF-layout shards natively; no hand-rolled byte writer, no dtype/alignment bugs.
72
+ - **`mlx_value_and_grad` + `mlx_closure_new_func_payload`** are drivable from a
73
+ `bun:ffi` JSCallback (the repo already uses one for the mlx error handler).
74
+ The closure swaps the differentiated primals into `LoraWeights.{a,b}`, builds
75
+ the loss graph, and restores; mlx's graph keeps its own refs, so only A/B
76
+ differentiate and the quantized base stays frozen.
77
+ - **No optimizer in mlx-c** → AdamW hand-rolled as array ops (m/v EMA, bias
78
+ correction, decoupled weight decay), each step swapping the leaf handle back
79
+ into the `LoraWeights` so the next forward sees the update.
80
+ - **pi tool-approval gate** is a real pre-execution hook: an inline extension
81
+ via `DefaultResourceLoader.extensionFactories` registers `pi.on("tool_call")`
82
+ returning `{block:true}` to deny — read-only tools auto-allow, bash/edit/write
83
+ await a browser decision (120s auto-deny).
84
+
85
+ ## Bugs found and fixed (during the real-model runs)
86
+
87
+ - **LoRA init key shape** — `mlx_random_normal` got a `[1,2]` key (a row slice of
88
+ the split-keys array) but mlx wants `[2]`. Reshaped the per-module subkey.
89
+ - **Quantize e2e path** — the gated test passed a model *id* to
90
+ `quantizeModelDir` instead of a resolved snapshot *path* (the job runner
91
+ resolves via the registry; the direct call didn't). Pointed it at
92
+ `SNAPSHOT_MINICPM5`.
93
+ - **Flaky chat test** — the agent sometimes takes a tool-call turn before
94
+ emitting text; the test resolved on the first `turn_end` and saw no tokens.
95
+ Fixed to settle on the first turn that actually streams text. The embed was
96
+ always correct (a debug run showed 25+ text deltas + a tool execution).
97
+
98
+ ## Measured evidence (M4 Pro, MiniCPM5-1B, on-disk)
99
+
100
+ - **Quantize:** 170 modules → **4.50 bits/weight**, **0.62 GB** dir, **7.0 s**,
101
+ reopened correctly through the loader (quantized embed_tokens as `uint32`).
102
+ - **Train:** 20 SFT iters, **loss 4.48 → 0.68** in **4.6 s**, 168 adapters
103
+ mounted. Behavior changed — on "Tell me about your day": base replies normally,
104
+ the UPPERCASE-fixture adapter replies `"MY DAY AHEAD. APPROVE AND PROPERLY…"`.
105
+ - **Chat:** a real WebSocket turn streamed text + executed a tool to completion.
106
+ - **Tests:** 85+ new, all green; `tsc --noEmit` clean; existing server suite
107
+ 17/17 (no regressions).
108
+
109
+ ## Scope: shipped vs designed-seam
110
+
111
+ - **Shipped (v1):** full SPA (5 sections), pi web chat with tools + approval
112
+ gate, native uniform-affine quantize, native SFT + DPO LoRA, 12/13 dataset
113
+ templates, shared job system, real tiny-run verification.
114
+ - **Designed seams (follow-ups):** ~~OptIQ sensitivity+knapsack mixed-precision
115
+ quantization; batched (B>1) training; `hf_dataset_import`; HF push-to-hub;
116
+ folding pi's assets into the compiled single binary.~~ **ALL DONE in Round 2 —
117
+ see below.**
118
+
119
+ ## Round 2 — the leftover seams, ported 1:1 (2026-06-14)
120
+
121
+ Josh: "all of those, port, one to one functionality." Built the five remaining
122
+ items as faithful ports of the OptIQ / mlx-lm source (open book), in parallel on
123
+ non-overlapping files, integrated centrally (server routes + one `app.html` UI
124
+ pass), and verified.
125
+
126
+ - **Mixed-precision quantization** (`src/quantize/{allocator,sensitivity,
127
+ calibration}.ts`) — `optimize_mixed_precision` ported **1:1** from
128
+ `optiq/core/optimizer.py` (greedy knapsack + block-aware floor + protected
129
+ layers + block-run guard + the uniform-vs-bf16 `_kl_reduction` sign detection).
130
+ Verified **bit-identical to OptIQ** on shared synthetic inputs by spawning the
131
+ venv Python in the test (4/4 cases). Per-layer KL sensitivity
132
+ (`analyze_sensitivity_exact`) runs on our graph: dequantize each layer's weight
133
+ to bf16, re-quantize at each candidate bit, swap into the running
134
+ `QuantizedLinear`, forward on a WikiText-2 calibration set, KL vs reference.
135
+ Real e2e: MiniCPM5-1B at target 5.0 bpw → 80×4-bit + 90×8-bit, **5.19 bpw, 32 s**.
136
+ - **Batched (B>1) training** (`src/train/{dataset,forward,loss}.ts`) — ragged
137
+ padding + a `[B,1,L,L]` causal-AND-key-validity mask via a cache wrapper (no
138
+ model/op edits); B=1 stays a separate bit-identical path. Bug found: comparing a
139
+ length-4 reference to a length-6 batched row conflated mask correctness with
140
+ bf16 sequence-length rounding — the corrected shape-held parity test passes,
141
+ confirming the mask isolates rows.
142
+ - **`hf_dataset_import`** (`src/dataset/generators.ts`) — the 13th template via
143
+ the HF datasets-server REST API (pagination, config resolve, label/min-chars/
144
+ cap filters, three output formats).
145
+ - **HF push-to-hub** (`src/hf-push.ts`) — native Hub upload (create repo → LFS
146
+ preupload → S3 PUT → NDJSON commit) for model/adapter/dataset repos, token at
147
+ `~/.mlx-bun/hf.json`. Mock-server tested end-to-end (11 tests); real uploads
148
+ untested by design (would publish under the user's account).
149
+ - **Single-binary web chat** (`scripts/build-binary.sh`) — pi's JS already
150
+ bundles under `bun build --compile`; only `photon_rs_bg.wasm` is sidecarred.
151
+ The compiled binary's headless pi session was verified to construct.
152
+
153
+ UI: one `app.html` pass enabled the Uniform/Mixed quantize toggle + target-bpw
154
+ slider, live merge/export/push on the fine-tune done step, push on quantize &
155
+ dataset, and an HF-token settings gear. New server routes:
156
+ `/api/finetune/{merge,export}`, `/api/{quantize,finetune,dataset}/push`,
157
+ `/api/settings/hf-token`, + mixed-precision params on quantize submit.
@@ -0,0 +1,308 @@
1
+ # Investigation: pi as a "built-in" terminal for mlx-bun
2
+
3
+ *2026-06-12. Question (PRODUCT_ROADMAP "Harness UX" / PLAN direction A):
4
+ can `mlx-bun pi` drop the user into a working pi agent session against
5
+ the locally running model — and what's the best mechanism? Sources:
6
+ https://pi.dev/docs/latest (sdk, tui, rpc, custom-provider pages) plus
7
+ direct inspection of the installed packages
8
+ (`~/.bun/install/global/node_modules/@earendil-works/*`, v0.79.1).*
9
+
10
+ > **Outcome (2026-06-14, what actually shipped).** This investigation
11
+ > proposed shipping the subprocess launcher first and promoting to an
12
+ > embedded flagship later. We went straight to the embedded design and
13
+ > then **dropped the subprocess path entirely** (Option 2 + the
14
+ > `--external-pi` fallback in P4): `mlx-bun pi` runs pi's own
15
+ > `InteractiveMode` **in-process** (src/pi-terminal.ts) with a custom
16
+ > mlx-bun coding-agent prompt, full coding tools + web tools, and pi's
17
+ > native approval prompts — nothing to install. A user who already runs
18
+ > pi connects it to the local model with **`mlx-bun harness pi`**
19
+ > (Option 1, src/harness-pi.ts) — that's the only "their pi" path, since
20
+ > launching a pi they already have was pure duplication. The web-chat
21
+ > half (Option 4) ships via src/pi-web.ts. The analysis below is the
22
+ > point-in-time reasoning that led here.
23
+
24
+ ## TL;DR
25
+
26
+ **Yes, and better than hoped.** pi is not just embeddable — it is a
27
+ Bun-native, MIT-licensed npm package whose own release binary is built
28
+ with `bun build --compile`, the exact toolchain mlx-bun's Phase 5
29
+ single-binary bundle already uses. The full interactive TUI (editor,
30
+ chat history, all built-in commands) is an exported SDK class
31
+ (`InteractiveMode`), not CLI-only internals. Four integration options
32
+ exist, they are not mutually exclusive, and the recommended path is:
33
+ ship the cheap subprocess launcher now, promote to a true in-binary
34
+ embed as the flagship. pi's RPC mode separately gives us the protocol
35
+ for the built-in web chat UI, so this investigation pays for direction
36
+ A's part 1 as well.
37
+
38
+ ## What pi is, technically
39
+
40
+ - Four MIT-licensed packages, all v0.79.1, all from
41
+ `github.com/earendil-works/pi`:
42
+ `pi-coding-agent` (13 MB, the agent + CLI + SDK), `pi-ai` (4.9 MB,
43
+ provider/streaming layer), `pi-agent-core` (1.3 MB), `pi-tui`
44
+ (1.9 MB, differential-rendering TUI lib). ~21 MB total on disk
45
+ before tree-shaking — negligible next to model weights, modest next
46
+ to mlx-bun's current binary.
47
+ - **Bun-compile native**: `pi-coding-agent`'s `build:binary` script is
48
+ literally `bun build --compile ./dist/bun/cli.js ...` — upstream
49
+ ships a Bun single-file executable themselves. Embedding it in our
50
+ Bun bundle is a supported configuration, not an experiment.
51
+ - Engines: node ≥ 22.19 (pi-tui uses `/v`-flag regexes — fails under
52
+ node 18, fine under Bun ≥ 1.3.x, which mlx-bun already pins).
53
+ - Assets that must ride along in a binary (upstream's
54
+ `copy-binary-assets` is the manifest): theme JSONs, a few PNGs,
55
+ export-html templates, `photon_rs_bg.wasm` (image ops), and pi-tui's
56
+ prebuilt `.node` natives (`darwin-arm64`/`darwin-x64` ship in the
57
+ package).
58
+ - Local-model support is **already proven on this machine**:
59
+ `~/.pi/agent/models.json` has an `mlx` provider
60
+ (`api: "openai-completions"`, baseUrl + `sk-optiq-` key, compat
61
+ flags) serving Qwen quants. mlx-bun's OpenAI surface is what pi
62
+ expects.
63
+
64
+ ## The four integration options
65
+
66
+ ### Option 1 — `mlx-bun harness pi`: configure the user's existing pi
67
+
68
+ Write/merge the provider entry into `~/.pi/agent/models.json` (or drop
69
+ a tiny extension that calls `pi.registerProvider(...)` — the
70
+ documented programmatic route, which can even discover models
71
+ dynamically from our `/v1/models`). Validate with one tiny request,
72
+ print the launch command.
73
+
74
+ - **Effort**: hours. **Risk**: low (must merge, not clobber, user
75
+ config — the docs' extension route avoids touching models.json at
76
+ all). **Verdict**: do regardless; this is the harness-adapter
77
+ pattern from the roadmap and it's nearly free.
78
+
79
+ **Pros**: nearly free; respects the user's pi setup, sessions, themes,
80
+ extensions; zero version coupling; the dynamic-discovery extension
81
+ keeps the model list in sync with our server automatically.
82
+ **Cons**: requires pi to already be installed; not "built-in" — the
83
+ appliance story still has two moving parts; config-merge bugs are the
84
+ classic way to annoy users (the extension route mostly avoids this).
85
+
86
+ ### Option 2 — `mlx-bun pi` (v1): spawn the user's installed pi
87
+
88
+ Start our server, then spawn `pi` with a session-scoped provider
89
+ config (extension dir or env) pointing at it. If pi isn't installed,
90
+ print the install one-liner.
91
+
92
+ - **Effort**: a day with polish. **Risk**: version skew with whatever
93
+ pi the user has; not "built-in" — two installs.
94
+ - **Verdict**: the right v1. Real UX win immediately, zero coupling.
95
+ - **Bonus**: unrecognized flags pass straight through, so
96
+ `mlx-bun pi -p "summarize @notes.md"` (pi's non-interactive print
97
+ mode), `--mode json/rpc`, `--continue`, `--resume` etc. all work on
98
+ day one without us implementing anything.
99
+
100
+ **Pros**: fast to ship; full pi feature surface via passthrough;
101
+ upgrades to pi arrive for free; failure modes are simple (no pi →
102
+ print install hint). **Cons**: still two installs; we can't control
103
+ the pi version (an upstream flag/behavior change can break our
104
+ launcher); per-run provider injection needs care so we don't pollute
105
+ the user's global config; process lifecycle (server + child TUI)
106
+ needs clean shutdown handling.
107
+
108
+ ### Option 3 — SDK embed (the real "built-in"): pi inside the mlx-bun binary
109
+
110
+ This is the one the question was really about, and the SDK is
111
+ explicitly designed for it:
112
+
113
+ - `createAgentSession({...})` builds a session; the
114
+ `examples/sdk/12-full-control.ts` example shows **zero-discovery**
115
+ embedding: in-memory `SessionManager`/`SettingsManager`/
116
+ `ModelRegistry`, custom `ResourceLoader`, explicit tool list,
117
+ runtime API keys — nothing leaks from/to `~/.pi` unless we want it
118
+ to.
119
+ - `InteractiveMode` is exported: "full TUI interactive mode with
120
+ editor, chat history, and all built-in commands", constructed from
121
+ an `AgentSessionRuntime`. We do not rebuild a chat TUI from pi-tui
122
+ primitives (the tui docs are explicit that components are for
123
+ extensions, not a full-app kit) — we run *pi's own* TUI in-process.
124
+ - Provider wiring: register an `openai-completions` provider at our
125
+ own server's baseUrl (loopback HTTP into our already-running
126
+ `Bun.serve` — simplest and keeps one code path for external
127
+ clients), models listed from our registry with real context-window
128
+ numbers per device profile.
129
+ - Flow for `mlx-bun pi`: detect profile → ensure model → start server
130
+ in-process → `createAgentSession` against `localhost` →
131
+ `new InteractiveMode(runtime).run()`.
132
+
133
+ Open engineering questions (all look tractable, none look fatal):
134
+
135
+ 1. **One process, two workloads**: the TUI event loop and the
136
+ server/generation queue share the JS thread; mlx compute happens in
137
+ native code but graph-build is JS-side (~ms-scale per decode step).
138
+ TUI rendering is differential and cheap; needs a smoke test for
139
+ input latency during decode, not a redesign.
140
+ 2. **Bundle plumbing**: embed pi's asset list + pi-tui `.node`
141
+ prebuilds + photon wasm into the Phase 5 compile. Upstream's own
142
+ binary build proves each piece works under `bun build --compile`;
143
+ our work is making *their* asset-path resolution find files inside
144
+ *our* bundle.
145
+ 3. **Version pinning**: pi moves fast (0.79.x); pin exact, upgrade
146
+ deliberately, keep Option 2 as the fallback path when upstream
147
+ breaks SDK surface.
148
+ 4. **Identity**: embedded pi should read/write its own session dir
149
+ (`~/.mlx-bun/pi-sessions` or similar) and not fight a user's
150
+ standalone pi install. The full-control example shows every knob
151
+ needed (`agentDir`, custom storage paths).
152
+
153
+ - **Effort**: ~1–2 weeks including bundle work and latency testing.
154
+ - **Verdict**: feasible and high-value; this is the appliance moment —
155
+ one binary, one command, a real agent on a local model. `-p` and
156
+ `--mode rpc/json` remain supported natively: `runPrintMode` and
157
+ `runRpcMode` are SDK exports, so the embedded CLI keeps pi's
158
+ headless modes.
159
+
160
+ **Pros**: the true appliance — one binary, no prior installs; we pin
161
+ and test the exact pi version we ship; full control of defaults
162
+ (session dir, tools, system prompt, model list per device profile);
163
+ in-process events shared with the web UI; offline-clean.
164
+ **Cons**: most engineering (asset bundling, latency validation); we
165
+ own the upgrade treadmill for a fast-moving 0.x dependency; embedded
166
+ pi diverges from the user's own pi config/extensions unless we
167
+ deliberately opt into discovery; binary grows ~20 MB.
168
+
169
+ ### Option 4 — RPC / JSON event-stream mode (not for the terminal — for the web UI)
170
+
171
+ `pi --mode rpc` speaks JSONL over stdin/stdout: `prompt`, `steer`,
172
+ `abort`, model control, state/stats queries, plus a streamed event
173
+ model (`message_update` deltas, `tool_execution_*`, `turn_*`) and even
174
+ an extension-UI sub-protocol. The docs name "custom frontends (web
175
+ dashboards…)" as a primary use case.
176
+
177
+ - **Verdict**: wrong tool for `mlx-bun pi` (we'd be rebuilding pi's
178
+ TUI on top of pi), but the **right backbone for the built-in web
179
+ chat UI**: `mlx-bun` serves a page; the server bridges
180
+ WebSocket ↔ RPC events (or drives `AgentSession.subscribe()`
181
+ in-process — same event shapes, no subprocess). Tool calls,
182
+ steering, and progress come for free instead of us re-implementing
183
+ an agent loop in the browser.
184
+
185
+ **Pros**: documented, versioned JSONL protocol; UI-agnostic (web,
186
+ IDE, bots); steering/abort/stats built in; works with either the
187
+ subprocess or the embedded session as the engine. **Cons**: not a
188
+ terminal UX at all; strict framing rules to honor; the extension-UI
189
+ sub-protocol must be bridged if extensions prompt the user.
190
+
191
+ ## Does this make sense as a feature?
192
+
193
+ Yes — it is arguably the highest-leverage product move available:
194
+
195
+ - It converts mlx-bun from "a server you point things at" into a
196
+ complete local-AI appliance, which is the PRODUCT_ROADMAP north
197
+ star verbatim.
198
+ - The marginal cost is small because the hard parts are already done
199
+ on both sides: our OpenAI surface is SDK-verified, pi's SDK is
200
+ designed for embedding, and both projects compile with the same Bun
201
+ toolchain. MIT licensing removes the bundling question.
202
+ - One investigation, both deliverables: the same `AgentSession` event
203
+ stream powers the embedded TUI *and* the web chat UI, so direction
204
+ A's two halves share a core.
205
+ - Caveats worth stating: we take on pi's release cadence as a
206
+ dependency; the agent quality ceiling is the local model's (12B
207
+ OptiQ tool-calling is decent but this becomes the showcase — the
208
+ Lucien-direction evals would tell us exactly how good); and the
209
+ embedded TUI must not degrade decode throughput (measure, per house
210
+ rules).
211
+
212
+ ## Implementation plan
213
+
214
+ To be explicit about the recommendation: **the locally installed pi is
215
+ the v1 stepping stone, not the destination.** Users who have their own
216
+ pi always get first-class support (P1); the flagship `mlx-bun pi` ends
217
+ as the embedded, single-binary experience (P3/P4).
218
+
219
+ ### P1 — `mlx-bun harness pi` (hours–1 day)
220
+
221
+ 1. Detect pi (`which pi`, `~/.bun/bin/pi`, npm global); report version.
222
+ 2. Generate an extension file (e.g. `~/.pi/agent/extensions/mlx-bun.ts`)
223
+ that calls `pi.registerProvider("mlx-bun", ...)` with **dynamic
224
+ discovery**: fetch our `/v1/models` at pi startup so the model list
225
+ tracks the server. No models.json surgery; trivially reversible
226
+ (delete one file).
227
+ 3. Validate: tiny completion against the running server through pi's
228
+ provider path; print `pi --provider mlx-bun` launch hint.
229
+ 4. Failure modes: pi missing → print install one-liner; server down →
230
+ offer to start it.
231
+
232
+ ### P2 — `mlx-bun pi` v1: subprocess launcher (~1–2 days)
233
+
234
+ 1. Ensure server: reuse a running instance if healthy, else start
235
+ in-process and wait for readiness.
236
+ 2. Resolve the pi binary; if absent, offer the installer.
237
+ 3. Spawn pi with stdio inherited and a **session-scoped provider**: a
238
+ generated `-e <tmp-extension>.ts` registering the provider for this
239
+ run only (no global config writes), plus
240
+ `--provider mlx-bun --model <profile default>`.
241
+ 4. **Pass through all remaining argv** — this is where `mlx-bun pi -p
242
+ "..."`, `--mode rpc`, `--continue`, `@file` args work for free.
243
+ 5. Lifecycle: child exit → optionally keep the server warm (flag);
244
+ SIGINT goes to the child (pi handles double-ctrl-C), server shuts
245
+ down cleanly after.
246
+
247
+ ### P3 — embed spike (≤1 week, run via `bun run`, no bundling yet)
248
+
249
+ 1. Add pi packages as pinned deps (exact `0.79.x`).
250
+ 2. Wire `createAgentSession` full-control style: in-memory
251
+ registry/settings, our provider at the loopback baseUrl, session
252
+ dir under `~/.mlx-bun/`, default tools, our system prompt.
253
+ 3. Run `new InteractiveMode(runtime)` in the same process as the
254
+ server.
255
+ 4. **Gates** (house rule — numbers on this machine): editor latency
256
+ during 12B decode subjectively clean + decode tok/s within noise
257
+ of server-only; tool-call round-trip (read/bash/edit) works against
258
+ the 12B; clean teardown.
259
+ 5. Decide discovery policy: default isolated; `--pi-config` opt-in to
260
+ the user's `~/.pi` extensions/skills/themes.
261
+
262
+ ### P4 — single binary + shared event plumbing (~1 week)
263
+
264
+ 1. Fold pi's asset manifest (theme JSONs, PNGs, export-html templates,
265
+ photon wasm, pi-tui darwin `.node` prebuilds) into the Phase 5
266
+ `bun build --compile`; patch/verify pi's asset-path resolution
267
+ inside the bundle (upstream's own binary build is the existence
268
+ proof).
269
+ 2. CLI surface: `mlx-bun pi [pi-args]` → embedded `InteractiveMode`;
270
+ `-p` → `runPrintMode`; `--mode rpc` → `runRpcMode`. Subprocess
271
+ path (P2) remains as `--external-pi` fallback.
272
+ 3. Web chat UI rides the same `AgentSession.subscribe()` events over
273
+ WebSocket (Option 4 shape, in-process — no subprocess needed).
274
+ 4. Upgrade policy: bump pi deliberately per release, re-run the P3
275
+ gates as the regression suite.
276
+
277
+ ### First-run starter model (folds into P2/P4; model gated on Phase 14)
278
+
279
+ The first-run experience must not be "watch a progress bar for 10
280
+ minutes." Plan: a sub-gigabyte **starter model** downloads first and is
281
+ chatting within ~1 minute — with a docs-grounded "tour guide" system
282
+ prompt (README + server-api + pi guide preloaded, so a 1B doesn't
283
+ hallucinate about mlx-bun) and a banner: *"the ideal model for your Mac
284
+ is downloading in the background."* The profile model streams behind it
285
+ (sequenced, not parallel, so downloads don't split bandwidth); swap
286
+ when ready.
287
+
288
+ Measured sizes (HF tree API, 2026-06-12):
289
+
290
+ | Candidate | Download | Note |
291
+ |---|---|---|
292
+ | gemma-4-e2b-it-OptiQ-4bit | 5.26 GB | smallest Gemma — disqualifies the family for the starter slot |
293
+ | gemma-4-e4b / 12B OptiQ-4bit | 7.0 / 8.4 GB | profile models, not starters |
294
+ | **Qwen3.5-0.8B-OptiQ-4bit** | **0.89 GB** | **preferred**: rides the Phase 14 Qwen port; doubles as the first tier-a parity target |
295
+ | MiniCPM5-1B-OptiQ-4bit | 0.92 GB | strong (52% MMLU, +4.44 OptiQ delta) but a third architecture family — A/B later |
296
+
297
+ Decisions: starter is **downloaded, never bundled** (a ~900 MB binary
298
+ kills brew install); it stays on disk afterward (future
299
+ speculative-decoding draft + always-works fallback). Hot-swap (or
300
+ offer-to-swap) when the profile model lands is the only new runtime
301
+ work. Dependency: a runnable sub-GB model requires the Phase 14 Qwen
302
+ bring-up — the starter feature is the reason the 0.8B goes first there.
303
+
304
+ ## Recommended sequence
305
+
306
+ P1 + P2 now (days, immediate demo value, zero coupling) → P3 spike
307
+ with its latency gate → P4 single-binary flagship, reusing the event
308
+ plumbing for the built-in web UI.