@fugood/buttress-server 2.26.0-beta.2 → 2.26.0-beta.21

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 (70) hide show
  1. package/README.md +415 -10
  2. package/config/function-samples/README.md +1 -0
  3. package/config/function-samples/classify-ticket.ts +73 -0
  4. package/config/function-samples/run-agent.ts +39 -0
  5. package/config/sample.toml +67 -0
  6. package/lib/agent/cli.d.ts +19 -0
  7. package/lib/agent/client.d.ts +66 -0
  8. package/lib/agent/codemode-worker.d.ts +1 -0
  9. package/lib/agent/codemode.d.ts +7 -0
  10. package/lib/agent/config.d.ts +15 -0
  11. package/lib/agent/context.d.ts +11 -0
  12. package/lib/agent/decisions.check.d.ts +1 -0
  13. package/lib/agent/decisions.d.ts +113 -0
  14. package/lib/agent/device-auth.d.ts +3 -0
  15. package/lib/agent/device-tools.d.ts +5 -0
  16. package/lib/agent/loopback.d.ts +21 -0
  17. package/lib/agent/mcp.d.ts +22 -0
  18. package/lib/agent/models.d.ts +22 -0
  19. package/lib/agent/service.d.ts +16 -0
  20. package/lib/agent/sessions.d.ts +30 -0
  21. package/lib/agent/tools.d.ts +35 -0
  22. package/lib/agent/tui.d.ts +17 -0
  23. package/lib/agent/types.d.ts +155 -0
  24. package/lib/cli-KbjOekMI.mjs +22 -0
  25. package/lib/client-qqI-HCOj.mjs +8 -0
  26. package/lib/codemode-Cva7OeQX.mjs +112 -0
  27. package/lib/codemode-worker.mjs +3 -0
  28. package/lib/command-nXCdWBlc.mjs +38 -0
  29. package/lib/config-DiYweJ2m.mjs +2 -0
  30. package/lib/functions/executor.d.ts +7 -1
  31. package/lib/functions/index.d.ts +3 -1
  32. package/lib/functions/status.d.ts +1 -1
  33. package/lib/functions/templates.d.ts +1 -1
  34. package/lib/functions/types.d.ts +15 -2
  35. package/lib/index.d.ts +12 -5
  36. package/lib/index.mjs +146 -93
  37. package/lib/mcp-DzknURyz.mjs +5 -0
  38. package/lib/mlx-bridge.py +707 -0
  39. package/lib/net-OvedjqrB.mjs +46 -0
  40. package/lib/protocol-WkPByalT.mjs +468 -0
  41. package/lib/quickjs.wasm +0 -0
  42. package/lib/rolldown-runtime-dTnj95Mm.mjs +2 -0
  43. package/lib/routes/agents.d.ts +66 -0
  44. package/lib/routes/decision-shared.d.ts +28 -0
  45. package/lib/routes/generator-cache.d.ts +3 -1
  46. package/lib/routes/index.d.ts +2 -0
  47. package/lib/routes/llm-shared.d.ts +8 -0
  48. package/lib/routes/openai-compat.d.ts +13 -2
  49. package/lib/routes/openai-decisions.check.d.ts +1 -0
  50. package/lib/routes/openai-decisions.d.ts +97 -0
  51. package/lib/routes/status.d.ts +2 -1
  52. package/lib/routes/typesafe-compat.check.d.ts +1 -0
  53. package/lib/routes/typesafe-compat.d.ts +30 -0
  54. package/lib/services/ggml-decision.d.ts +80 -0
  55. package/lib/services/ggml-llm.d.ts +29 -1
  56. package/lib/services/index.d.ts +3 -0
  57. package/lib/tui-ClNupsCH.mjs +2 -0
  58. package/lib/types.d.ts +48 -2
  59. package/lib/ui/command.d.ts +2 -0
  60. package/lib/ui/host.d.ts +1 -0
  61. package/lib/ui/window-module.d.ts +6 -0
  62. package/lib/utils/config.d.ts +3 -3
  63. package/lib/utils/cors.d.ts +1 -0
  64. package/lib/utils/decision-sharing.d.ts +37 -0
  65. package/lib/utils/downloadStatus.d.ts +57 -0
  66. package/lib/utils/hostFacts.d.ts +13 -0
  67. package/lib/utils/workspaceState.d.ts +8 -0
  68. package/lib/wrapper-3PX6qE3t.mjs +7 -0
  69. package/package.json +17 -9
  70. package/public/status.html +219 -1489
package/README.md CHANGED
@@ -27,7 +27,8 @@ irm https://cdn.bricks.tools/bricks-buttress/release/install.ps1 | iex
27
27
  The installer detects the host's supported GGML accelerator (CUDA > Vulkan >
28
28
  Snapdragon, falling back to the default CPU/Metal build) and downloads only
29
29
  the native modules the binary needs — the ggml llama.node / whisper.node
30
- variant packages plus onnxruntime, sharp, and oxc-transform prebuilds — into a
30
+ variant packages plus onnxruntime, sharp, oxc-transform and (where gpuix
31
+ publishes one) the settings window's gpuix prebuilds — into a
31
32
  `node_modules` sidecar next to the executable. It installs to
32
33
  `~/.bricks-cli/bin` (shared with the BRICKS CLI, so one PATH entry covers
33
34
  both). Override detection with
@@ -60,6 +61,67 @@ npx bricks-buttress --config ./config.toml
60
61
  npx bricks-buttress
