@fugood/buttress-server 2.26.0-beta.19 → 2.26.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.
@@ -243,6 +305,7 @@ Every generator entry has a `type`, an optional `[generators.backend]` table, an
243
305
  ```toml
244
306
  [[generators]]
245
307
  type = "ggml-llm" # or "ggml-stt" / "ggml-tts" / "ggml-decision" / "mlx-llm"
308
+ # enabled = false # keep the entry but skip it at startup
246
309
 
247
310
  [generators.backend]
248
311
  # (see per-type sections below)
@@ -252,6 +315,11 @@ repo_id = "..."
252
315
  # (see per-type sections below)
253
316
  ```
254
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
+
255
323
  #### Common `[generators.model]` keys
256
324
 
257
325
  Shared by **all** generator types:
@@ -499,8 +567,7 @@ vocoder_batch_size = 4096
499
567
  Loads a typed decision model (System One — Julia-1, Laya, Kev, Jevling, lev,
500
568
  ...) and answers `choice` / `score` / `noul` questions about a state with
501
569
  calibrated probabilities: one forward pass per answer, no generated tokens.
502
- Requests and results follow the TypeSafe `/v1/systemone` API that llama-server
503
- serves. Clients reach it through the Typed Decision (GGML) generator; local
570
+ Clients reach it through the Typed Decision (GGML) generator; local
504
571
  functions through `context.buttress.decide`.
505
572
 
506
573
  The model is checked when its context loads: a GGUF with no decision metadata
@@ -702,9 +769,12 @@ Buttress server for remote inference with GGML backends.
702
769
 
703
770
  Usage:
704
771
  bricks-buttress [options]
772
+ bricks-buttress ui [-c] <config.toml>
705
773
  bricks-buttress update [--check] [-y] [--channel <release|beta>]
706
774
 
707
775
  Commands:
776
+ ui Open the settings, status and chat window for a
777
+ config file (see `bricks-buttress ui --help`)
708
778
  update Update bricks-buttress to the latest version.
709
779
  Standalone binary installs re-run the CDN
