@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 +91 -37
- package/config/sample.toml +1 -2
- package/lib/agent/tools.d.ts +5 -2
- package/lib/{cli-CIa2rDj7.mjs → cli-KbjOekMI.mjs} +2 -2
- package/lib/{client-BVyIuSyr.mjs → client-qqI-HCOj.mjs} +1 -1
- package/lib/command-B92OGHj0.mjs +38 -0
- package/lib/config-DiYweJ2m.mjs +2 -0
- package/lib/index.mjs +61 -99
- package/lib/net-DPM9odNy.mjs +46 -0
- package/lib/routes/agents.d.ts +33 -4
- package/lib/routes/openai-compat.d.ts +13 -2
- package/lib/routes/openai-decisions.check.d.ts +1 -0
- package/lib/routes/openai-decisions.d.ts +97 -0
- package/lib/routes/status.d.ts +2 -1
- package/lib/routes/typesafe-compat.d.ts +5 -6
- package/lib/{tui-Bxeb1d7E.mjs → tui-ClNupsCH.mjs} +1 -1
- package/lib/types.d.ts +14 -2
- package/lib/ui/command.d.ts +2 -0
- package/lib/ui/host.d.ts +1 -0
- package/lib/ui/window-module.d.ts +6 -0
- package/lib/utils/config.d.ts +2 -1
- package/lib/utils/downloadStatus.d.ts +57 -0
- package/lib/utils/hostFacts.d.ts +13 -0
- package/package.json +10 -8
- package/public/status.html +219 -1592
- package/lib/config-cxApm_PZ.mjs +0 -2
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,
|
|
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
|
-
|
|
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
|
|
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]]
|
|
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
|
|
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
|
|
1051
|
-
|
|
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
|
|
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
|
package/config/sample.toml
CHANGED
|
@@ -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
|
package/lib/agent/tools.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
17
|
-
|
|
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-
|
|
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(`[1m${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(`[36m> ${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-
|
|
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(`[1m${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(`[36m> ${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-
|
|
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};
|