61
62
  ```
62
63
 
64
+ ## Settings, status and chat window (`bricks-buttress ui`)
65
+
66
+ `bricks-buttress ui` opens a native desktop window for one config file:
67
+
68
+ ```bash
69
+ bricks-buttress ui -c ./config.toml # or: bricks-buttress ui ./config.toml
70
+ bricks-buttress ui -c ./config.toml --tab chat # open on a given tab: chat, status or settings
71
+ ```
72
+
73
+ - **Settings** edits the TOML through a form: server, runtime defaults, discovery and endpoints, and generators (a HuggingFace search adds models, and each model shows whether it fits the memory budget). `[functions]`, `[[agents]]`, `[agents_options]` and `[env]` are edited as raw TOML. HuggingFace requests use `[runtime] huggingface_token`, else `$HUGGINGFACE_TOKEN`, else `$HF_TOKEN`.
74
+ - **Status** is a live dashboard of the server started with this config: identity, hardware, pre-downloads, the generators and requests of each backend, and local functions.
75
+ - **Chat** talks to the `[[agents]]` of that server (see [Chat tab](#chat-tab) below).
76
+
77
+ Saving shows a diff of the file first, and an invalid value blocks it. The write is atomic (temporary file + rename), and the previous content is kept as `<config>.bak` next to the file (one backup, replaced on each save). The form rewrites the sections it owns, so comments inside them are lost; the raw sections are written back verbatim, comments included. When the file changes on disk while the window is open, the window offers to reload it or to keep your edits; it never merges them. A missing file opens as a new config from defaults, and nothing is written until you confirm **Create file**.
78
+
79
+ The window never starts, stops or restarts the server, and the server reads its config only at startup: restart it to apply saved changes (the window shows the start command). The settings stay editable while the server runs.
80
+
81
+ **Server detection.** The window reads `[server] port` (default `2080`) and `id` (default `buttress-<machineId>`) from the config, then asks `http://127.0.0.1:<port>` for the server id every 3 seconds:
82
+
83
+ - the same `id` answers: the server runs, and the window opens on Chat when the server reports agents, else on Status;
84
+ - another `id` answers: the port is used by another buttress server;
85
+ - something answers HTTP without a buttress id: the port is used by another program;
86
+ - no answer: the server is not running. The window opens on Settings, and its Status tab turns live by itself when the server appears.
87
+
88
+ The id comes from `/buttress/info` when the server serves it, else from `server.id` in `/buttress/status`. The server serves `/buttress/info` only while autodiscover HTTP is on (`autodiscover = true`, or an `[autodiscover]` table without `http.enabled = false`), and the window asks for it at the default path only: with a custom `http.path` it identifies the server from `/buttress/status`. `/buttress/status` is always served, so detection works with autodiscover off too. Without `/buttress/info` the window shows no addresses or workspace binding for the server.
89
+
90
+ When you save a new `[server] port` or `id` while the server runs, the window keeps showing the running server (with a "restart required" banner) until it stops, then looks for the new port and id.
91
+
92
+ `--tab chat|status|settings` opens the window on that tab instead of the one detection picks.
93
+
94
+ **Platform support.** The window uses gpuix (a React renderer for GPUI) and runs under Bun on `darwin-arm64`, `linux-x64` and `win32-x64`, the platforms with gpuix prebuilt modules. Under Node.js or on another platform the command stops with a message; open `http://<host>:<port>/status` in a browser instead. The standalone binary includes the window (its installer adds gpuix's native module to the sidecar on those platforms); the npm package does not include `@fugood/buttress-ui`, and the command says so. Light or dark follows the macOS appearance; other platforms use light.
95
+
96
+ ### Chat tab
97
+
98
+ The Chat tab is a client of the [agent endpoints](#agents-experimental), like `bricks-buttress agent`: it calls `http://127.0.0.1:<port>` of the config with the runtime token the server writes at startup, so it works on an unbound server without `allow_unauthenticated`. The sidebar groups the sessions by agent, newest first, with the first prompt as the title and a badge on forked sessions; it refreshes every 10 seconds and after each run. A run streams into the chat: the thinking (collapsed), the answer as markdown, each tool call with its arguments and result, and a footer with the stop reason, turns and tokens. Enter sends and Shift+Enter starts a new line. Stop aborts the run and the session stays continuable; Fork sends the next prompt to a copy of the session; New session (or `/new` in the composer) starts a fresh one. The window runs one prompt at a time. The server keeps no turn of a stopped or failed run in the session, so a session opened later shows such a prompt with a "No saved answer" note. The tab needs the running server: without it the tab shows the start command, and a config without `[[agents]]` shows where to add them (Settings › Advanced). When the server starts again, the tab reads the new runtime token and reloads the agents by itself. A saved file with `[[agents]]` that the running server does not have (it was started before they were saved, or from another file) shows the restart command instead of an error; agents the server would refuse to start with show that refusal, and the Advanced section warns about it before you save. An agent on a `buttress/…` model needs `[openai_compat] enabled = true` (see [Agents](#agents-experimental), Models).
99
+
100
+ ### Status page and `/buttress/status`
101
+
102
+ The server still serves the dashboard at `/status` for any browser (phone width included). It is built from the same view model as the Status tab (see DEVELOPMENT.md) and polls `/buttress/status` every 3 seconds, and `/buttress/info` while the server serves it (the page footer names the path, or says there is none).
103
+
104
+ Next to the per-backend status (`ggmlLlm`, `ggmlStt`, `ggmlTts`, `ggmlDecision`, `mlxLlm`, `onnxStt`, `onnxTts`) and `functions`, `/buttress/status` returns:
105
+
106
+ | Field | Content |
107
+ | ---------------------- | -------------------------------------------------------------------------------------------------------- |
108
+ | `server.id`, `server.name`, `server.version`, `server.port` | The identity `/buttress/info` reports, here also when autodiscover HTTP is off |
109
+ | `server.startedAt` | ISO time the server started |
110
+ | `server.uptimeMs` | Milliseconds since `startedAt` |
111
+ | `server.sessions.open` | RPC sessions in memory: connected clients, plus disconnected ones inside their `session_timeout` window |
112
+ | `server.generatorTypes` | The generator types this server loaded from the config (entries with `enabled = false` excluded), each once |
113
+ | `server.generators` | The generators this server loaded from the config, in file order: `type`, `repoId` and `quantization` (`null` when the entry sets none). The status page lists the ones no client has loaded yet |
114
+ | `server.features.openaiCompat`, `.anthropicMessages`, `.typesafeCompat` | `true` when that compatibility endpoint is served (env var or config flag) |
115
+ | `server.features.autodiscover` | `{ udpPort, udpAnnouncing, httpPath }` as configured (`null` for a transport that is off), or `null` when autodiscover is off. `udpAnnouncing` is `false` when UDP is configured but does not run: it needs a server bound to a workspace with an announce key |
116
+ | `server.features.functions` | `{ enabled, count, daemons, hotReload }`: the local functions loaded now, and the daemon functions configured (`meta.daemon`), including ones that failed to start |
117
+ | `server.features.agents` | `{ enabled, count, names }` of the configured agents, or `null` when none run |
118
+ | `server.host` | This machine, read at startup: `cpuModel`, `cpuCores`, `totalMemoryBytes`, `appleSilicon`, and `gpuName` on Apple Silicon (the chip name) |
119
+ | `server.caps` | The hardware probe of each configured generator type, run at startup: `type`, `score`, `hasGpu`, `usableBytes` (the `generators` list of `/buttress/info`, here also when that route is off) |
120
+ | `downloads.active` | Pre-downloads in progress: `type`, `repoId`, `filename`, `startedAt`, `progress` (0–1), `receivedBytes`, `totalBytes`, `bytesPerSecond` |
121
+ | `downloads.recent` | The last 20 finished pre-downloads, newest first: `type`, `repoId`, `filename`, `totalBytes`, `durationMs`, `finishedAt`, `success`, `error` |
122
+
123
+ `downloads` covers the startup pre-downloads (`download = true`) only; a download that starts when a client loads a model that is not cached yet is not listed. The backends report download progress as a fraction, so `receivedBytes` is `0` and `totalBytes` / `bytesPerSecond` are `null`: use `progress`.
124
+
63
125
  ## Workspace Binding (`bricks buttress`)
64
126
 
65
127
  By default, a buttress-server runs in **public mode**: any client on the LAN can connect, no auth required. To restrict access to a single BRICKS workspace and enable workspace-scoped JWT auth, **bind** the server with the `bricks buttress` CLI commands. Once bound, the server only accepts WebSocket / file-transfer requests carrying a valid access token signed by that workspace's issuer.
@@ -69,14 +131,14 @@ The `bricks` CLI is the tool that performs the binding and writes the local stat
69
131
  ### Bind a server to a workspace
70
132
 
71
133
  ```bash
72
- # Pair the local machine's buttress-server with the workspace of the current bricks-cli profile
134
+ # Pair the local server and provision a workspace DevTools token using the current CLI profile
73
135
  bricks buttress bind
74
136
 
75
137
  # Override the auto-detected server id, give it a friendly name, or write to a custom state dir
76
138
  bricks buttress bind --server-id buttress-mac-studio --name "Studio LLM" --state-dir /etc/buttress
77
139
 
78
140
  # For headless/remote setups: emit state.json to stdout instead of writing to disk
79
- bricks buttress bind --print > /etc/buttress/state.json
141
+ (umask 077; bricks buttress bind --print > /etc/buttress/state.json)
80
142
  ```
81
143
 
82
144
  The state file (`~/.bricks-cli/buttress/state.json` by default, or `$BRICKS_BUTTRESS_STATE_DIR`) stores:
@@ -84,6 +146,15 @@ The state file (`~/.bricks-cli/buttress/state.json` by default, or `$BRICKS_BUTT
84
146
  - `workspace.id` / `workspace.name` — which workspace this server belongs to
85
147
  - `workspace.serverId` — the server's stable id (defaults to `buttress-<machineId>`)
86
148
  - `workspace.issuerPublicKey` + `workspace.kid` — Ed25519 SPKI used to verify access tokens
149
+ - `devtools` — workspace-scoped DevTools JWT (`k: "da"`), workspace id and expiry,
150
+ provisioned automatically by `bind` for agent device tools
151
+
152
+ Normal bind/status output reports only token expiry, never the token. The CLI writes
153
+ state with mode `0600`; `--print` includes credentials and must be treated as a secret.
154
+ Rebinding provisions a fresh token (default lifetime: 30 days) and rotates the announce
155
+ key. Issuance failure stops binding before changing the remote announce key. Rebinding
156
+ or unbinding does not revoke previously issued DevTools JWTs; they remain valid until
157
+ expiry or issuer-key rotation.
87
158
 
88
159
  **Restart `bricks-buttress` after binding** for the change to take effect — the state file is read once at startup.
89
160
 
@@ -153,7 +224,9 @@ Configuration is loaded from a TOML file passed via `--config` / `-c`. Every top
153
224
  | `[autodiscover]` | LAN UDP / HTTP / mDNS discovery toggles |
154
225
  | `[openai_compat]` | Enable `/oai-compat/v1/*` — see [Compatibility Endpoints](#compatibility-endpoints-experimental) |
155
226
  | `[anthropic_messages]` | Enable `/anthropic-messages` — see [Compatibility Endpoints](#compatibility-endpoints-experimental)|
227
+ | `[typesafe_compat]` | Enable `/typesafe-compat/v1/*` — see [Compatibility Endpoints](#compatibility-endpoints-experimental) |
156
228
  | `[functions]` | Enable local functions — see [Local Functions](#local-functions-experimental) |
229
+ | `[[agents]]` | Config-defined agents — see [Agents](#agents-experimental) |
157
230
  | `[[generators]]` | Array of generator instances — one entry per loaded model |
158
231
 
159
232
  ### `[env]`
@@ -206,6 +279,8 @@ Most ggml-llm `[generators.model]` keys can also live in `[runtime]` as defaults
206
279
  | `no_extra_bufts` | boolean | Disable extra compute buffer types |
207
280
  | `cpu_mask`, `cpu_strict` | string / boolean | CPU affinity (advanced) |
208
281
  | `devices` | string[] | Restrict to specific GGML devices |
282
+ | `split_mode` | `"none"` / `"layer"` / `"row"` / `"tensor"` | ggml-llm GPU splitting: single GPU, layers + KV (default), weight rows, or weights + KV (experimental tensor parallelism) |
283
+ | `tensor_split` | number[]\|string | ggml-llm per-GPU proportions, e.g. `[3, 1]` or `"3,1"`; unset uses the native automatic split |
209
284
  | Speculative keys | various | `speculative`, `spec_type`, `spec_draft_n_max/n_min/p_min/p_split`, plus draft-model GPU/cache settings |
210
285
 
211
286
  ### `[autodiscover]`
@@ -229,7 +304,8 @@ Every generator entry has a `type`, an optional `[generators.backend]` table, an
229
304
 
230
305
  ```toml
231
306
  [[generators]]
232
- type = "ggml-llm" # or "ggml-stt" / "ggml-tts" / "mlx-llm"
307
+ type = "ggml-llm" # or "ggml-stt" / "ggml-tts" / "ggml-decision" / "mlx-llm"
308
+ # enabled = false # keep the entry but skip it at startup
233
309
 
234
310
  [generators.backend]
235
311
  # (see per-type sections below)
@@ -239,6 +315,11 @@ repo_id = "..."
239
315
  # (see per-type sections below)
240
316
  ```
241
317
 
318
+ | Key | Type | Notes |
319
+ | -------------------- | ------- | ----------------------------------------------------------------------------------------- |
320
+ | `type` *(required)* | string | `ggml-llm`, `ggml-stt`, `ggml-tts`, `ggml-decision`, `mlx-llm`, `onnx-stt` or `onnx-tts` |
321
+ | `enabled` | boolean | Default `true`. With `false` the server skips the entry at startup: it is not served, announced or pre-downloaded, but stays in the file (the settings window uses this to switch a model off) |
322
+
242
323
  #### Common `[generators.model]` keys
243
324
 
244
325
  Shared by **all** generator types:
@@ -249,7 +330,7 @@ Shared by **all** generator types:
249
330
  | `revision` | string | Default `"main"` |
250
331
  | `download` | boolean | Pre-download at server startup (default `false`) |
251
332
 
252
- Additional keys honored by **ggml-llm**, **ggml-stt** and **ggml-tts** (mlx-llm gets quantization from the repo itself and does not use these):
333
+ Additional keys honored by **ggml-llm**, **ggml-stt**, **ggml-tts** and **ggml-decision** (mlx-llm gets quantization from the repo itself and does not use these):
253
334
 
254
335
  | Key | Type | Notes |
255
336
  | ------------------------- | --------- | -------------------------------------------------------------------------------- |
@@ -267,6 +348,10 @@ Additional keys honored by **ggml-llm**, **ggml-stt** and **ggml-tts** (mlx-llm
267
348
 
268
349
  Loads a GGUF LLM. Runtime keys above can be overridden per-generator under `[generators.model]`; `[generators.backend]` only controls backend selection and resource planning.
269
350
 
351
+ GPU splitting (`split_mode`, `tensor_split`) is currently configurable only in Buttress.
352
+ TODO: Expose these options in the LLM (GGML) generator. The experimental `tensor` mode
353
+ requires a compatible GPU backend and model architecture.
354
+
270
355
  **`[generators.backend]`**
271
356
 
272
357
  | Key | Type | Default | Notes |
@@ -282,6 +367,7 @@ Loads a GGUF LLM. Runtime keys above can be overridden per-generator under `[gen
282
367
  | ----------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------- |
283
368
  | `n_ctx` | number | Context window. Auto-capped at the model's training context. |
284
369
  | `n_gpu_layers` | number\|`"auto"` | Layers offloaded to GPU (default `"auto"`) |
370
+ | `split_mode`, `tensor_split` | string / number[]\|string | Per-model overrides for the GPU split defaults in `[runtime]` |
285
371
  | `n_batch` | number | Prompt batch size (default `512`) |
286
372
  | `embedding` | boolean | Enable llama.cpp embedding mode for a dedicated embedding model (default `false`) |
287
373
  | `pooling_type` | string | Optional embedding pooling override such as `mean` |
@@ -354,12 +440,21 @@ spec_draft_cache_type_v = "f16"
354
440
 
355
441
  Loads a Whisper GGML model for speech-to-text.
356
442
 
443
+ With `@fugood/whisper.node` 1.2.0-rc.0, `snapdragon` enables experimental
444
+ Whisper Hexagon NPU acceleration on Linux arm64 with a working Qualcomm
445
+ HTP/FastRPC runtime. This variant does not include OpenCL. Install it with
446
+ `--ggml-variant=snapdragon` (the default CPU package remains available for fallback).
447
+ The platform package includes the HTP libraries; the loader defaults
448
+ `ADSP_LIBRARY_PATH` to that package directory and `GGML_HEXAGON_NDEV` to `16`,
449
+ without overriding explicit environment values. Hexagon always enables flash
450
+ attention, even when `use_flash_attn` is off.
451
+
357
452
  **`[generators.backend]`**
358
453
 
359
454
  | Key | Type | Default | Notes |
360
455
  | --------------------- | -------- | ----------------------------- | ---------------------------------- |
361
- | `variant` | string | auto | `cuda` / `vulkan` / `default` |
362
- | `variant_preference` | string[] | `["cuda","vulkan","default"]` | Probe order |
456
+ | `variant` | string | auto | `cuda` / `vulkan` / `snapdragon` / `default` |
457
+ | `variant_preference` | string[] | `["cuda","vulkan","snapdragon","default"]` | Probe order |
363
458
  | `gpu_memory_fraction` | number | `0.85` | |
364
459
  | `cpu_memory_fraction` | number | `0.5` | |
365
460
 
@@ -384,7 +479,7 @@ Loads a Whisper GGML model for speech-to-text.
384
479
  [[generators]]
385
480
  type = "ggml-stt"
386
481
  [generators.backend]
387
- variant_preference = ["cuda", "vulkan", "default"]
482
+ variant_preference = ["cuda", "vulkan", "snapdragon", "default"]
388
483
  [generators.model]
389
484
  repo_id = "BricksDisplay/whisper-ggml"
390
485
  filename = "ggml-large-v3-turbo-q8_0.bin"
@@ -467,6 +562,105 @@ vocoder_batch_size = 4096
467
562
 
468
563
  ---
469
564
 
565
+ ### `ggml-decision` (typed decisions via `@fugood/llama.node`)
566
+
567
+ Loads a typed decision model (System One — Julia-1, Laya, Kev, Jevling, lev,
568
+ ...) and answers `choice` / `score` / `noul` questions about a state with
569
+ calibrated probabilities: one forward pass per answer, no generated tokens.
570
+ Clients reach it through the Typed Decision (GGML) generator; local
571
+ functions through `context.buttress.decide`.
572
+
573
+ The model is checked when its context loads: a GGUF with no decision metadata
574
+ (`<arch>.decision.type`) and no `system_one` template, or one whose decision
575
+ type this build cannot serve, fails the load with the reason. Requests on one
576
+ generator run one at a time.
577
+
578
+ **Sharing a model with `ggml-llm`** — a model that both generates and decides
579
+ (Jevling, for one) can be configured as both a `ggml-llm` and a
580
+ `ggml-decision`. By default each loads its own context. Set
581
+ `[generators.backend] share_llm_context = true` on the decision entry to run
582
+ its decisions on the LLM's context instead, when the two entries name the same
583
+ weights (same `repo_id`, `revision`, `filename`, `quantization`, `url` and
584
+ `local_path`): the model is then loaded once, with the LLM's config. This
585
+ applies to local functions, the TypeSafe-compatible endpoint and client
586
+ sessions (whose decision runtime options then do not apply).
587
+
588
+ Sharing trades throughput for memory. A decision takes one of the LLM's
589
+ `n_parallel` slots, and its forward passes run between the LLM's batches, so
590
+ concurrent completions stop generating while it runs (in a CPU test, a
591
+ 128-token completion took twice as long beside six decisions; with a separate
592
+ decision context it did not slow down). Results are not affected: the slot's
593
+ sequence and prompt cache are cleared after each decision. Without
594
+ `kv_unified = true` on the LLM a slot holds `n_ctx / n_parallel` tokens, which
595
+ must fit the state plus the questions. The decision loads its own context
596
+ anyway when it sets `rank_pooling` (it needs an embedding context), when its
597
+ projector differs from the LLM's, or when the LLM is embedding or vocab-only.
598
+ The server logs at startup which decision models share.
599
+
600
+ ```toml
601
+ [[generators]]
602
+ type = "ggml-llm"
603
+ [generators.model]
604
+ repo_id = "BricksDisplay/jevling-0.8b-v1-GGUF"
605
+ filename = "jevling-0.8b-v1-q8_0-embf16.gguf"
606
+ [generators.runtime]
607
+ n_ctx = 16384
608
+
609
+ [[generators]]
610
+ type = "ggml-decision"
611
+ [generators.backend]
612
+ share_llm_context = true
613
+ [generators.model]
614
+ repo_id = "BricksDisplay/jevling-0.8b-v1-GGUF"
615
+ filename = "jevling-0.8b-v1-q8_0-embf16.gguf"
616
+ ```
617
+
618
+ **`[generators.backend]`** — same keys as `ggml-tts` (`variant`,
619
+ `variant_preference`, `gpu_memory_fraction`, `cpu_memory_fraction`).
620
+
621
+ **`[generators.model]`** — common keys plus:
622
+
623
+ | Key | Type | Default | Notes |
624
+ | ------------------------- | -------- | -------------------------------- | ------------------------------------------------------------------ |
625
+ | `preferred_quantizations` | string[] | `["q8_0", "q4_k_m", <no-quant>]` | Default fallback chain |
626
+ | `mmproj_repo_id` | string | falls back to `repo_id` | Vision projector repo, for models whose prompt takes images |
627
+ | `mmproj_filename` | string | auto (`*mmproj*`) | Pin a specific projector artifact |
628
+ | `mmproj_url` | string | — | Direct projector URL (skips manifest lookup) |
629
+ | `mmproj_revision` | string | falls back to `revision` | |
630
+ | `mmproj_local_path` | string | — | Requires `allow_local_file` |
631
+ | `rank_pooling` | boolean | `false` | `system_one` models with a classification head (`rank_head` readout) |
632
+ | `use_gpu` | boolean | `true` | Force-disable GPU even when available |
633
+ | `n_gpu_layers` | number | `99` | Forced to `0` when the GPU is unused |
634
+
635
+ Setting any `mmproj_*` key loads a projector; requests can then pass `images`
636
+ (file paths or data URLs). The projector is ignored, with a warning, when the
637
+ model's prompt has no place for images.
638
+
639
+ **Runtime extras** — under `[runtime]` for ggml-decision:
640
+
641
+ | Key | Type | Default | Notes |
642
+ | -------------------------- | ------ | --------------- | ---------------------------------------- |
643
+ | `n_ctx` | number | `4096` | Must hold the state plus every question |
644
+ | `n_batch` / `n_ubatch` | number | `2048` / `512` | |
645
+ | `max_threads` | number | auto | Caps the llama.cpp thread count |
646
+ | `flash_attn_type` | string | `auto` | `auto` / `on` / `off` |
647
+ | `context_release_delay_ms` | number | `10000` | `0` releases the context right away |
648
+
649
+ **Example**
650
+
651
+ ```toml
652
+ [[generators]]
653
+ type = "ggml-decision"
654
+ [generators.model]
655
+ repo_id = "ggml-org/Julia-1-GGUF"
656
+ filename = "Julia-1-Q8_0.gguf"
657
+ download = true
658
+ [generators.runtime]
659
+ n_ctx = 4096
660
+ ```
661
+
662
+ ---
663
+
470
664
  ### `mlx-llm` (Apple Silicon, Python `mlx-lm` / `mlx-vlm` bridge)
471
665
 
472
666
  Loads an MLX-format model on Apple Silicon. On first use, the backend creates a virtualenv at `{cache_dir}/mlx-env` and installs `mlx_lm_package`, `mlx_vlm_package`, plus `torch` and `torchvision` (required by some VLM processors). If an existing venv already has `mlx_vlm` and `torch` importable, the install step is skipped. There is no `[generators.backend]` section.
@@ -575,9 +769,12 @@ Buttress server for remote inference with GGML backends.
575
769
 
576
770
  Usage:
577
771
  bricks-buttress [options]
772
+ bricks-buttress ui [-c] <config.toml>
578
773
  bricks-buttress update [--check] [-y] [--channel <release|beta>]
579
774
 
580
775
  Commands:
776
+ ui Open the settings, status and chat window for a
777
+ config file (see `bricks-buttress ui --help`)
581
778
  update Update bricks-buttress to the latest version.
582
779
  Standalone binary installs re-run the CDN
583
780
  installer (refreshing the native-module
@@ -612,7 +809,7 @@ Examples:
612
809
 
613
810
  ## Compatibility Endpoints (Experimental)
614
811
 
615
- The server can expose OpenAI- and Anthropic-compatible HTTP endpoints in addition to the native RPC. Each endpoint is opt-in via the TOML config:
812
+ The server can expose OpenAI-, Anthropic- and TypeSafe-compatible HTTP endpoints in addition to the native RPC. Each endpoint is opt-in via the TOML config:
616
813
 
617
814
  ```toml
618
815
  [openai_compat]
@@ -622,14 +819,19 @@ enabled = true
622
819
  [anthropic_messages]
623
820
  enabled = true
624
821
  # cors_allowed_origins = ["http://localhost:3000"]
822
+
823
+ [typesafe_compat]
824
+ enabled = true
625
825
  ```
626
826
 
627
827
  | Endpoint | Config flag |
628
828
  | --------------------- | ------------------------------------ |
629
829
  | `/oai-compat/v1/*` | `[openai_compat] enabled = true` |
630
830
  | `/anthropic-messages` | `[anthropic_messages] enabled = true` |
831
+ | `/typesafe-compat/v1/*` | `[typesafe_compat] enabled = true` |
631
832
 
632
- Completion requests may select only LLM models declared in `[[generators]]`. Use each
833
+ Completion requests may select only LLM models declared in `[[generators]]`, and decision
834
+ requests only `ggml-decision` generators. Use each
633
835
  endpoint's `GET /v1/models` response for valid model IDs; unknown IDs are rejected before the
634
836
  server starts or downloads a generator.
635
837
 
@@ -693,6 +895,7 @@ export default async function ({ path }: { path: string }, context: ButtressFunc
693
895
  | `buttress.detokenize({ model?, tokens })` | Convert token ids back into text with the same model. |
694
896
  | `buttress.transcribe({ model?, filePath \| audioData, options? })` | Transcribe audio with this server's STT generator. |
695
897
  | `buttress.synthesize({ model?, text, options? })` | Synthesize speech with this server's TTS generator (`onnx-tts` or `ggml-tts`); the WAV lands in `tempDir` → `{ path, sampling_rate, channels }`. |
898
+ | `buttress.decide({ model?, state, questions, images? })` | Answer typed `choice` / `score` / `noul` questions about `state` with this server's `ggml-decision` generator → `{ model, answers, usage }`. `answers[id]` is typed from its question. `images` are local paths or data URLs. |
696
899
  | `bank.list({ keyword?, meta?, ids? })` / `bank.get(id)` / `bank.update(properties, { dontNotify? })` / `bank.remove(id)` | Read/write the bound workspace's remote **Data Bank** (see below). To listen for changes, use `bank.subscribe` inside a daemon function. |
697
900
  | `daemons.emit(name, event, data?)` / `daemons.list()` | Signal a running **daemon function**'s `onEvent` handlers (see below) / list running daemon names. |
698
901
  | `emit(event, data)` | Progress event; delivered to SSE callers, ignored otherwise. |
@@ -850,6 +1053,208 @@ The handler receives `{ method, path, name?, headers, query, token, workspaceAut
850
1053
 
851
1054
  Function files themselves are trusted input, exactly like this config file. They run in a `node:vm` context with a clean global (no ambient `process` or `require`), but that is for clarity, not isolation — a function that is handed `spawn` can do anything the server process can. Only put code you wrote (or reviewed) in the functions directory.
852
1055
 
1056
+ ## Agents (Experimental)
1057
+
1058
+ Buttress can host **agents**: pi-based LLM loops that run inside the server
1059
+ process, use **local functions** (and MCP servers) as their tools, and keep
1060
+ **config-scoped sessions** on disk. The primary consumer is automation — a
1061
+ local function or daemon calls `context.agents.run(...)` to add multi-step
1062
+ reasoning to a server-side workflow; an interactive CLI exists for driving and
1063
+ inspecting the same agents.
1064
+
1065
+ ```toml
1066
+ [[agents]]
1067
+ name = "ops-assistant" # unique; the session scope key
1068
+ model = "buttress/ggml-org/gpt-oss-20b-GGUF" # split on the FIRST slash: provider/model-id
1069
+ # model = "anthropic/claude-sonnet-5" # any pi-supported provider; API key from env
1070
+ system_prompt_file = "./prompts/ops.md" # or inline: system_prompt = "..."
1071
+ tools = ["get_server_status", "restart_service"] # local function names (explicit; no wildcard)
1072
+ local_devices = false # opt in to LAN scan + device DevTools tools
1073
+ max_turns = 30 # assistant↔tool round-trips per run (default 30)
1074
+ # codemode = true # opt in: a sandboxed JS tool that calls the other tools
1075
+ # max_tokens_per_run = 200000 # per-run token budget; unset = unlimited
1076
+ # temperature = 0.2 # unrecognized keys pass through to generation
1077
+
1078
+ [agents.mcp_servers.github] # optional MCP servers (StreamableHTTP or stdio)
1079
+ url = "https://api.githubcopilot.com/mcp/"
1080
+ # headers = { Authorization = "Bearer ..." }
1081
+ # optional = true # continue without this server if it won't connect
1082
+ # timeout = 60 # per-request seconds (progress renews it)
1083
+ # exposure = "direct" # "hidden": unreachable; "codemode": scripts only (needs codemode = true)
1084
+ # tool_exposure = { "delete_*" = "hidden" } # per tool, by exact name or * pattern
1085
+
1086
+ [agents_options] # optional; defaults shown
1087
+ # sessions_dir = "./.buttress-agent/sessions" # relative to this config file
1088
+ # session_max_age = "30d" # retention sweep; 0 disables
1089
+ # session_max_count = 500 # per agent; 0 disables
1090
+ # max_depth = 2 # function → agent → function → agent chain cap
1091
+ # allow_unauthenticated = false # serve /agents on an UNBOUND server (see below)
1092
+ ```
1093
+
1094
+ **Models.** `buttress/<repo_id>` targets a configured `[[generators]]` LLM
1095
+ (ggml/mlx) and is validated at startup; traffic flows through an in-process
1096
+ OpenAI-compat loopback (no socket). The loopback calls the server's own
1097
+ `/oai-compat/v1` routes, which exist only with `[openai_compat] enabled = true`
1098
+ (or `ENABLE_OPENAI_COMPAT_ENDPOINT=1`): an agent with a `buttress/…` model
1099
+ needs it, or every run fails with `404 NOT_FOUND`. Any other provider prefix is a pi
1100
+ built-in provider (`anthropic/…`, `openai/…`, `google/…`, …) authenticated by
1101
+ its usual environment variable (`ANTHROPIC_API_KEY`, …) — set it via `[env]`
1102
+ or the process environment. OAuth-based logins are not supported headless.
1103
+
1104
+ **Tools.** `tools` lists local function names explicitly. Each tool call runs
1105
+ through the normal functions executor (same lazy reload, scratch dir, spawn
1106
+ tracking, and the function's own `meta.timeout` deadline), and aborting the
1107
+ run aborts in-flight tool calls and their spawned processes. A listed function
1108
+ that is missing fails the run loudly rather than letting a headless automation
1109
+ improvise around it. MCP tools get server-qualified names
1110
+ (`mcp__github__create_issue`; characters outside `[A-Za-z0-9_]` become `_`);
1111
+ MCP servers connect lazily on the first run and fail closed unless marked
1112
+ `optional = true`.
1113
+
1114
+ **Codemode.** `codemode = true` on an `[[agents]]` entry (default `false`) adds a
1115
+ `codemode` tool: the model writes JavaScript that runs in a QuickJS sandbox and
1116
+ calls the agent's other tools (`await tools.<name>(args)`), so it can batch calls
1117
+ and filter large results before they reach the context. MCP servers of such an
1118
+ agent may use `exposure = "codemode"` (or `"deferred"`) to keep their tools out
1119
+ of the model's tool list; scripts find them with `searchTools()`.
1120
+ `models.classify()` runs pi-ai classifier models with the agent's credentials,
1121
+ including this server's decision generators (`buttress/<ggml-decision id>`).
1122
+ Small local models may write poor scripts — prefer it with capable models.
1123
+
1124
+ **Decisions (System One).** `decision_model` gives an agent a typed-decision
1125
+ model: `buttress/<ggml-decision id>` (as `GET /typesafe-compat/v1/models` lists
1126
+ it), or a hosted classifier such as `typesafe/jev-latest` (`TYPESAFE_API_KEY`;
1127
+ the questions and the state are then sent to TypeSafe). Unset means no
1128
+ decisions; it never falls back to the chat model.
1129
+
1130
+ ```toml
1131
+ [[agents]]
1132
+ name = "ops-assistant"
1133
+ model = "buttress/unsloth/Qwen3.5-4B-GGUF"
1134
+ tools = ["host-info"]
1135
+ decision_model = "buttress/BricksDisplay/jevling-e2b-v1-GGUF:jevling-e2b-v1-q8_0-embf16.gguf"
1136
+ decision_assistance = true # opt-in tool prefilter (default false)
1137
+ # decision_timeout_ms = 500 # prefilter deadline; past it the run keeps every tool
1138
+ ```
1139
+
1140
+ - With a `decision_model`, the agent gets a `decision_evaluate` tool. The model
1141
+ calls it itself with a `state` and `choice` / `score` / `bool` questions, and
1142
+ gets calibrated probabilities back. The tool sees only that state, not the
1143
+ conversation. Its answers are evidence for the model, not permission.
1144
+ - With `decision_assistance = true` too, a tool prefilter runs before each run.
1145
+ It asks three yes/no questions about the request: does it need live facts
1146
+ about the system, does it ask for an action (or approve/retry one), and does
1147
+ it point at something not in the conversation. Only when all three are
1148
+ clearly no (each probability < 0.35) does the run go out **without tool
1149
+ definitions**: a shorter prompt for greetings, thanks, explanations or
1150
+ translations. Anything else, including a timeout or an error, keeps every
1151
+ tool. It sends the request, the nearest user/assistant text (≤ 6,000
1152
+ characters) and the tool names; never tool arguments, results or reasoning.
1153
+ The server logs each outcome (`tool prefilter pruned|kept …`).
1154
+ - The prefilter never adds or allows a tool; it only withholds them for one
1155
+ run. Small decision models judge it less reliably: jevling-0.8b pruned some
1156
+ live-fact questions that need a tool, jevling-e2b kept them (about 170 ms per
1157
+ run on a GPU). The first run after start may time out while the model loads.
1158
+
1159
+ **Local devices.** Set `local_devices = true` on an individual `[[agents]]` entry
1160
+ (default `false`) to add just two built-in tools, independently of local functions/MCP:
1161
+
1162
+ - `devices_scan`: the same UDP + HTTP LAN discovery as `bricks devtools scan`,
1163
+ including project previews found relative to the Buttress config directory.
1164
+ - `devtools`: one action-based tool for screenshots (inline images), brick tree/query,
1165
+ tap/key/text input, console capture, JavaScript evaluation, storage overview, and
1166
+ raw CDP requests for other supported operations. Reuses the CLI CDP transport and
1167
+ shared DevTools operations; no CLI subprocess or separate tool per device/action.
1168
+
1169
+ For example, scan with `{}` then call `devtools` with
1170
+ `{ "action": "tree", "address": "192.168.1.42", "port": 19851 }`.
1171
+ After `bricks buttress bind` and a restart, the workspace credential is injected
1172
+ server-side automatically when the device advertises the same workspace and issuer.
1173
+ The token is validated for signature, kind and expiry before use; inbound Buttress
1174
+ JWTs (`k: "ba"`) are never forwarded. Older bindings without the credential, or expired
1175
+ credentials, require rebinding and a restart. The server does not read the CLI login
1176
+ or renew tokens itself. Local previews can still use advertised inspect credentials;
1177
+ explicit `passcode` / `accessToken` arguments remain optional overrides. Calls open/close their own connection;
1178
+ avoid parallel calls or other inspectors on the same device (one CDP client at a time).
1179
+
1180
+ **Security:** enabling this option grants network discovery and device control,
1181
+ including arbitrary JavaScript evaluation on reachable authenticated devices. It is
1182
+ not read-only or a network sandbox. Automatic authentication requires a **trusted LAN**:
1183
+ workspace/issuer hints from device info are not cryptographic peer authentication.
1184
+ The stored token is never inserted into model arguments and is redacted from text
1185
+ results/errors before truncation. Explicit credentials supplied in arguments still
1186
+ enter session transcripts, as do console output and device data; prefer automatic
1187
+ authentication and only grant this to trusted agents/callers. Disable with `local_devices = false` and
1188
+ restart Buttress. Aborting closes active CDP connections; discovery completes its
1189
+ bounded scan before returning the abort result.
1190
+
1191
+ **Sessions.** Each run returns a `sessionId`; pass it back to continue the
1192
+ conversation, or add `fork: true` to branch it into a fresh session. Sessions
1193
+ are JSONL files under `sessions_dir`, scoped by agent name (renaming an agent
1194
+ orphans its sessions), written as the run streams — an aborted or timed-out
1195
+ run still leaves a continuable transcript. Same-session runs queue; different
1196
+ sessions run in parallel. A retention sweep prunes by age and count.
1197
+
1198
+ **From a local function** (the primary surface):
1199
+
1200
+ ```ts
1201
+ export default async ({ service }, context) => {
1202
+ const result = await context.agents.run('ops-assistant', {
1203
+ prompt: `Investigate the '${service}' service and fix it if needed.`,
1204
+ // sessionId, fork, onEvent are also accepted
1205
+ })
1206
+ return { conclusion: result.content, sessionId: result.sessionId }
1207
+ }
1208
+ ```
1209
+
1210
+ `context.agents.run` resolves within the caller's lifetime and deadline
1211
+ (`context.signal` aborts it); long-running agent work belongs in a **daemon**
1212
+ (no deadline) or a function with a raised `meta.timeout`. `context.agents.list()`
1213
+ and `context.agents.sessions(name)` round out the surface. Chained invocations
1214
+ (a tool function calling another agent) are capped by `agents_options.max_depth`.
1215
+
1216
+ **HTTP endpoints** (also what the CLI uses):
1217
+
1218
+ ```
1219
+ GET /agents { agents: names, details: [{ name, model, tools,
1220
+ mcpServers, maxTurns, localDevices, codemode,
1221
+ decisionModel }] }
1222
+ POST /agents/:name/run { prompt, sessionId?, fork? }; ?stream=1 for SSE
1223
+ (a `session` event with the id arrives first,
1224
+ then `agent` events, then `result`/`error`)
1225
+ GET /agents/:name/sessions newest-first summaries (id, timestamps, preview)
1226
+ GET /agents/:name/sessions/:id full transcript
1227
+ POST /agents/:name/sessions/:id/abort abort the active run
1228
+ ```
1229
+
1230
+ `details` has, per agent, its `model` reference (`provider/model-id`), its local function `tools`, the names of its `mcpServers` (never their urls, commands, headers or env), `maxTurns`, `localDevices`, `codemode` and `decisionModel` (`null` when unset).
1231
+
1232
+ The `agent` events are pi agent events made small for the wire. `message_update` carries only the delta (`event: { type, contentIndex, delta }`), `message_start` / `message_end` only the `role`, and `tool_execution_end` carries `toolCallId`, `toolName`, `isError` and `result: { text, images, truncated }`: the text blocks of the tool result joined by newlines (`null` when there are none), cut after 4,000 characters with a trailing `…` (then `truncated` is `true`), and the number of images. Image data is never streamed; the transcript has the full result. `tool_emit` (a local function's `context.emit` progress) carries `toolCallId`, `toolName`, `event` and `data`: the calls of one turn run in parallel, so the call id tells two calls of the same tool apart.
1233
+
1234
+ Auth mirrors the functions surface, not the open inference endpoints: a bound
1235
+ server requires a workspace JWT; an **unbound server rejects remote calls**
1236
+ unless `allow_unauthenticated = true`. The server also writes an ephemeral
1237
+ internal token to `<sessions_dir>/../runtime-token` (mode 0600) at startup —
1238
+ same-host CLIs authenticate with it automatically, bound or not.
1239
+
1240
+ **Interactive CLI**:
1241
+
1242
+ ```sh
1243
+ bricks-buttress agent -c config.toml # list agents
1244
+ bricks-buttress agent ops-assistant -c config.toml # chat (streams text, thinking, tool calls)
1245
+ bricks-buttress agent ops-assistant --sessions -c config.toml
1246
+ bricks-buttress agent ops-assistant --session <id> -c config.toml # continue
1247
+ bricks-buttress agent ops-assistant --fork <id> -c config.toml # fork, then continue the fork
1248
+ ```
1249
+
1250
+ On a real terminal the chat runs a pi-tui interface (markdown answers, dim
1251
+ thinking, tool-call lines, Ctrl+C aborts the running turn); pipes/scripts —
1252
+ or `--plain` — get a line-based renderer with identical semantics.
1253
+
1254
+ Like function files, agent definitions are trusted input: an agent is only as
1255
+ safe as the functions and MCP servers you hand it.
1256
+
1257
+
853
1258
  ## Session State Cache
854
1259
 
855
1260
  The server supports session state caching for ggml-llm generators, which saves KV cache state to disk after completions. This enables:
@@ -14,6 +14,7 @@ needed. On first start the server scaffolds `buttress-functions.d.ts` and a
14
14
  | `simple-rag.ts` | Token chunking, embeddings, `sqlite3` + `sqlite-vec` retrieval, then completion | a chat LLM + a GGML embedding `[[generators]]` entry (below) |
15
15
  | `transcribe-media.ts` | `context.spawn` (ffmpeg), the scratch dir, SSE progress, STT | `ffmpeg` on PATH + an STT `[[generators]]` entry |
16
16
  | `text-to-speech.ts` | TTS (`context.buttress.synthesize`) + downloadable output (`context.fileUrl`) | an `onnx-tts` `[[generators]]` entry |
17
+ | `classify-ticket.ts` | Typed decisions (`context.buttress.decide`): a choice, a score and yes/no flags with calibrated probabilities | a `ggml-decision` `[[generators]]` entry |
17
18
  | `bank-note.ts` | Reading/writing the workspace's remote Data Bank (`context.bank`) | `bricks buttress bank-key` run on this host |
18
19
  | `bank-watch-daemon.ts` | A daemon (`meta.daemon = true`): `context.bank.subscribe`, `context.setInterval`, `context.onEvent` | `bricks buttress bank-key` run on this host |
19
20
  | `_auth.ts` | Custom auth: keep workspace tokens working, add static API keys | see the file header |