@fugood/buttress-server 2.26.0-beta.3 → 2.26.0-beta.31

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 (73) hide show
  1. package/README.md +439 -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-C7j8OJ4D.mjs +112 -0
  27. package/lib/codemode-worker.mjs +3 -0
  28. package/lib/command-To7fgOfj.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 +160 -93
  37. package/lib/mcp-DzknURyz.mjs +5 -0
  38. package/lib/mlx-bridge.py +717 -0
  39. package/lib/net-CKg7oBYC.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/tts-shared.d.ts +5 -0
  53. package/lib/routes/typesafe-compat.check.d.ts +1 -0
  54. package/lib/routes/typesafe-compat.d.ts +30 -0
  55. package/lib/services/ggml-decision.d.ts +91 -0
  56. package/lib/services/ggml-llm.d.ts +34 -1
  57. package/lib/services/index.d.ts +3 -0
  58. package/lib/services/register-tts-result.d.ts +4 -0
  59. package/lib/tui-ClNupsCH.mjs +2 -0
  60. package/lib/types.d.ts +56 -2
  61. package/lib/ui/command.d.ts +2 -0
  62. package/lib/ui/host.d.ts +1 -0
  63. package/lib/ui/window-module.d.ts +6 -0
  64. package/lib/utils/SessionFileManager.d.ts +12 -4
  65. package/lib/utils/config.d.ts +3 -3
  66. package/lib/utils/cors.d.ts +1 -0
  67. package/lib/utils/decision-sharing.d.ts +37 -0
  68. package/lib/utils/downloadStatus.d.ts +57 -0
  69. package/lib/utils/hostFacts.d.ts +13 -0
  70. package/lib/utils/workspaceState.d.ts +8 -0
  71. package/lib/wrapper-3PX6qE3t.mjs +7 -0
  72. package/package.json +17 -9
  73. 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`, `win32-x64` and `win32-arm64`, 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,127 @@ 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
+ Clef, LiquidAI d1, pplx-decider, ...) and answers `choice` / `score` / `noul`
569
+ questions about a state with calibrated probabilities: one forward pass per
570
+ answer, no generated tokens. Clients reach it through the Typed Decision (GGML)
571
+ generator; local 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
+ **Requests** follow the TypeSafe `/v1/systemone` shape:
579
+ `{ state, questions, images?, files? }`.
580
+
581
+ - `state` is text, or a JSON object/array (given to the model as JSON text).
582
+ - `questions` maps an id to a `choice`, `score` or `noul` question. A `noul`
583
+ question takes `criteria: { true?, false? }`; `lfm2-d1-omni` also takes
584
+ `yes` / `no`.
585
+ - `images` carries the media: images, or audio clips for a model whose prompt
586
+ takes audio (`lfm2-d1-omni`). `files` is an alias; both lists are used. A
587
+ client session sends uploaded file names (resolved like STT audio) or data
588
+ URLs (`data:image/...`, `data:audio/...`); local functions send file paths
589
+ or data URLs; the HTTP endpoints take data URLs only. `lfm2-d1-omni` takes
590
+ images or one audio clip, not both.
591
+ - With media, `state` may be `null` (or empty) to ask about the media only.
592
+ Only `lfm2-d1` and `lfm2-d1-omni` accept that; other models reject it. Without
593
+ media a `state` is required.
594
+
595
+ The OpenAI-compatible `POST /oai-compat/v1/decisions` also takes `input_audio`
596
+ content parts (`{ type: 'input_audio', input_audio: { data, format } }`, base64
597
+ `data`, `format` `wav` or `mp3`). An input with only images or audio gets a
598
+ placeholder state (`The attached image.`), since every model type takes one.
599
+
600
+ **Sharing a model with `ggml-llm`** — a model that both generates and decides
601
+ (Jevling, for one) can be configured as both a `ggml-llm` and a
602
+ `ggml-decision`. By default each loads its own context. Set
603
+ `[generators.backend] share_llm_context = true` on the decision entry to run
604
+ its decisions on the LLM's context instead, when the two entries name the same
605
+ weights (same `repo_id`, `revision`, `filename`, `quantization`, `url` and
606
+ `local_path`): the model is then loaded once, with the LLM's config. This
607
+ applies to local functions, the TypeSafe-compatible endpoint and client
608
+ sessions (whose decision runtime options then do not apply).
609
+
610
+ Sharing trades throughput for memory. A decision takes one of the LLM's
611
+ `n_parallel` slots, and its forward passes run between the LLM's batches, so
612
+ concurrent completions stop generating while it runs (in a CPU test, a
613
+ 128-token completion took twice as long beside six decisions; with a separate
614
+ decision context it did not slow down). Results are not affected: the slot's
615
+ sequence and prompt cache are cleared after each decision. Without
616
+ `kv_unified = true` on the LLM a slot holds `n_ctx / n_parallel` tokens, which
617
+ must fit the state plus the questions. The decision loads its own context
618
+ anyway when it sets `rank_pooling` (it needs an embedding context), when its
619
+ projector differs from the LLM's, or when the LLM is embedding or vocab-only.
620
+ The server logs at startup which decision models share.
621
+
622
+ ```toml
623
+ [[generators]]
624
+ type = "ggml-llm"
625
+ [generators.model]
626
+ repo_id = "BricksDisplay/jevling-0.8b-v1-GGUF"
627
+ filename = "jevling-0.8b-v1-q8_0-embf16.gguf"
628
+ [generators.runtime]
629
+ n_ctx = 16384
630
+
631
+ [[generators]]
632
+ type = "ggml-decision"
633
+ [generators.backend]
634
+ share_llm_context = true
635
+ [generators.model]
636
+ repo_id = "BricksDisplay/jevling-0.8b-v1-GGUF"
637
+ filename = "jevling-0.8b-v1-q8_0-embf16.gguf"
638
+ ```
639
+
640
+ **`[generators.backend]`** — same keys as `ggml-tts` (`variant`,
641
+ `variant_preference`, `gpu_memory_fraction`, `cpu_memory_fraction`).
642
+
643
+ **`[generators.model]`** — common keys plus:
644
+
645
+ | Key | Type | Default | Notes |
646
+ | ------------------------- | -------- | -------------------------------- | ------------------------------------------------------------------ |
647
+ | `preferred_quantizations` | string[] | `["q8_0", "q4_k_m", <no-quant>]` | Default fallback chain |
648
+ | `mmproj_repo_id` | string | falls back to `repo_id` | Projector repo, for models whose prompt takes images or audio |
649
+ | `mmproj_filename` | string | auto (`*mmproj*`) | Pin a specific projector artifact |
650
+ | `mmproj_url` | string | — | Direct projector URL (skips manifest lookup) |
651
+ | `mmproj_revision` | string | falls back to `revision` | |
652
+ | `mmproj_local_path` | string | — | Requires `allow_local_file` |
653
+ | `rank_pooling` | boolean | `false` | `system_one` models with a classification head (`rank_head` readout) |
654
+ | `use_gpu` | boolean | `true` | Force-disable GPU even when available |
655
+ | `n_gpu_layers` | number | `99` | Forced to `0` when the GPU is unused |
656
+
657
+ Setting any `mmproj_*` key loads a projector; requests can then pass `images`
658
+ (images or audio clips, as file paths or data URLs). The projector is ignored,
659
+ with a warning, when the model's prompt has no place for images or audio.
660
+
661
+ **Runtime extras** — under `[runtime]` for ggml-decision:
662
+
663
+ | Key | Type | Default | Notes |
664
+ | -------------------------- | ------ | --------------- | ---------------------------------------- |
665
+ | `n_ctx` | number | `4096` | Must hold the state plus every question |
666
+ | `n_batch` / `n_ubatch` | number | `2048` / `512` | |
667
+ | `max_threads` | number | auto | Caps the llama.cpp thread count |
668
+ | `flash_attn_type` | string | `auto` | `auto` / `on` / `off` |
669
+ | `context_release_delay_ms` | number | `10000` | `0` releases the context right away |
670
+
671
+ **Example**
672
+
673
+ ```toml
674
+ [[generators]]
675
+ type = "ggml-decision"
676
+ [generators.model]
677
+ repo_id = "ggml-org/Julia-1-GGUF"
678
+ filename = "Julia-1-Q8_0.gguf"
679
+ download = true
680
+ [generators.runtime]
681
+ n_ctx = 4096
682
+ ```
683
+
684
+ ---
685
+
470
686
  ### `mlx-llm` (Apple Silicon, Python `mlx-lm` / `mlx-vlm` bridge)
471
687
 
472
688
  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 +791,12 @@ Buttress server for remote inference with GGML backends.
575
791
 
576
792
  Usage:
577
793
  bricks-buttress [options]
794
+ bricks-buttress ui [-c] <config.toml>
578
795
  bricks-buttress update [--check] [-y] [--channel <release|beta>]
579
796
 
580
797
  Commands:
798
+ ui Open the settings, status and chat window for a
799
+ config file (see `bricks-buttress ui --help`)
581
800
  update Update bricks-buttress to the latest version.
582
801
  Standalone binary installs re-run the CDN
583
802
  installer (refreshing the native-module
@@ -612,7 +831,7 @@ Examples:
612
831
 
613
832
  ## Compatibility Endpoints (Experimental)
614
833
 
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:
834
+ 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
835
 
617
836
  ```toml