710
780
  installer (refreshing the native-module
@@ -739,7 +809,7 @@ Examples:
739
809
 
740
810
  ## Compatibility Endpoints (Experimental)
741
811
 
742
- 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:
743
813
 
744
814
  ```toml
745
815
  [openai_compat]
@@ -749,6 +819,9 @@ enabled = true
749
819
  [anthropic_messages]
750
820
  enabled = true
751
821
  # cors_allowed_origins = ["http://localhost:3000"]
822
+
823
+ [typesafe_compat]
824
+ enabled = true
752
825
  ```
753
826
 
754
827
  | Endpoint | Config flag |
@@ -757,38 +830,11 @@ enabled = true
757
830
  | `/anthropic-messages` | `[anthropic_messages] enabled = true` |
758
831
  | `/typesafe-compat/v1/*` | `[typesafe_compat] enabled = true` |
759
832
 
760
- 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
761
835
  endpoint's `GET /v1/models` response for valid model IDs; unknown IDs are rejected before the
762
836
  server starts or downloads a generator.
763
837
 
764
- ### TypeSafe-compatible System One API
765
-
766
- `POST /typesafe-compat/v1/systemone` answers typed decisions in the shape of
767
- the hosted [TypeSafe API](https://docs.typesafe.ai/api) with this server's
768
- `ggml-decision` generators. A TypeSafe SDK reaches it with
769
- `TYPESAFE_BASE_URL=http://<host>:<port>/typesafe-compat`; models are listed at
770
- `GET /typesafe-compat/v1/models`.
771
-
772
- ```toml
773
- [typesafe_compat]
774
- enabled = true
775
- # cors_allowed_origins = ["http://localhost:3000"]
776
- # SDKs send `model = "jev-latest"` unless told otherwise; map such names to a
777
- # configured decision generator id (as `GET /typesafe-compat/v1/models` lists it)
778
- [typesafe_compat.models]
779
- "jev-latest" = "BricksDisplay/jevling-0.8b-v1-GGUF:jevling-0.8b-v1-q8_0-embf16.gguf"
780
- ```
781
-
782
- - `model` must be a configured decision generator id or a mapped name. An
783
- unknown model gets a 422, never another model: a different model answers
784
- with different calibration. The response `model` is the resolved generator id.
785
- - Auth is the same as the other endpoints: a bound server takes the
786
- workspace-access JWT as the Bearer key; an unbound server is open.
787
- - Validation errors are 422 with a FastAPI-style `detail` list (`loc`, `msg`),
788
- as the SDKs print them; other failures are `{ "error": { "message" } }`.
789
- - `images` (data URLs only) is a Buttress extension for models with a
790
- projector. The hosted API is text-only.
791
-
792
838
  ## Local Functions (Experimental)
793
839
 
794
840
  Local functions are `.ts`/`.js` files you drop into a directory on the server. Each one becomes an MCP tool **and** an HTTP endpoint, so an agent (or any HTTP client) can run server-side work — shelling out to `ffmpeg`, calling this server's own LLM/STT generators — without you writing a service.
@@ -849,7 +895,7 @@ export default async function ({ path }: { path: string }, context: ButtressFunc
849
895
  | `buttress.detokenize({ model?, tokens })` | Convert token ids back into text with the same model. |
850
896
  | `buttress.transcribe({ model?, filePath \| audioData, options? })` | Transcribe audio with this server's STT generator. |
851
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 }`. |
852
- | `buttress.decide({ model?, state, questions, images? })` | Answer typed `choice` / `score` / `noul` questions about `state` with this server's `ggml-decision` generator (TypeSafe `/v1/systemone` shape) → `{ model, answers, usage }`. `answers[id]` is typed from its question. `images` are local paths or data URLs. |
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. |
853
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. |
854
900
  | `daemons.emit(name, event, data?)` / `daemons.list()` | Signal a running **daemon function**'s `onEvent` handlers (see below) / list running daemon names. |
855
901
  | `emit(event, data)` | Progress event; delivered to SSE callers, ignored otherwise. |
@@ -1047,8 +1093,10 @@ url = "https://api.githubcopilot.com/mcp/"
1047
1093
 
1048
1094
  **Models.** `buttress/<repo_id>` targets a configured `[[generators]]` LLM
1049
1095
  (ggml/mlx) and is validated at startup; traffic flows through an in-process
1050
- OpenAI-compat loopback (no socket, no extra config — `[openai_compat] enabled`
1051
- still only governs the external HTTP route). Any other provider prefix is a pi
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
1052
1100
  built-in provider (`anthropic/…`, `openai/…`, `google/…`, …) authenticated by
1053
1101
  its usual environment variable (`ANTHROPIC_API_KEY`, …) — set it via `[env]`
1054
1102
  or the process environment. OAuth-based logins are not supported headless.
@@ -1168,7 +1216,9 @@ and `context.agents.sessions(name)` round out the surface. Chained invocations
1168
1216
  **HTTP endpoints** (also what the CLI uses):
1169
1217
 
1170
1218
  ```
1171
- GET /agents configured agent names
1219
+ GET /agents { agents: names, details: [{ name, model, tools,
1220
+ mcpServers, maxTurns, localDevices, codemode,
1221
+ decisionModel }] }
1172
1222
  POST /agents/:name/run { prompt, sessionId?, fork? }; ?stream=1 for SSE
1173
1223
  (a `session` event with the id arrives first,
1174
1224
  then `agent` events, then `result`/`error`)
@@ -1177,6 +1227,10 @@ GET /agents/:name/sessions/:id full transcript
1177
1227
  POST /agents/:name/sessions/:id/abort abort the active run
1178
1228
  ```
1179
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
+
1180
1234
  Auth mirrors the functions surface, not the open inference endpoints: a bound
1181
1235
  server requires a workspace JWT; an **unbound server rejects remote calls**
1182
1236
  unless `allow_unauthenticated = true`. The server also writes an ephemeral
@@ -35,8 +35,6 @@ enabled = true
35
35
  # [typesafe_compat]
36
36
  # enabled = true
37
37
  # cors_allowed_origins = ["http://localhost:3000"]
38
- # [typesafe_compat.models]
39
- # "jev-latest" = "ggml-org/Julia-1-GGUF:Julia-1-Q8_0.gguf"
40
38
 
41
39
  # Local functions (EXPERIMENTAL): .ts/.js files exposed as MCP tools and HTTP
42
40
  # endpoints. Interfaces may change between releases.
@@ -140,6 +138,7 @@ n_ctx = 12800 # Max: 131072
140
138
 
141
139
  [[generators]]
142
140
  type = "ggml-llm"
141
+ # enabled = false # keep this entry in the file but skip it at startup (default: true)
143
142
  [generators.backend]
144
143
  variant_preference = ["cuda", "vulkan", "snapdragon", "default"]
145
144
  gpu_memory_fraction = 0.95
@@ -13,8 +13,11 @@ export type FunctionToolContext = {
13
13
  runtime: FunctionRuntime;
14
14
  /** Agent-invocation depth for calls made BY this run's tools. */
15
15
  depth: number;
16
- /** Forwards `context.emit` progress as agent events. */
17
- onToolEmit?: (toolName: string, event: string, data?: unknown) => void;
16
+ /**
17
+ * Forwards `context.emit` progress as agent events. `toolCallId` tells calls of
18
+ * one tool apart: pi runs the calls of a turn in parallel.
19
+ */
20
+ onToolEmit?: (toolName: string, event: string, data: unknown, toolCallId: string) => void;
18
21
  };
19
22
  export type AgentFunctionTool = {
20
23
  name: string;
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{a as e,i as t,n,r,t as i}from"./client-BVyIuSyr.mjs";import a from"node:readline";const o=`\x1B[2m`,s=`\x1B[33m`,c=`\x1B[31m`,l=`\x1B[0m`,u=e=>{let t={name:null,configArg:null,url:null,token:null,sessionId:null,fork:!1,listSessions:!1,plain:!1};for(let n=0;n<e.length;n+=1){let r=e[n];r===`-h`||r===`--help`?(console.log(`
2
+ import{a as e,i as t,n,r,t as i}from"./client-qqI-HCOj.mjs";import a from"node:readline";const o=`\x1B[2m`,s=`\x1B[33m`,c=`\x1B[31m`,l=`\x1B[0m`,u=e=>{let t={name:null,configArg:null,url:null,token:null,sessionId:null,fork:!1,listSessions:!1,plain:!1};for(let n=0;n<e.length;n+=1){let r=e[n];r===`-h`||r===`--help`?(console.log(`
3
3
  bricks-buttress agent — chat with a configured agent on a running server
4
4
 
5
5
  Usage:
@@ -19,4 +19,4 @@ In the chat: /exit quits, /new starts a fresh session, Ctrl+C aborts the
19
19
  current run (a second Ctrl+C quits).
20
20
  `),process.exit(0)):r===`-c`||r===`--config`?t.configArg=e[++n]??null:r===`--url`?t.url=e[++n]??null:r===`--token`?t.token=e[++n]??null:r===`--session`?t.sessionId=e[++n]??null:r===`--fork`?(t.sessionId=e[++n]??null,t.fork=!0):r===`--sessions`?t.listSessions=!0:r===`--plain`?t.plain=!0:!r.startsWith(`-`)&&!t.name?t.name=r:(console.error(`Unknown argument: ${r}`),process.exit(1))}return t},d=e=>{console.error(`${c}${e}${l}`),process.exit(1)},f=async(t,n,r,i,a,u)=>{let d=i,f=!1,p=!1,m=()=>{f&&=(process.stdout.write(`${l}\n`),!1)},h=await e(t,n,{prompt:r,sessionId:i??void 0,fork:a},{signal:u,onFrame:({event:e,payload:t})=>{if(e===`session`){t.sessionId&&t.sessionId!==d&&(d=t.sessionId,i||console.log(`${o}session ${d}${l}`));return}if(e!==`agent`)return;let n=t?.event;if(t?.type===`message_update`&&n)n.type===`thinking_delta`?(f||=(process.stdout.write(`${o}`),!0),process.stdout.write(n.delta??``),p=!0):n.type===`text_delta`&&(m(),process.stdout.write(n.delta??``),p=!0);else if(t?.type===`tool_execution_start`){m(),p&&process.stdout.write(`
21
21
  `);let e=JSON.stringify(t.args??{});console.log(`${s}⚙ ${t.toolName}${l}${o}(${e.length>160?`${e.slice(0,160)}…`:e})${l}`),p=!1}else t?.type===`tool_execution_end`?console.log(t.isError?`${c}✗ ${t.toolName} failed${l}`:`${o}✓ ${t.toolName}${l}`):t?.type===`tool_emit`&&console.log(`${o} ${t.toolName} → ${t.event}${l}`)}});if(m(),h.kind===`result`){let e=h.result;d=e.sessionId??d;let t=e.usage||{};return process.stdout.write(`
22
- `),console.log(`${o}— ${e.stopReason} · ${t.totalTurns??`?`} turn(s) · ${t.input??0} in / ${t.output??0} out · session ${d}${l}`),{sessionId:d,ok:!0}}return h.kind===`aborted`?(console.log(`\n${s}(aborted)${l}`),{sessionId:h.sessionId??d,ok:!1}):h.kind===`error`?(console.error(`\n${c}Run failed: ${h.message}${l}`),{sessionId:h.sessionId??d,ok:!1}):(console.error(`\n${c}Connection lost mid-run.${l}`),{sessionId:h.sessionId??d,ok:!1})},p=async(e,t,n,r)=>{let s=n,u=r;console.log(`${t}${l} @ ${e.baseUrl}`+(s?` ${o}(${u?`forking`:`continuing`} ${s})${l}`:``)),console.log(`${o}/exit to quit, /new for a fresh session, Ctrl+C aborts a running turn${l}`);let d=a.createInterface({input:process.stdin,output:process.stdout}),p=!1;d.on(`close`,()=>{p=!0});let m=e=>new Promise(t=>{if(p){t(null);return}d.question(e,t),d.once(`close`,()=>t(null))}),h=null;for(d.on(`SIGINT`,()=>{h?(h.abort(),h=null):(d.close(),process.exit(0))});;){let n=await m(`> ${l}`);if(n==null)break;let r=n.trim();if(!r)continue;if(r===`/exit`||r===`/quit`)break;if(r===`/new`){s=null,u=!1,console.log(`${o}Started a fresh session.${l}`);continue}h=new AbortController;let a=s;try{let n=await f(e,t,r,s,u,h.signal);n.sessionId&&(s=n.sessionId),i(a,s)&&(u=!1)}catch(e){console.error(`${c}${e?.message||e}${l}`)}finally{h=null}}d.close()},m=async e=>{let i=u(e),{config:a,agentsConfig:s}=n(i.configArg),c=a.server.port||2080,f=(i.url||`http://127.0.0.1:${c}`).replace(/\/$/,``),m={baseUrl:f,token:i.token||(s?r(s.runtimeTokenFile):null)||process.env.BUTTRESS_AGENT_TOKEN||null},h=await t(m,`/agents`).catch(e=>d(`Cannot reach ${f}: ${e?.message||e} (is the server running?)`));if(!h.ok){let e=await h.text().catch(()=>``);d(`Server rejected the request (${h.status}): ${e.slice(0,300)}`)}let{agents:g}=await h.json();if(!i.name){console.log(`Configured agents on ${f}:`);for(let e of g)console.log(`- ${e}`);g.length===0&&console.log(`(none — add [[agents]] to the server config)`);return}if(g.includes(i.name)||d(`Unknown agent "${i.name}" (configured: ${g.join(`, `)||`none`})`),i.listSessions){let{sessions:e}=await(await t(m,`/agents/${encodeURIComponent(i.name)}/sessions`)).json();if(!e?.length){console.log(`No sessions yet.`);return}for(let t of e)console.log(`${t.sessionId} ${o}${t.updatedAt}${l}${t.parentSession?` ${o}(fork of ${t.parentSession})${l}`:``}`),t.preview&&console.log(` ${o}${t.preview}${l}`);return}if(process.stdin.isTTY&&process.stdout.isTTY&&!i.plain){let{runAgentTui:e}=await import(`./tui-Bxeb1d7E.mjs`);await e({connection:m,agentName:i.name,sessionId:i.sessionId,fork:i.fork});return}await p(m,i.name,i.sessionId,i.fork)};export{m as runAgentCommand};
22
+ `),console.log(`${o}— ${e.stopReason} · ${t.totalTurns??`?`} turn(s) · ${t.input??0} in / ${t.output??0} out · session ${d}${l}`),{sessionId:d,ok:!0}}return h.kind===`aborted`?(console.log(`\n${s}(aborted)${l}`),{sessionId:h.sessionId??d,ok:!1}):h.kind===`error`?(console.error(`\n${c}Run failed: ${h.message}${l}`),{sessionId:h.sessionId??d,ok:!1}):(console.error(`\n${c}Connection lost mid-run.${l}`),{sessionId:h.sessionId??d,ok:!1})},p=async(e,t,n,r)=>{let s=n,u=r;console.log(`${t}${l} @ ${e.baseUrl}`+(s?` ${o}(${u?`forking`:`continuing`} ${s})${l}`:``)),console.log(`${o}/exit to quit, /new for a fresh session, Ctrl+C aborts a running turn${l}`);let d=a.createInterface({input:process.stdin,output:process.stdout}),p=!1;d.on(`close`,()=>{p=!0});let m=e=>new Promise(t=>{if(p){t(null);return}d.question(e,t),d.once(`close`,()=>t(null))}),h=null;for(d.on(`SIGINT`,()=>{h?(h.abort(),h=null):(d.close(),process.exit(0))});;){let n=await m(`> ${l}`);if(n==null)break;let r=n.trim();if(!r)continue;if(r===`/exit`||r===`/quit`)break;if(r===`/new`){s=null,u=!1,console.log(`${o}Started a fresh session.${l}`);continue}h=new AbortController;let a=s;try{let n=await f(e,t,r,s,u,h.signal);n.sessionId&&(s=n.sessionId),i(a,s)&&(u=!1)}catch(e){console.error(`${c}${e?.message||e}${l}`)}finally{h=null}}d.close()},m=async e=>{let i=u(e),{config:a,agentsConfig:s}=n(i.configArg),c=a.server.port||2080,f=(i.url||`http://127.0.0.1:${c}`).replace(/\/$/,``),m={baseUrl:f,token:i.token||(s?r(s.runtimeTokenFile):null)||process.env.BUTTRESS_AGENT_TOKEN||null},h=await t(m,`/agents`).catch(e=>d(`Cannot reach ${f}: ${e?.message||e} (is the server running?)`));if(!h.ok){let e=await h.text().catch(()=>``);d(`Server rejected the request (${h.status}): ${e.slice(0,300)}`)}let{agents:g}=await h.json();if(!i.name){console.log(`Configured agents on ${f}:`);for(let e of g)console.log(`- ${e}`);g.length===0&&console.log(`(none — add [[agents]] to the server config)`);return}if(g.includes(i.name)||d(`Unknown agent "${i.name}" (configured: ${g.join(`, `)||`none`})`),i.listSessions){let{sessions:e}=await(await t(m,`/agents/${encodeURIComponent(i.name)}/sessions`)).json();if(!e?.length){console.log(`No sessions yet.`);return}for(let t of e)console.log(`${t.sessionId} ${o}${t.updatedAt}${l}${t.parentSession?` ${o}(fork of ${t.parentSession})${l}`:``}`),t.preview&&console.log(` ${o}${t.preview}${l}`);return}if(process.stdin.isTTY&&process.stdout.isTTY&&!i.plain){let{runAgentTui:e}=await import(`./tui-ClNupsCH.mjs`);await e({connection:m,agentName:i.name,sessionId:i.sessionId,fork:i.fork});return}await p(m,i.name,i.sessionId,i.fork)};export{m as runAgentCommand};
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{a as e,n as t}from"./config-cxApm_PZ.mjs";import n from"node:path";import r from"node:fs";import i from"@iarna/toml";const a=a=>{let o={},s=process.cwd();a&&(r.existsSync(a)?(o=i.parse(r.readFileSync(a,`utf8`)),s=n.dirname(n.resolve(a))):o=i.parse(a));let c=e(o);return{config:c,configDir:s,agentsConfig:t(c,{configDir:s})}},o=e=>{try{return r.readFileSync(e,`utf8`).trim()||null}catch{return null}},s=async(e,t,n)=>fetch(`${e.baseUrl}${t}`,{...n,headers:{"content-type":`application/json`,...e.token?{authorization:`Bearer ${e.token}`}:{},...n?.headers}});async function*c(e){let t=e.getReader(),n=new TextDecoder,r=``;try{for(;;){let{done:e,value:i}=await t.read();if(e)break;r+=n.decode(i,{stream:!0});let a=r.indexOf(`
2
+ import{a as e,n as t}from"./config-DiYweJ2m.mjs";import n from"node:path";import r from"node:fs";import i from"@iarna/toml";const a=a=>{let o={},s=process.cwd();a&&(r.existsSync(a)?(o=i.parse(r.readFileSync(a,`utf8`)),s=n.dirname(n.resolve(a))):o=i.parse(a));let c=e(o);return{config:c,configDir:s,agentsConfig:t(c,{configDir:s})}},o=e=>{try{return r.readFileSync(e,`utf8`).trim()||null}catch{return null}},s=async(e,t,n)=>fetch(`${e.baseUrl}${t}`,{...n,headers:{"content-type":`application/json`,...e.token?{authorization:`Bearer ${e.token}`}:{},...n?.headers}});async function*c(e){let t=e.getReader(),n=new TextDecoder,r=``;try{for(;;){let{done:e,value:i}=await t.read();if(e)break;r+=n.decode(i,{stream:!0});let a=r.indexOf(`
3
3
 
4
4
  `);for(;a!==-1;){let e=r.slice(0,a);r=r.slice(a+2),a=r.indexOf(`
5
5
 
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import{a as e,i as t,n,o as r,r as i}from"./net-DPM9odNy.mjs";import{a,c as o,n as s}from"./config-DiYweJ2m.mjs";import{n as c,r as l}from"./client-qqI-HCOj.mjs";import u from"node:fs/promises";import d from"node:path";import f from"node:fs";import{execFile as p,spawn as m}from"node:child_process";import h from"node-machine-id";import g from"@iarna/toml";import{promisify as _}from"node:util";const v=_(p),y=e=>e?.code===`ENOENT`,b=async e=>{try{return await u.stat(e)}catch(e){if(y(e))return null;throw e}},x=e=>{if(!e)return{};try{let{env:t}=g.parse(e);return t&&typeof t==`object`&&!Array.isArray(t)?t:{}}catch{return{}}},S=e=>(process.platform===`win32`?/^[\w@%+=:,./\\-]+$/:/^[\w@%+=:,./-]+$/).test(e)?e:process.platform===`win32`?`"${e}"`:`'${e.replaceAll(`'`,`'\\''`)}'`,C=e=>process.platform===`darwin`?[`open`,e]:process.platform===`win32`?[`rundll32`,`url.dll,FileProtocolHandler`,e]:[`xdg-open`,e],w=()=>{if(process.platform===`darwin`)return[{argv:[`pbcopy`]}];if(process.platform===`win32`)return[{argv:[`clip`],encode:e=>Buffer.from(`\ufeff${e}`,`utf16le`)}];let e={argv:[`wl-copy`]},t={argv:[`xclip`,`-selection`,`clipboard`]};return process.env.WAYLAND_DISPLAY?[e,t]:[t,e]},T=(e,t)=>new Promise((n,r)=>{let i=m(e,t,{detached:!0,stdio:`ignore`});i.once(`error`,r),i.once(`spawn`,()=>{i.unref(),n()})}),E=(e,t,n)=>new Promise((r,i)=>{let a=m(e,t,{stdio:[`pipe`,`ignore`,`ignore`]});a.once(`error`,i),a.stdin.once(`error`,i),a.once(`exit`,t=>{t===0?r():i(Error(`${e} exited with code ${t}`))}),a.stdin.end(n)}),D=n=>{let o=new Set,p=()=>{for(let e of[...o])e()};return{configPath:n,serverVersion:e().version,machineId:h.machineIdSync(),async readConfigText(){try{return await u.readFile(n,`utf8`)}catch(e){if(y(e))return null;throw e}},async writeConfigText(e,{backup:t}){let r=await u.realpath(n).catch(()=>n),i=await b(r);await u.mkdir(d.dirname(r),{recursive:!0}),p();let a=d.join(d.dirname(r),`.${d.basename(r)}.${process.pid}-${Date.now()}.tmp`);try{await u.writeFile(a,e,{encoding:`utf8`,flag:`wx`,...i?{mode:i.mode&511}:{}}),i&&await u.chmod(a,i.mode&4095);let o=null;return t&&i&&(o=`${n}.bak`,await u.copyFile(r,o)),await u.rename(a,r),{backupPath:o}}catch(e){throw await u.rm(a,{force:!0}),e}},async statConfig(){let e=await b(n);return e?{mtimeMs:e.mtimeMs}:null},watchConfig(e){let t=d.basename(n),r=null,i=null,a=!1,s=()=>{if(o.delete(s),!a)try{i=f.watch(d.dirname(n),(n,i)=>{i&&i!==t||(r&&clearTimeout(r),r=setTimeout(()=>{r=null,e()},100))}),i.on(`error`,e=>{console.warn(`[ui] Stopped watching ${n}: ${e.message}`),i?.close()})}catch(e){y(e)?o.add(s):console.warn(`[ui] Cannot watch ${n}: ${e.message}`)}};return s(),()=>{a=!0,o.delete(s),r&&clearTimeout(r),i?.close()}},async probeCaps(e){return(await t(e.map(e=>({type:e})))).map(({type:e,score:t,hasGpu:n,usableBytes:r})=>({type:e,score:t??0,hasGpu:n??!1,usableBytes:r??0}))},async getBackendDefaults(e){return r(e)},resolveHfToken(e){let t={...x(e.raw.env),...process.env};return[e.runtime.huggingface_token,t.HUGGINGFACE_TOKEN,t.HF_TOKEN].find(e=>typeof e==`string`&&e.trim()!==``)?.trim()??null},async hostInfo(){return i()},async preferredAppearance(){if(process.platform!==`darwin`)return`light`;try{let{stdout:e}=await v(`defaults`,[`read`,`-g`,`AppleInterfaceStyle`]);return e.trim()===`Dark`?`dark`:`light`}catch{return`light`}},async openExternal(e){let{protocol:t}=new URL(e);if(t!==`http:`&&t!==`https:`)throw Error(`Only http(s) links can be opened: ${e}`);let[n,...r]=C(e);await T(n,r)},async copyToClipboard(e){let t=null;for(let{argv:n,encode:r}of w()){let[i,...a]=n;try{await E(i,a,r?r(e):e);return}catch(e){t=e}}throw t},startCommand:()=>`bricks-buttress --config ${S(n)}`,agentConnection(e){if(!f.existsSync(n))return null;let t;try{t=c(n)}catch{return null}let{config:r,agentsConfig:i}=t;return i?{baseUrl:`http://127.0.0.1:${e?.port??(r.server.port||2080)}`,token:l(i.runtimeTokenFile)}:null},agentsConfigError(e){let t=e;if(t===void 0)try{t=f.readFileSync(n,`utf8`)}catch{return{agents:0,error:null}}let r;try{r=g.parse(t)}catch(e){return{agents:0,error:`invalid TOML: ${O(e)}`}}let i=Array.isArray(r.agents)?r.agents.length:0;if(i===0)return{agents:0,error:null};try{return s(a(r),{configDir:d.dirname(n)}),{agents:i,error:null}}catch(e){return{agents:i,error:O(e)}}}}},O=e=>e instanceof Error?e.message:String(e),k=()=>import(`@fugood/buttress-ui`),A=[`darwin-arm64`,`linux-x64`,`win32-x64`],j=[`chat`,`status`,`settings`],M=e=>j.includes(e),N=e=>{let t=[],n;for(let r=0;r<e.length;r+=1){let i=e[r];if(i===`-h`||i===`--help`)return{kind:`help`};if(i===`-c`||i===`--config`){let n=e[r+1];if(n===void 0)return{kind:`error`,message:`${i} needs a config file path`};t.push(n),r+=1}else if(i===`--tab`){let t=e[r+1];if(t===void 0||!M(t)){let e=t===void 0?``:` (got "${t}")`;return{kind:`error`,message:`--tab needs one of ${j.join(`, `)}${e}`}}n=t,r+=1}else if(i.startsWith(`-`))return{kind:`error`,message:`Unknown option: ${i}`};else t.push(i)}return t.length===0?{kind:`error`,message:`bricks-buttress ui needs a config file path`}:t.length>1?{kind:`error`,message:`Give one config file path (got ${t.join(`, `)})`}:t[0].includes(`
3
+ `)?{kind:`error`,message:`bricks-buttress ui edits a config file; inline TOML is not supported`}:{kind:`run`,configPath:d.resolve(t[0]),initialTab:n}},P=()=>({isBun:`Bun`in globalThis,platform:process.platform,arch:process.arch}),F=(e,t)=>e.isBun?A.includes(`${e.platform}-${e.arch}`)?null:`bricks-buttress ui runs on darwin-arm64, linux-x64 and win32-x64; on this machine open ${t()} in a browser instead`:`bricks-buttress ui needs Bun; run it with bun or the standalone bricks-buttress binary`,I=e=>{try{let t=g.parse(f.readFileSync(e,`utf8`)).server?.port;return Number.isInteger(t)&&t>0?t:o}catch{return o}},L=e=>`http://${n()??`localhost`}:${I(e)}/status`,R=e=>e instanceof Error?e.message:String(e),z=e=>{let{code:t,specifier:n}=e??{};return t===`ERR_MODULE_NOT_FOUND`&&n===`@fugood/buttress-ui`},B=e=>process.env.BUTTRESS_SERVER_DISTRIBUTION===`1`&&R(e).includes(`@gpuix/native-`),V=async e=>{try{let{launchButtressUi:e}=await k();return e}catch(t){z(t)?console.error(`This bricks-buttress install does not include the settings window (the npm package leaves it out); use the standalone bricks-buttress binary or a source checkout, or open ${L(e)} in a browser instead`):B(t)?console.error(`The window's native module (gpuix) is missing next to bricks-buttress; run \`bricks-buttress update\` to reinstall it, or open ${L(e)} in a browser instead (${R(t)})`):console.error(`bricks-buttress ui failed to load the window: ${R(t)}`),process.exit(1)}},H=async e=>{let t=N(e);t.kind===`help`&&(console.log(`
4
+ Usage:
5
+ bricks-buttress ui [-c|--config] <config.toml> [--tab <chat|status|settings>]
6
+
7
+ Opens a native window to edit a buttress-server config file, to watch the
8
+ server that runs it and to chat with its agents.
9
+
10
+ Settings A form over the TOML file. Save shows the diff first, writes the
11
+ file atomically and keeps the previous content as <config>.bak.
12
+ A missing file starts a new config that is written only after
13
+ you confirm Create file.
14
+ Status Live dashboard of the server started with this config, found by
15
+ its id on http://127.0.0.1:<port> (/buttress/info, else
16
+ /buttress/status). The window never starts, stops or restarts
17
+ the server: restart it to apply saved changes.
18
+ Chat Chat with the [[agents]] of the running server: sessions grouped
19
+ by agent, streamed answers and tool calls, Stop, Fork and new
20
+ sessions. Uses the server's local runtime token, so it works on
21
+ an unbound server too.
22
+
23
+ The window opens on Chat when the server runs and has agents, on Status
24
+ when it runs without agents, and on Settings otherwise; --tab overrides it.
25
+
26
+ Runs under Bun on darwin-arm64, linux-x64 and win32-x64. On other machines,
27
+ open http://<host>:<port>/status in a browser.
28
+
29
+ Options:
30
+ -c, --config <path> Config file to edit (a positional path works too)
31
+ --tab <chat|status|settings> Tab to open first
32
+ -h, --help Show this help message
33
+
34
+ Examples:
35
+ bricks-buttress ui -c ./config.toml
36
+ bricks-buttress ui ./config.toml
37
+ bricks-buttress ui -c ./config.toml --tab chat
38
+ `),process.exit(0)),t.kind===`error`&&(console.error(`${t.message}\nRun \`bricks-buttress ui --help\` for usage.`),process.exit(1));let{configPath:n,initialTab:r}=t,i=F(P(),()=>L(n));i&&(console.error(i),process.exit(1));let a=await V(n);try{await a(D(n),{initialTab:r})}catch(e){console.error(`bricks-buttress ui failed: ${R(e)}`),process.exit(1)}process.exit(0)};export{H as runUiCommand};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import e from"node:path";import t from"node:os";import n from"bytes";import{z as r}from"zod";import i from"ms";import a from"node-machine-id";import"@iarna/toml";const o=e=>{let t=e?.model?.repo_id||e?.type||``,n=e?.model?.filename;return n?`${t}:${n}`:t},s=({family:e,types:t})=>(n,r)=>{let i=(n.generators||[]).filter(e=>t.includes(e.type));if(i.length===0){let n=t.map(e=>`"${e}"`).join(` or `);throw Error(`No ${e} generator configured. Add a [[generators]] with type = ${n} to your config.`)}let a=i[0];if(r){let t=i.find(e=>o(e)===r)||i.find(e=>{let{repo_id:t,filename:n}=e?.model||{};return t===r||n===r});if(!t){let t=i.map(o).join(`, `);throw Error(`Unknown ${e} model "${r}". Configured models: ${t||`(none)`}`)}a=t}let{generators:s,server:c,...l}=n.global||{};return{type:a.type||t[0],repoId:o(a),mergedConfig:{...l,...a,model:{...a.model}}}},c=({max:e,resolve:t,prepare:n,dispose:r})=>{let i=new Map,a=new Map,o=(e,t)=>{i.delete(e),i.set(e,t)},s=(e,t)=>(t.activeRequests+=1,o(e,t),t),c=async(t,n)=>{for(;i.size>e;){let e=Array.from(i.entries()).find(([,e])=>e.activeRequests===0);if(!e)return;let[a,o]=e;i.delete(a);try{await r?.(t,o),await t.finalizeGenerator?.(o.id),console.log(`${n} Evicted idle generator: ${a}`)}catch(e){console.warn(`${n} Failed to evict idle generator ${a}:`,e?.message)}}};return{getOrCreate:async(e,r,o,l)=>{let{type:u,repoId:d,mergedConfig:f}=t(r,o),p=d,m=i.get(p);if(m?.initialized)return s(p,m);let h=a.get(p);if(h)return s(p,await h);console.log(`${l} Creating ${u} generator for ${p}`);let g=(async()=>{let{id:t}=await e.startGenerator(u,f),r={id:t,type:u,config:f,repoId:d,initialized:!1,activeRequests:0};return await n(e,r),r.initialized=!0,i.set(p,r),console.log(`${l} Generator ready: ${p}`),r})().finally(()=>{a.delete(p)});a.set(p,g);let _=s(p,await g);return await c(e,l),_},release:async(e,t,n)=>{t&&(t.activeRequests=Math.max(0,t.activeRequests-1),await c(e,n))}}};function l(e){e.cancel().catch(()=>{})}const u=[`ggml-llm`,`ggml-stt`,`ggml-tts`,`ggml-decision`,`mlx-llm`,`onnx-stt`,`onnx-tts`],d=2080;function f(e){return`buttress-${e}`}function p(e){return`Buttress Server (${e.slice(-8)})`}function m(e){return e.enabled!==!1}function h(e){return e.filter(m)}const g=[`ggml-llm`,`ggml-stt`,`ggml-tts`,`ggml-decision`],_=[`onnx-stt`,`onnx-tts`],v=[`ggml-llm`],y=[`ggml-stt`],b=[`ggml-tts`],x=[`ggml-decision`],S=[`mlx-llm`],C=[...g,..._],w=[`debug`,`info`,`warn`,`error`],T=[`auto`,`on`,`off`],E=[`none`,`layer`,`row`,`tensor`],D=[`f16`,`f32`,`bf16`,`q8_0`,`q4_0`,`q4_1`,`iq4_nl`,`q5_0`,`q5_1`],O=[`cuda`,`vulkan`,`snapdragon`,`default`],k=[`cuda`,`vulkan`,`default`],A=[`cuda`,`coreml`,`dml`,`webgpu`,`cpu`],j=(e,t,n,r,i,a={})=>({section:e,key:t,kind:n,label:r,help:i,tier:2,...a}),M=(e,t,n,r,i)=>j(`server`,e,t,n,r,{tier:1,...i}),ee=[M(`id`,`string`,`Server id`,`Stable id used for autodiscover and workspace binding.`,{defaultHint:`buttress-<machine id>`}),M(`name`,`string`,`Display name`,`Name shown to clients that discover this server.`,{defaultHint:`Buttress Server (<short id>)`}),M(`port`,`integer`,`Port`,`HTTP/RPC listener port; the --port flag overrides it.`,{defaultHint:`2080`,min:1,max:65535}),M(`log_level`,`enum`,`Log level`,`Server log verbosity.`,{enumValues:w}),M(`max_body_size`,`size`,`Max body size`,`Request body limit: "100MB", "1GB" or bytes.`,{defaultHint:`50MB`}),M(`session_timeout`,`duration`,`Session timeout`,`Idle time before a session is dropped: milliseconds or "30s", "5m".`,{defaultHint:`60000`}),M(`temp_file_dir`,`path`,`Temp file directory`,`Where uploaded session files go.`,{defaultHint:`$TMPDIR/.buttress`})],N=[j(`runtime`,`cache_dir`,`path`,`Cache directory`,`Model and metadata cache root.`,{tier:1,defaultHint:`~/.buttress/models`}),j(`runtime`,`huggingface_token`,`secret`,`HuggingFace token`,`Used for HuggingFace downloads; falls back to $HUGGINGFACE_TOKEN.`,{tier:1}),j(`runtime`,`n_threads`,`integer`,`Threads`,`CPU thread count.`,{tier:1,min:1}),j(`runtime`,`n_gpu_layers`,`integer`,`GPU layers`,`Layers offloaded to the GPU.`,{tier:1,enumValues:[`auto`],defaultHint:`auto`}),j(`runtime`,`n_parallel`,`integer`,`Parallel sequences`,`Parallel sequences.`,{tier:1,min:1,defaultHint:`4`}),j(`runtime`,`n_batch`,`integer`,`Batch size`,`Prompt batch size. ggml-llm has a model-level default of 512 that shadows this value; set n_batch on the generator model instead.`,{tier:1,min:1}),j(`runtime`,`n_ctx`,`integer`,`Context window`,`Context window. A per-model value wins; ggml-llm caps it at the training context.`,{tier:1,min:1}),j(`runtime`,`context_release_delay_ms`,`integer`,`Context release delay (ms)`,`Idle time before a context is unloaded; 0 releases it right away.`,{tier:1,min:0,defaultHint:`10000`}),j(`runtime`,`flash_attn_type`,`enum`,`Flash attention`,`Defaults to "auto" when a GPU backend is selected and "off" on CPU.`,{tier:1,enumValues:T,defaultHint:`auto on GPU, off on CPU`}),j(`runtime`,`cache_type_k`,`enum`,`KV cache type (K)`,`KV-cache dtype for keys.`,{tier:1,enumValues:D,defaultHint:`f16`}),j(`runtime`,`cache_type_v`,`enum`,`KV cache type (V)`,`KV-cache dtype for values.`,{tier:1,enumValues:D,defaultHint:`f16`}),j(`runtime`,`kv_unified`,`boolean`,`Unified KV cache`,`One KV cache across sequences.`,{tier:1}),j(`runtime`,`http_headers`,`table`,`HTTP headers`,`Extra headers for HF / HTTP downloads.`),j(`runtime`,`prefer_variants`,`string-list`,`Preferred variants`,`Overrides the variant probe order of the ggml backends.`),j(`runtime`,`n_ubatch`,`integer`,`Micro-batch size`,`Prompt micro-batch size.`,{min:1}),j(`runtime`,`n_cpu_moe`,`integer`,`CPU MoE layers`,`MoE expert layers kept on the CPU.`,{min:0}),j(`runtime`,`swa_full`,`boolean`,`Full SWA cache`,`Full attention for sliding-window layers.`),j(`runtime`,`ctx_shift`,`boolean`,`Context shift`,`Allow llama.cpp's rolling context shift.`),j(`runtime`,`state_cache_budget_mb`,`integer`,`State cache budget (MB)`,`Memory budget of the cross-turn KV prefix cache for recurrent / hybrid models; 0 disables it.`,{min:0,defaultHint:`160`}),j(`runtime`,`state_cache_max_checkpoints`,`integer`,`State cache checkpoints`,`Snapshot count cap for that cache; 0 = unlimited. The memory budget takes precedence.`,{min:0,defaultHint:`8`}),j(`runtime`,`use_mmap`,`boolean`,`Memory-map weights`,`Memory-map the model file.`),j(`runtime`,`use_mlock`,`boolean`,`Lock weights in RAM`,`Lock the model in memory.`),j(`runtime`,`no_extra_bufts`,`boolean`,`No extra buffer types`,`Disable extra compute buffer types.`),j(`runtime`,`cpu_mask`,`string`,`CPU mask`,`CPU affinity mask.`),j(`runtime`,`cpu_strict`,`boolean`,`Strict CPU placement`,`Strict CPU affinity.`),j(`runtime`,`devices`,`string-list`,`Devices`,`Restrict to these GGML devices.`),j(`runtime`,`split_mode`,`enum`,`GPU split mode`,`ggml-llm GPU splitting: one GPU, layers + KV, weight rows, or weights + KV (experimental).`,{enumValues:E,defaultHint:`layer`}),j(`runtime`,`tensor_split`,`number-list`,`Tensor split`,`ggml-llm per-GPU proportions, e.g. [3, 1] or "3,1"; unset uses the automatic split.`),j(`runtime`,`model_draft`,`table`,`Draft model`,`Default draft model for speculative decoding: a URL/path string or a model table.`),...P(`runtime`),j(`runtime`,`max_threads`,`integer`,`Max threads`,`Caps the native thread count.`,{generatorTypes:[`ggml-stt`,`ggml-tts`,`ggml-decision`],min:1}),j(`runtime`,`vocoder_batch_size`,`integer`,`Vocoder batch size`,`Passed to initVocoder.`,{generatorTypes:b,min:1,defaultHint:`4096`}),j(`runtime`,`output_cache`,`table`,`Output cache`,`Synthesized audio cache: enabled, max_size_bytes, max_entries.`,{generatorTypes:[...b,`onnx-tts`],defaultHint:`enabled, 2GB, 5000 entries`}),j(`runtime`,`mlx_env_dir`,`path`,`MLX venv directory`,`Auto-managed Python venv.`,{generatorTypes:S,defaultHint:`{cache_dir}/mlx-env`}),j(`runtime`,`mlx_lm_package`,`string`,`mlx-lm package`,`pip spec for the venv.`,{generatorTypes:S,defaultHint:`mlx-lm==0.31.1`}),j(`runtime`,`mlx_vlm_package`,`string`,`mlx-vlm package`,`pip spec for the venv.`,{generatorTypes:S,defaultHint:`mlx-vlm==0.4.0`}),j(`runtime`,`prefer_providers`,`string-list`,`Preferred providers`,`Overrides the ONNX execution provider order.`,{generatorTypes:_})];function P(e,t){let n=(n,r,i,a,o)=>j(e,n,r,i,a,{generatorTypes:t,...o});return[n(`speculative`,`table`,`Speculative decoding`,`Enable speculative decoding: true, a strategy name, or a table.`),n(`spec_type`,`string`,`Speculative strategy`,`Strategy, such as "draft-mtp".`),n(`spec_draft_n_max`,`integer`,`Draft tokens (max)`,`Max drafted tokens per step.`,{min:0}),n(`spec_draft_n_min`,`integer`,`Draft tokens (min)`,`Min drafted tokens.`,{min:0}),n(`spec_draft_p_min`,`number`,`Draft min probability`,`Min acceptance probability.`),n(`spec_draft_p_split`,`number`,`Draft split threshold`,`Split threshold.`),n(`spec_draft_n_gpu_layers`,`integer`,`Draft GPU layers`,`Draft-model layers offloaded to the GPU (-1 auto, -2 all).`),n(`spec_draft_cache_type_k`,`enum`,`Draft KV cache type (K)`,`Draft-model K-cache dtype.`,{enumValues:D}),n(`spec_draft_cache_type_v`,`enum`,`Draft KV cache type (V)`,`Draft-model V-cache dtype.`,{enumValues:D})]}const F=[j(`runtime.session_cache`,`enabled`,`boolean`,`Session cache`,`Save the KV cache to disk after completions for prompt reuse (ggml-llm).`,{tier:1,defaultHint:`true`}),j(`runtime.session_cache`,`max_size_bytes`,`size`,`Max cache size`,`Disk budget: "10GB", "500MB" or bytes.`,{tier:1,defaultHint:`10GB`}),j(`runtime.session_cache`,`max_entries`,`integer`,`Max entries`,`Cached entry cap.`,{tier:1,min:1,defaultHint:`1000`})],I=[j(`autodiscover`,`enabled`,`boolean`,`LAN discovery`,"The server runs discovery whenever an [autodiscover] table exists; `autodiscover = false` turns it off.",{tier:1,defaultHint:`on while the [autodiscover] table exists`}),j(`autodiscover`,`udp.port`,`integer`,`UDP port`,`Discovery UDP port.`,{tier:1,min:1,max:65535,defaultHint:`8089`}),j(`autodiscover`,`udp.announcements.enabled`,`boolean`,`UDP announcements`,`Announce on the LAN.`,{defaultHint:`true`}),j(`autodiscover`,`udp.announcements.interval`,`integer`,`Announcement interval (ms)`,`Time between announcements.`,{min:1,defaultHint:`5000`}),j(`autodiscover`,`udp.requests.enabled`,`boolean`,`Answer UDP requests`,`Reply to scans.`,{defaultHint:`true`}),j(`autodiscover`,`udp.requests.responseDelay`,`integer`,`Response delay (ms)`,`Delay before replying to a scan.`,{min:0,defaultHint:`100`}),j(`autodiscover`,`http.enabled`,`boolean`,`HTTP info endpoint`,`Serve the info endpoint.`,{defaultHint:`true`}),j(`autodiscover`,`http.path`,`string`,`HTTP info path`,`Path of the info endpoint.`,{defaultHint:`/buttress/info`}),j(`autodiscover`,`http.cors`,`boolean`,`HTTP CORS`,`Allow cross-origin info requests.`,{defaultHint:`true`}),j(`autodiscover`,`mdns.enabled`,`boolean`,`mDNS`,`Bonjour / Avahi advertisement.`,{defaultHint:`false`})],L=(e,t,n)=>[j(e,`enabled`,`boolean`,t,`Serve ${n}.`,{tier:1,defaultHint:`false`}),j(e,`cors_allowed_origins`,`string-list`,`CORS allowed origins`,`"*" or a list of origins; unset keeps browsers blocked.`,{tier:1})],R=[...L(`openai_compat`,`OpenAI-compatible API`,`/oai-compat/v1/*`),...L(`anthropic_messages`,`Anthropic Messages API`,`/anthropic-messages`),...L(`typesafe_compat`,`TypeSafe-compatible API`,`/typesafe-compat/v1/*`)],z=(e,t,n,r,i)=>j(`generator.backend`,e,t,n,r,{tier:1,...i}),B=[z(`variant`,`enum`,`Variant`,`Force a native library variant.`,{generatorTypes:[`ggml-llm`,`ggml-stt`],enumValues:O,defaultHint:`auto`}),z(`variant`,`enum`,`Variant`,`Force a native library variant.`,{generatorTypes:[`ggml-tts`,`ggml-decision`],enumValues:k,defaultHint:`auto`}),z(`variant_preference`,`string-list`,`Variant order`,`Probe order when variant is unset.`,{generatorTypes:[`ggml-llm`,`ggml-stt`],defaultHint:`cuda, vulkan, snapdragon, default`}),z(`variant_preference`,`string-list`,`Variant order`,`Probe order when variant is unset.`,{generatorTypes:[`ggml-tts`,`ggml-decision`],defaultHint:`cuda, vulkan, default`}),z(`gpu_memory_fraction`,`fraction`,`GPU memory fraction`,`Max GPU memory share the hardware guardrails may plan against.`,{generatorTypes:C,defaultHint:`0.85`}),z(`cpu_memory_fraction`,`fraction`,`CPU memory fraction`,`Max RAM share for CPU-side buffers.`,{generatorTypes:C,defaultHint:`0.5`}),z(`share_llm_context`,`boolean`,`Share the LLM context`,`Run decisions on a ggml-llm entry with the same weights instead of loading them again.`,{generatorTypes:x,defaultHint:`false`}),z(`provider`,`enum`,`Execution provider`,`Force an ONNX execution provider.`,{tier:2,generatorTypes:_,enumValues:A,defaultHint:`auto`}),z(`provider_preference`,`string-list`,`Provider order`,`Probe order when provider is unset.`,{tier:2,generatorTypes:_,defaultHint:A.join(`, `)})],V=(e,t,n,r,i)=>j(`generator.model`,e,t,n,r,i),H=[`n_ubatch`,`n_threads`,`n_parallel`,`n_cpu_moe`,`flash_attn_type`,`cache_type_k`,`cache_type_v`,`kv_unified`,`swa_full`,`ctx_shift`,`state_cache_budget_mb`,`state_cache_max_checkpoints`,`use_mmap`,`use_mlock`,`no_extra_bufts`,`cpu_mask`,`cpu_strict`,`devices`,`split_mode`,`tensor_split`],te=[V(`repo_id`,`string`,`Repository`,`HuggingFace repo (org/repo). Required.`,{tier:1,generatorTypes:u.filter(e=>e!==`ggml-stt`)}),V(`repo_id`,`string`,`Repository`,`HuggingFace repo (org/repo).`,{tier:1,generatorTypes:y,defaultHint:`BricksDisplay/whisper-ggml`}),V(`revision`,`string`,`Revision`,`Branch, tag or commit.`,{tier:1,defaultHint:`main`}),V(`download`,`boolean`,`Download at startup`,`Pre-download the model when the server starts.`,{tier:1,defaultHint:`false`}),V(`filename`,`string`,`File`,`Pin a specific artifact in the repo.`,{tier:1,generatorTypes:g}),V(`quantization`,`string`,`Quantization`,`Preferred quant tag, e.g. q4_0, q8_0, mxfp4.`,{tier:1,generatorTypes:g}),V(`url`,`string`,`Download URL`,`Direct download URL; skips the manifest lookup.`,{generatorTypes:g}),V(`preferred_quantizations`,`string-list`,`Quantization fallbacks`,`Ordered fallback list when quantization does not match.`,{generatorTypes:v}),V(`preferred_quantizations`,`string-list`,`Quantization fallbacks`,`Ordered fallback list when quantization does not match.`,{generatorTypes:y,defaultHint:`q8_0, <no-quant>, q5_1`}),V(`preferred_quantizations`,`string-list`,`Quantization fallbacks`,`Ordered fallback list when quantization does not match.`,{generatorTypes:b,defaultHint:`q4_k_m, q8_0, <no-quant>`}),V(`preferred_quantizations`,`string-list`,`Quantization fallbacks`,`Ordered fallback list when quantization does not match.`,{generatorTypes:x,defaultHint:`q8_0, q4_k_m, <no-quant>`}),V(`quantizations`,`string-list`,`Quantization fallbacks (alias)`,`Alias of preferred_quantizations.`,{generatorTypes:g}),V(`allow_local_file`,`boolean`,`Allow local files`,`Required to use local_path and the other *_local_path keys.`,{generatorTypes:g,defaultHint:`false`}),V(`local_path`,`path`,`Local file`,`Load this local file; repo metadata still comes from repo_id.`,{generatorTypes:g}),V(`api_base`,`string`,`HF API base`,`Override the HuggingFace API host (mirror / proxy).`,{generatorTypes:g}),V(`base_url`,`string`,`HF base URL`,`Override the HuggingFace blob host (mirror / proxy).`,{generatorTypes:[...g,..._]}),V(`n_ctx`,`integer`,`Context window`,`Auto-capped at the model training context.`,{tier:1,generatorTypes:v,min:1,defaultHint:`auto · capped at the model training context`}),V(`n_gpu_layers`,`integer`,`GPU layers`,`Layers offloaded to the GPU.`,{generatorTypes:v,enumValues:[`auto`],defaultHint:`auto`}),V(`n_batch`,`integer`,`Batch size`,`Prompt batch size.`,{generatorTypes:v,min:1,defaultHint:`512`}),V(`embedding`,`boolean`,`Embedding mode`,`For a dedicated embedding model.`,{generatorTypes:v,defaultHint:`false`}),V(`pooling_type`,`string`,`Pooling type`,`Embedding pooling override, e.g. mean.`,{generatorTypes:v}),...N.filter(e=>H.includes(e.key)).map(e=>({...e,section:`generator.model`,tier:2,generatorTypes:v})),V(`enable_mtmd`,`boolean`,`Vision (mtmd)`,`Load the matching mmproj projector.`,{tier:1,generatorTypes:v,defaultHint:`false`}),V(`mmproj_filename`,`string`,`Projector file`,`Pin a specific projector file.`,{generatorTypes:[...v,...x]}),V(`mmproj_url`,`string`,`Projector URL`,`Direct projector URL.`,{generatorTypes:[...v,...x]}),V(`mmproj_local_path`,`path`,`Local projector`,`Requires allow_local_file.`,{generatorTypes:[...v,...x]}),V(`mmproj_use_gpu`,`boolean`,`Projector on GPU`,`Unset = on when n_gpu_layers > 0.`,{generatorTypes:v,defaultHint:`auto`}),V(`mmproj_image_min_tokens`,`integer`,`Min visual tokens`,`Dynamic-resolution models; -1 = unset.`,{generatorTypes:v,defaultHint:`-1`}),V(`mmproj_image_max_tokens`,`integer`,`Max visual tokens`,`-1 = unset.`,{generatorTypes:v,defaultHint:`-1`}),V(`model_draft`,`table`,`Draft model`,`Separate GGUF draft model: a URL/path string or a table with repo_id, filename, url, local_path.`,{generatorTypes:v}),...P(`generator.model`,v),V(`use_gpu`,`boolean`,`Use GPU`,`Turn off to keep the model on the CPU.`,{tier:1,generatorTypes:[...y,...b,...x],defaultHint:`true`}),V(`use_flash_attn`,`enum`,`Flash attention`,`"auto" enables it when the GPU is in use; true / false are accepted for "on" / "off".`,{tier:1,generatorTypes:y,enumValues:T,defaultHint:`auto`}),V(`engine`,`enum`,`Engine`,`Speech engine; detected from the repo and file names.`,{generatorTypes:y,enumValues:[`whisper`,`parakeet`],defaultHint:`auto`}),V(`n_gpu_layers`,`integer`,`GPU layers`,`Forced to 0 when the GPU is unused.`,{generatorTypes:[...b,...x],defaultHint:`99`}),V(`vocoder_repo_id`,`string`,`Codec repository`,`Repo holding the codec GGUF.`,{tier:1,generatorTypes:[...b,`onnx-tts`],defaultHint:`same as repo_id`}),V(`vocoder_filename`,`string`,`Codec file`,`Pin a specific codec artifact.`,{tier:1,generatorTypes:b,defaultHint:`auto`}),V(`vocoder_url`,`string`,`Codec URL`,`Direct codec URL.`,{generatorTypes:b}),V(`vocoder_revision`,`string`,`Codec revision`,`Falls back to revision.`,{generatorTypes:b}),V(`vocoder_local_path`,`path`,`Local codec`,`Requires allow_local_file.`,{generatorTypes:b}),V(`sample_rate`,`integer`,`Sample rate`,`Fallback only; the codec reports the real rate.`,{generatorTypes:b,min:1,defaultHint:`24000`}),V(`mmproj_repo_id`,`string`,`Projector repository`,`For models whose prompt takes images.`,{generatorTypes:x,defaultHint:`same as repo_id`}),V(`mmproj_revision`,`string`,`Projector revision`,`Falls back to revision.`,{generatorTypes:x}),V(`rank_pooling`,`boolean`,`Rank pooling`,`system_one models with a classification head (rank_head readout).`,{generatorTypes:x,defaultHint:`false`}),V(`vlm`,`boolean`,`Vision model`,`"auto" infers it from the repo.`,{tier:1,generatorTypes:S,enumValues:[`auto`],defaultHint:`auto`}),V(`adapter_path`,`path`,`LoRA adapter`,`Local LoRA adapter directory.`,{generatorTypes:S}),V(`tokenizer_config`,`table`,`Tokenizer config`,`Forwarded to mlx_lm.load.`,{generatorTypes:S}),V(`model_config`,`table`,`Model config`,`Forwarded to mlx_lm.load.`,{generatorTypes:S}),V(`task`,`string`,`Task`,`transformers.js pipeline task.`,{generatorTypes:_}),V(`device`,`string`,`Device`,`transformers.js device.`,{generatorTypes:_}),V(`dtype`,`string`,`Data type`,`Weight dtype.`,{generatorTypes:_,defaultHint:`auto`})],ne=[...N.map(e=>({...e,section:`generator.runtime`,tier:2})),j(`generator.runtime`,`session_cache`,`table`,`Session cache`,`Overrides [runtime.session_cache].`)],re=[...ee,...N,...F,...I,...R,...B,...te,...ne];r.enum(u),new Set(re.filter(e=>e.section===`generator.backend`).map(e=>e.key));const U=(e={},t={})=>{let n=Array.isArray(e)?[...e]:{...e};return Object.entries(t||{}).forEach(([e,t])=>{t&&typeof t==`object`&&!Array.isArray(t)?n[e]=U(n[e]||{},t):n[e]=t}),n},W=e=>e&&typeof e==`object`?structuredClone(e):null,ie=(e,t)=>{let n=W(e)||{},r=W(t)||{};return U(n,r)},G=(e,t)=>U(structuredClone(e.global),t||{}),ae=(e,t,n,r)=>{if(e.generators.length>0){let i=e.generators.filter(e=>e?.type===n);if(i.length>0&&r){let a=i.find(e=>t.getModelIdentifier(n,e)===r);if(a)return G(e,a)}}return Object.keys(e.global).length>0?G(e,{}):null},K={udp:{port:8089,announcements:{enabled:!0,interval:5e3},requests:{enabled:!0,responseDelay:100}},http:{enabled:!0,path:`/buttress/info`,cors:!0}},oe=e=>e?e===!0?{...K}:U(K,e):null,se=(e,t)=>{if(!e.generators||e.generators.length===0)return t.map(e=>({type:e}));let n=new Set;return e.generators.forEach(e=>{e.type&&n.add(e.type)}),n.size===0?t.map(e=>({type:e})):Array.from(n).map(e=>({type:e}))},q=(e,t,n)=>e===void 0?n:typeof e==`number`?e:t(e)??n,J=6e4,Y=1024*1024*50,ce=r=>{let o=a.machineIdSync(),s={server:{id:f(o),name:p(o),port:d,temp_file_dir:e.join(t.tmpdir(),`.buttress`),session_timeout:J,max_body_size:Y},autodiscover:!1},c=U(s,W(r)||{}),l=Array.isArray(c.generators)?h(c.generators):[],{server:u,generators:m,autodiscover:g,..._}=c;return{autodiscover:oe(g),server:{id:u.id,name:u.name,port:u.port,log_level:u.log_level,temp_file_dir:u.temp_file_dir,max_body_size:q(u.max_body_size,n.parse,Y),session_timeout:q(u.session_timeout,e=>i(e),J)},global:_,generators:l}},X=/^[A-Za-z0-9_-]{1,64}$/,Z=n=>n===`~`||n.startsWith(`~/`)?e.join(t.homedir(),n.slice(1)):n;var Q=class extends Error{constructor(e){super(`[Agents] ${e}`),this.name=`AgentsConfigError`}};const le=(e,t,n)=>{if(t==null)return n;if(t===0||t===!1)return null;if(typeof t==`number`){if(Number.isFinite(t)&&t>0)return t}else if(typeof t==`string`){let e;try{e=i(t)}catch{e=void 0}if(typeof e==`number`&&e>0)return e}throw new Q(`agents_options.${e} must be a duration string (e.g. "30d") or a positive number of milliseconds; use 0 to disable it`)},$=[`direct`,`codemode`,`deferred`,`hidden`],ue=new Set([`codemode`,`deferred`]),de=(e,t,n)=>{if(t==null)return{};if(typeof t!=`object`||Array.isArray(t))throw new Q(`agent "${e}": mcp_servers must be a table`);let r={};for(let[i,a]of Object.entries(t)){if(!X.test(i))throw new Q(`agent "${e}": MCP server name "${i}" must match ${X}`);let t=!!(typeof a?.url==`string`&&a.url),o=!!(typeof a?.command==`string`&&a.command);if(t===o)throw new Q(`agent "${e}": MCP server "${i}" needs exactly one of url / command`);let s=`agent "${e}": MCP server "${i}"`;if(a?.timeout!=null&&!(typeof a.timeout==`number`&&a.timeout>0))throw new Q(`${s}: timeout must be a positive number of seconds`);let c=a?.tool_exposure;if(c!=null&&(typeof c!=`object`||Array.isArray(c)))throw new Q(`${s}: tool_exposure must map tool names to exposures`);for(let e of[a?.exposure,...Object.values(c??{})]){if(e!=null&&!$.includes(e))throw new Q(`${s}: exposure must be one of ${$.map(e=>`"${e}"`).join(`, `)}`);if(!n&&ue.has(e))throw new Q(`${s}: exposure "${e}" needs codemode = true on the agent`)}r[i]={url:t?a.url:void 0,headers:a?.headers&&typeof a.headers==`object`?a.headers:void 0,command:o?a.command:void 0,args:Array.isArray(a?.args)?a.args.map(String):void 0,env:a?.env&&typeof a.env==`object`?a.env:void 0,optional:a?.optional===!0,timeout:a?.timeout??void 0,exposure:a?.exposure??void 0,toolExposure:c??void 0}}return r},fe=(e,t,n)=>{let r=e?.name;if(typeof r!=`string`||!X.test(r))throw new Q(`[[agents]] #${t+1}: name is required and must match ${X}`);let i=e?.model;if(typeof i!=`string`||!i.includes(`/`))throw new Q(`agent "${r}": model must be "provider/model-id" (e.g. "buttress/ggml-org/gpt-oss-20b-GGUF")`);let a=i.indexOf(`/`),s=i.slice(0,a),c=i.slice(a+1);if(!s||!c)throw new Q(`agent "${r}": invalid model reference "${i}"`);if(s===`buttress`&&!n.generators.filter(e=>e.type===`ggml-llm`||e.type===`mlx-llm`).some(e=>(e.model?.repo_id||e.model?.repository)===c))throw new Q(`agent "${r}": model "${c}" does not match any configured [[generators]] ggml-llm/mlx-llm repo_id`);if(e?.system_prompt!=null&&e?.system_prompt_file!=null)throw new Q(`agent "${r}": set system_prompt or system_prompt_file, not both`);let l=[];if(e?.tools!=null){if(!Array.isArray(e.tools))throw new Q(`agent "${r}": tools must be an array of function names`);for(let t of e.tools){if(typeof t!=`string`||!X.test(t))throw new Q(`agent "${r}": tool name "${t}" must match ${X} (local function names only)`);if(l.includes(t))throw new Q(`agent "${r}": duplicate tool "${t}"`);l.push(t)}}if(e?.local_devices!==void 0&&typeof e.local_devices!=`boolean`)throw new Q(`agent "${r}": local_devices must be a boolean`);if(e?.codemode!==void 0&&typeof e.codemode!=`boolean`)throw new Q(`agent "${r}": codemode must be a boolean`);let u=Number(e?.max_turns??30);if(!Number.isInteger(u)||u<1)throw new Q(`agent "${r}": max_turns must be a positive integer`);let d=null;if(e?.max_tokens_per_run!=null&&(d=Number(e.max_tokens_per_run),!Number.isInteger(d)||d<1))throw new Q(`agent "${r}": max_tokens_per_run must be a positive integer`);let f=null;if(e?.decision_model!=null&&e.decision_model!==``){let t=e.decision_model,i=typeof t==`string`?t.indexOf(`/`):-1;if(i<=0||i===t.length-1)throw new Q(`agent "${r}": decision_model must be "provider/model-id" (e.g. "buttress/<ggml-decision id>" or "typesafe/jev-latest")`);if(f={provider:t.slice(0,i),modelId:t.slice(i+1),modelRef:t},f.provider===`buttress`){let e=n.generators.filter(e=>e.type===`ggml-decision`).map(o);if(!e.includes(f.modelId))throw new Q(`agent "${r}": decision_model "${f.modelId}" does not match any configured ggml-decision generator (${e.join(`, `)||`none`})`)}}if(e?.decision_assistance!==void 0&&typeof e.decision_assistance!=`boolean`)throw new Q(`agent "${r}": decision_assistance must be a boolean`);if(e?.decision_assistance===!0&&!f)throw new Q(`agent "${r}": decision_assistance needs a decision_model`);let p=e?.decision_timeout_ms??500;if(!Number.isInteger(p)||p<1)throw new Q(`agent "${r}": decision_timeout_ms must be a positive integer`);let{name:m,model:h,system_prompt:g,system_prompt_file:_,tools:v,local_devices:y=!1,codemode:b=!1,mcp_servers:x,max_turns:S,max_tokens_per_run:C,thinking:w,decision_model:T,decision_assistance:E=!1,decision_timeout_ms:D,...O}=e;return{name:r,provider:s,modelId:c,modelRef:i,systemPrompt:typeof g==`string`?g:null,systemPromptFile:typeof _==`string`?_:null,tools:l,localDevices:y,codemode:b,mcpServers:de(r,x,b),maxTurns:u,maxTokensPerRun:d,thinking:typeof w==`string`?w:void 0,decisionModel:f,decisionAssistance:E,decisionTimeoutMs:p,generation:O}},pe=(t,{configDir:n=process.cwd()}={})=>{let r=t.global||{},i=r.agents;if(i==null)return null;if(!Array.isArray(i))throw new Q(`agents must be an array of tables ([[agents]])`);if(i.length===0)return null;let a=i.map((e,n)=>fe(e,n,t)),o=new Set;for(let e of a){if(o.has(e.name))throw new Q(`duplicate agent name "${e.name}"`);o.add(e.name)}let s=r.agents_options||{},c=typeof s.sessions_dir==`string`&&s.sessions_dir?s.sessions_dir:`./.buttress-agent/sessions`,l=Z(c),u=e.isAbsolute(l)?l:e.resolve(n,l);for(let t of a)if(t.systemPromptFile){let r=Z(t.systemPromptFile);t.systemPromptFile=e.isAbsolute(r)?r:e.resolve(n,r)}let d=Number(s.max_depth??2);if(!Number.isInteger(d)||d<1)throw new Q(`agents_options.max_depth must be a positive integer`);let f=s.session_max_count,p=500;if(f===0||f===!1)p=null;else if(f!=null&&(p=Number(f),!Number.isInteger(p)||p<1))throw new Q(`agents_options.session_max_count must be a positive integer`);return{agents:a,configDir:n,sessionsDir:u,runtimeTokenFile:e.join(e.dirname(u),`runtime-token`),sessionMaxAgeMs:le(`session_max_age`,s.session_max_age,2592e6),sessionMaxCount:p,maxDepth:d,allowUnauthenticated:s.allow_unauthenticated===!0}};export{ce as a,d as c,o as d,s as f,W as i,m as l,pe as n,ae as o,c as p,ie as r,se as s,Q as t,l as u};