618
837
  [openai_compat]
@@ -622,14 +841,19 @@ enabled = true
622
841
  [anthropic_messages]
623
842
  enabled = true
624
843
  # cors_allowed_origins = ["http://localhost:3000"]
844
+
845
+ [typesafe_compat]
846
+ enabled = true
625
847
  ```
626
848
 
627
849
  | Endpoint | Config flag |
628
850
  | --------------------- | ------------------------------------ |
629
851
  | `/oai-compat/v1/*` | `[openai_compat] enabled = true` |
630
852
  | `/anthropic-messages` | `[anthropic_messages] enabled = true` |
853
+ | `/typesafe-compat/v1/*` | `[typesafe_compat] enabled = true` |
631
854
 
632
- Completion requests may select only LLM models declared in `[[generators]]`. Use each
855
+ Completion requests may select only LLM models declared in `[[generators]]`, and decision
856
+ requests only `ggml-decision` generators. Use each
633
857
  endpoint's `GET /v1/models` response for valid model IDs; unknown IDs are rejected before the
634
858
  server starts or downloads a generator.
635
859
 
@@ -693,6 +917,7 @@ export default async function ({ path }: { path: string }, context: ButtressFunc
693
917
  | `buttress.detokenize({ model?, tokens })` | Convert token ids back into text with the same model. |
694
918
  | `buttress.transcribe({ model?, filePath \| audioData, options? })` | Transcribe audio with this server's STT generator. |
695
919
  | `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 }`. |
920
+ | `buttress.decide({ model?, state, questions, images?, files? })` | 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` (alias `files`) are images or audio clips, as local paths or data URLs; with media, `state` may be `null` for `lfm2-d1` / `lfm2-d1-omni`. |
696
921
  | `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
922
  | `daemons.emit(name, event, data?)` / `daemons.list()` | Signal a running **daemon function**'s `onEvent` handlers (see below) / list running daemon names. |
698
923
  | `emit(event, data)` | Progress event; delivered to SSE callers, ignored otherwise. |
@@ -850,6 +1075,210 @@ The handler receives `{ method, path, name?, headers, query, token, workspaceAut
850
1075
 
851
1076
  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
1077
 
1078
+ ## Agents (Experimental)
1079
+
1080
+ Buttress can host **agents**: pi-based LLM loops that run inside the server
1081
+ process, use **local functions** (and MCP servers) as their tools, and keep
1082
+ **config-scoped sessions** on disk. The primary consumer is automation — a
1083
+ local function or daemon calls `context.agents.run(...)` to add multi-step
1084
+ reasoning to a server-side workflow; an interactive CLI exists for driving and
1085
+ inspecting the same agents.
1086
+
1087
+ ```toml
1088
+ [[agents]]
1089
+ name = "ops-assistant" # unique; the session scope key
1090
+ model = "buttress/ggml-org/gpt-oss-20b-GGUF" # split on the FIRST slash: provider/model-id
1091
+ # model = "anthropic/claude-sonnet-5" # any pi-supported provider; API key from env
1092
+ system_prompt_file = "./prompts/ops.md" # or inline: system_prompt = "..."
1093
+ tools = ["get_server_status", "restart_service"] # local function names (explicit; no wildcard)
1094
+ local_devices = false # opt in to LAN scan + device DevTools tools
1095
+ max_turns = 30 # assistant↔tool round-trips per run (default 30)
1096
+ # codemode = true # opt in: a sandboxed JS tool that calls the other tools
1097
+ # max_tokens_per_run = 200000 # per-run token budget; unset = unlimited
1098
+ # temperature = 0.2 # unrecognized keys pass through to generation
1099
+
1100
+ [agents.mcp_servers.github] # optional MCP servers (StreamableHTTP or stdio)
1101
+ url = "https://api.githubcopilot.com/mcp/"
1102
+ # headers = { Authorization = "Bearer ..." }
1103
+ # optional = true # continue without this server if it won't connect
1104
+ # timeout = 60 # per-request seconds (progress renews it)
1105
+ # exposure = "direct" # "hidden": unreachable; "codemode": scripts only (needs codemode = true)
1106
+ # tool_exposure = { "delete_*" = "hidden" } # per tool, by exact name or * pattern
1107
+
1108
+ [agents_options] # optional; defaults shown
1109
+ # sessions_dir = "./.buttress-agent/sessions" # relative to this config file
1110
+ # session_max_age = "30d" # retention sweep; 0 disables
1111
+ # session_max_count = 500 # per agent; 0 disables
1112
+ # max_depth = 2 # function → agent → function → agent chain cap
1113
+ # allow_unauthenticated = false # serve /agents on an UNBOUND server (see below)
1114
+ ```
1115
+
1116
+ **Models.** `buttress/<repo_id>` targets a configured `[[generators]]` LLM
1117
+ (ggml/mlx) and is validated at startup; traffic flows through an in-process
1118
+ OpenAI-compat loopback (no socket). The loopback calls the server's own
1119
+ `/oai-compat/v1` routes, which exist only with `[openai_compat] enabled = true`
1120
+ (or `ENABLE_OPENAI_COMPAT_ENDPOINT=1`): an agent with a `buttress/…` model
1121
+ needs it, or every run fails with `404 NOT_FOUND`. Any other provider prefix is a pi
1122
+ built-in provider (`anthropic/…`, `openai/…`, `google/…`, …) authenticated by
1123
+ its usual environment variable (`ANTHROPIC_API_KEY`, …) — set it via `[env]`
1124
+ or the process environment. OAuth-based logins are not supported headless.
1125
+
1126
+ **Tools.** `tools` lists local function names explicitly. Each tool call runs
1127
+ through the normal functions executor (same lazy reload, scratch dir, spawn
1128
+ tracking, and the function's own `meta.timeout` deadline), and aborting the
1129
+ run aborts in-flight tool calls and their spawned processes. A listed function
1130
+ that is missing fails the run loudly rather than letting a headless automation
1131
+ improvise around it. MCP tools get server-qualified names
1132
+ (`mcp__github__create_issue`; characters outside `[A-Za-z0-9_]` become `_`);
1133
+ MCP servers connect lazily on the first run and fail closed unless marked
1134
+ `optional = true`.
1135
+
1136
+ **Codemode.** `codemode = true` on an `[[agents]]` entry (default `false`) adds a
1137
+ `codemode` tool: the model writes JavaScript that runs in a QuickJS sandbox and
1138
+ calls the agent's other tools (`await tools.<name>(args)`), so it can batch calls
1139
+ and filter large results before they reach the context. MCP servers of such an
1140
+ agent may use `exposure = "codemode"` (or `"deferred"`) to keep their tools out
1141
+ of the model's tool list; scripts find them with `searchTools()`.
1142
+ `models.classify()` runs pi-ai classifier models with the agent's credentials,
1143
+ including this server's decision generators (`buttress/<ggml-decision id>`).
1144
+ Small local models may write poor scripts — prefer it with capable models.
1145
+
1146
+ **Decisions (System One).** `decision_model` gives an agent a typed-decision
1147
+ model: `buttress/<ggml-decision id>` (as `GET /typesafe-compat/v1/models` lists
1148
+ it), or a hosted classifier such as `typesafe/jev-latest` (`TYPESAFE_API_KEY`;
1149
+ the questions and the state are then sent to TypeSafe). Unset means no
1150
+ decisions; it never falls back to the chat model.
1151
+
1152
+ ```toml
1153
+ [[agents]]
1154
+ name = "ops-assistant"
1155
+ model = "buttress/unsloth/Qwen3.5-4B-GGUF"
1156
+ tools = ["host-info"]
1157
+ decision_model = "buttress/BricksDisplay/jevling-e2b-v1-GGUF:jevling-e2b-v1-q8_0-embf16.gguf"
1158
+ decision_assistance = true # opt-in tool prefilter (default false)
1159
+ # decision_timeout_ms = 500 # prefilter deadline; past it the run keeps every tool
1160
+ ```
1161
+
1162
+ - With a `decision_model`, the agent gets a `decision_evaluate` tool. The model
1163
+ calls it itself with a `state` and `choice` / `score` / `bool` questions, and
1164
+ gets calibrated probabilities back. The tool sees only that state, not the
1165
+ conversation. Its answers are evidence for the model, not permission.
1166
+ - With `decision_assistance = true` too, a tool prefilter runs before each run.
1167
+ It asks three yes/no questions about the request: does it need live facts
1168
+ about the system, does it ask for an action (or approve/retry one), and does
1169
+ it point at something not in the conversation. Only when all three are
1170
+ clearly no (each probability < 0.35) does the run go out **without tool
1171
+ definitions**: a shorter prompt for greetings, thanks, explanations or
1172
+ translations. Anything else, including a timeout or an error, keeps every
1173
+ tool. It sends the request, the nearest user/assistant text (≤ 6,000
1174
+ characters) and the tool names; never tool arguments, results or reasoning.
1175
+ The server logs each outcome (`tool prefilter pruned|kept …`).
1176
+ - The prefilter never adds or allows a tool; it only withholds them for one
1177
+ run. Small decision models judge it less reliably: jevling-0.8b pruned some
1178
+ live-fact questions that need a tool, jevling-e2b kept them (about 170 ms per
1179
+ run on a GPU). The first run after start may time out while the model loads.
1180
+
1181
+ **Local devices.** Set `local_devices = true` on an individual `[[agents]]` entry
1182
+ (default `false`) to add just two built-in tools, independently of local functions/MCP:
1183
+
1184
+ - `devices_scan`: the same UDP + HTTP LAN discovery as `bricks devtools scan`,
1185
+ including project previews found relative to the Buttress config directory.
1186
+ - `devtools`: one action-based tool for screenshots (inline images), brick tree/query,
1187
+ tap/key/text input, a press by brick id, the open app modals and one operation on the topmost
1188
+ one (app version >= 2.26; a generator confirm dialog is only dismissed), console capture,
1189
+ JavaScript evaluation, storage overview, and raw CDP requests for other supported operations.
1190
+ Reuses the CLI CDP transport and shared DevTools operations; no CLI subprocess or separate tool
1191
+ per device/action.
1192
+
1193
+ For example, scan with `{}` then call `devtools` with
1194
+ `{ "action": "tree", "address": "192.168.1.42", "port": 19851 }`.
1195
+ After `bricks buttress bind` and a restart, the workspace credential is injected
1196
+ server-side automatically when the device advertises the same workspace and issuer.
1197
+ The token is validated for signature, kind and expiry before use; inbound Buttress
1198
+ JWTs (`k: "ba"`) are never forwarded. Older bindings without the credential, or expired
1199
+ credentials, require rebinding and a restart. The server does not read the CLI login
1200
+ or renew tokens itself. Local previews can still use advertised inspect credentials;
1201
+ explicit `passcode` / `accessToken` arguments remain optional overrides. Calls open/close their own connection;
1202
+ avoid parallel calls or other inspectors on the same device (one CDP client at a time).
1203
+
1204
+ **Security:** enabling this option grants network discovery and device control,
1205
+ including arbitrary JavaScript evaluation on reachable authenticated devices. It is
1206
+ not read-only or a network sandbox. Automatic authentication requires a **trusted LAN**:
1207
+ workspace/issuer hints from device info are not cryptographic peer authentication.
1208
+ The stored token is never inserted into model arguments and is redacted from text
1209
+ results/errors before truncation. Explicit credentials supplied in arguments still
1210
+ enter session transcripts, as do console output and device data; prefer automatic
1211
+ authentication and only grant this to trusted agents/callers. Disable with `local_devices = false` and
1212
+ restart Buttress. Aborting closes active CDP connections; discovery completes its
1213
+ bounded scan before returning the abort result.
1214
+
1215
+ **Sessions.** Each run returns a `sessionId`; pass it back to continue the
1216
+ conversation, or add `fork: true` to branch it into a fresh session. Sessions
1217
+ are JSONL files under `sessions_dir`, scoped by agent name (renaming an agent
1218
+ orphans its sessions), written as the run streams — an aborted or timed-out
1219
+ run still leaves a continuable transcript. Same-session runs queue; different
1220
+ sessions run in parallel. A retention sweep prunes by age and count.
1221
+
1222
+ **From a local function** (the primary surface):
1223
+
1224
+ ```ts
1225
+ export default async ({ service }, context) => {
1226
+ const result = await context.agents.run('ops-assistant', {
1227
+ prompt: `Investigate the '${service}' service and fix it if needed.`,
1228
+ // sessionId, fork, onEvent are also accepted
1229
+ })
1230
+ return { conclusion: result.content, sessionId: result.sessionId }
1231
+ }
1232
+ ```
1233
+
1234
+ `context.agents.run` resolves within the caller's lifetime and deadline
1235
+ (`context.signal` aborts it); long-running agent work belongs in a **daemon**
1236
+ (no deadline) or a function with a raised `meta.timeout`. `context.agents.list()`
1237
+ and `context.agents.sessions(name)` round out the surface. Chained invocations
1238
+ (a tool function calling another agent) are capped by `agents_options.max_depth`.
1239
+
1240
+ **HTTP endpoints** (also what the CLI uses):
1241
+
1242
+ ```
1243
+ GET /agents { agents: names, details: [{ name, model, tools,
1244
+ mcpServers, maxTurns, localDevices, codemode,
1245
+ decisionModel }] }
1246
+ POST /agents/:name/run { prompt, sessionId?, fork? }; ?stream=1 for SSE
1247
+ (a `session` event with the id arrives first,
1248
+ then `agent` events, then `result`/`error`)
1249
+ GET /agents/:name/sessions newest-first summaries (id, timestamps, preview)
1250
+ GET /agents/:name/sessions/:id full transcript
1251
+ POST /agents/:name/sessions/:id/abort abort the active run
1252
+ ```
1253
+
1254
+ `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).
1255
+
1256
+ 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.
1257
+
1258
+ Auth mirrors the functions surface, not the open inference endpoints: a bound
1259
+ server requires a workspace JWT; an **unbound server rejects remote calls**
1260
+ unless `allow_unauthenticated = true`. The server also writes an ephemeral
1261
+ internal token to `<sessions_dir>/../runtime-token` (mode 0600) at startup —
1262
+ same-host CLIs authenticate with it automatically, bound or not.
1263
+
1264
+ **Interactive CLI**:
1265
+
1266
+ ```sh
1267
+ bricks-buttress agent -c config.toml # list agents
1268
+ bricks-buttress agent ops-assistant -c config.toml # chat (streams text, thinking, tool calls)
1269
+ bricks-buttress agent ops-assistant --sessions -c config.toml
1270
+ bricks-buttress agent ops-assistant --session <id> -c config.toml # continue
1271
+ bricks-buttress agent ops-assistant --fork <id> -c config.toml # fork, then continue the fork
1272
+ ```
1273
+
1274
+ On a real terminal the chat runs a pi-tui interface (markdown answers, dim
1275
+ thinking, tool-call lines, Ctrl+C aborts the running turn); pipes/scripts —
1276
+ or `--plain` — get a line-based renderer with identical semantics.
1277
+
1278
+ Like function files, agent definitions are trusted input: an agent is only as
1279
+ safe as the functions and MCP servers you hand it.
1280
+
1281
+
853
1282
  ## Session State Cache
854
1283
 
855
1284
  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 |