@bolt-foundry/gambit 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,94 +1,167 @@
1
- # Gambit (draft README)
1
+ # Gambit
2
2
 
3
- This package is developed in Bolt Foundry’s monorepo and synced into gambitmono
4
- (`github.com/bolt-foundry/gambit`). For now, gambitmono is a strict sync target
5
- (please do not land direct changes there).
3
+ Gambit helps you build reliable LLM workflows by composing small, typed “decks”\
4
+ with clear inputs/outputs and guardrails. Run decks locally, stream traces, and\
5
+ debug with a built-in UI.
6
6
 
7
- CI runs `.github/workflows/gambit-mirror.yml` on every `main` push that touches
8
- this package, using `scripts/sync-gambit-mirror.sh` to open and auto-merge a PR
9
- from the `bft-codebot` fork into gambitmono.
7
+ ## Quickstart
10
8
 
11
- Gambit helps developers build the most accurate LLM apps by making it simple to
12
- provide exactly the right amount of context at the right time.
9
+ Requirements: Deno 2.2+ and `OPENROUTER_API_KEY` (set `OPENROUTER_BASE_URL` if\
10
+ you proxy OpenRouter-style APIs).
11
+
12
+ Run the CLI directly from JSR (no install):
13
+
14
+ ```
15
+ export OPENROUTER_API_KEY=...
16
+ deno run -A jsr:@bolt-foundry/gambit/cli init
17
+ ```
18
+
19
+ Downloads example files and sets environment variables.
20
+
21
+ Run an example in the terminal (`repl`):
22
+
23
+ ```
24
+ deno run -A jsr:@bolt-foundry/gambit/cli repl examples/hello.deck.md
25
+ ```
26
+
27
+ This example just says "hello" and repeats your message back to you.
28
+
29
+ Run an example in the browser (`serve`):
30
+
31
+ ```
32
+ deno run -A jsr:@bolt-foundry/gambit/cli serve examples/hello.deck.md
33
+ open http://localhost:8000/debug
34
+ ```
35
+
36
+ ---
13
37
 
14
38
  ## Status quo
15
39
 
16
- - Most teams wire one long prompt to several tools and hope the model routes
40
+ - Most teams wire one long prompt to several tools and hope the model routes\
17
41
  correctly.
18
- - Context often arrives as a single giant fetch or RAG blob, so costs climb and
42
+ - Context often arrives as a single giant fetch or RAG blob, so costs climb and\
19
43
  hallucinations slip in.
20
- - Input/outputs are rarely typed, which makes orchestration brittle and hard to
44
+ - Input/outputs are rarely typed, which makes orchestration brittle and hard to\
21
45
  test offline.
22
- - Debugging leans on provider logs instead of local traces, so reproducing
46
+ - Debugging leans on provider logs instead of local traces, so reproducing\
23
47
  failures is slow.
24
48
 
25
49
  ## Our vision
26
50
 
27
- - Treat each step as a small deck with explicit inputs/outputs and guardrails;
51
+ - Treat each step as a small deck with explicit inputs/outputs and guardrails;\
28
52
  model calls are just one kind of action.
29
- - Mix LLM and compute tasks interchangeably and effortlessly inside the same
53
+ - Mix LLM and compute tasks interchangeably and effortlessly inside the same\
30
54
  deck tree.
31
- - Feed models only what they need per step; inject references and cards instead
55
+ - Feed models only what they need per step; inject references and cards instead\
32
56
  of dumping every document.
33
- - Keep orchestration logic local and testable; run decks offline with
57
+ - Keep orchestration logic local and testable; run decks offline with\
34
58
  predictable traces.
35
- - Ship with built-in observability (streaming, REPL, debug UI) so debugging
59
+ - Ship with built-in observability (streaming, REPL, debug UI) so debugging\
36
60
  feels like regular software, not guesswork.
37
61
 
38
- ## 5-minute quickstart
62
+ ---
39
63
 
40
- Requirements: Deno 2.2+ and `OPENROUTER_API_KEY` (set `OPENROUTER_BASE_URL` if
41
- you proxy OpenRouter-style APIs).
64
+ ## Using the CLI
42
65
 
43
- Run the CLI directly from JSR (no install):
66
+ Use the CLI to run decks locally, stream output, and capture traces/state.
44
67
 
45
- ```sh
46
- export OPENROUTER_API_KEY=...
47
- deno run -A jsr:@bolt-foundry/gambit/cli --help
68
+ Install the CLI:
69
+
70
+ ```
71
+ deno install -A -n gambit jsr:@bolt-foundry/gambit/cli
48
72
  ```
49
73
 
50
- Run a packaged example without cloning:
74
+ Run a deck once:
51
75
 
52
- ```sh
53
- export OPENROUTER_API_KEY=...
54
- deno run -A jsr:@bolt-foundry/gambit/cli run --example hello_world.deck.md --init '"hi"'
76
+ ```
77
+ gambit run <deck> --init <json|string> --message <json|string>
55
78
  ```
56
79
 
57
- Run the built-in assistant (from a clone of this repo):
80
+ Drop into a REPL (streams by default):
58
81
 
59
- ```sh
60
- export OPENROUTER_API_KEY=...
61
- deno run -A src/cli.ts run src/decks/gambit-assistant.deck.md --init '"hi"' --stream
82
+ ```
83
+ gambit repl <deck>
84
+ ```
85
+
86
+ Run a persona against a root deck (test bot):
87
+
88
+ ```
89
+ gambit test-bot <root-deck> --test-deck <persona-deck>
62
90
  ```
63
91
 
64
- Talk to it in a REPL (default deck is `src/decks/gambit-assistant.deck.md`):
92
+ Grade a saved session:
65
93
 
66
- ```sh
67
- deno run -A src/cli.ts repl --message '"hello"' --stream --verbose
94
+ ```
95
+ gambit grade <grader-deck> --state <file>
68
96
  ```
69
97
 
70
- Open the debug UI:
98
+ Start the Debug UI server:
71
99
 
72
- ```sh
73
- deno run -A src/cli.ts serve src/decks/gambit-assistant.deck.md --port 8000
74
- open http://localhost:8000/debug
75
100
  ```
101
+ gambit serve <deck> --port 8000
102
+ ```
103
+
104
+ Tracing and state: 
105
+
106
+ `--trace <file>` for JSONL traces\
107
+ `--verbose` to print events\
108
+ `--state <file>` to persist a session.
109
+
110
+ ## Using the Simulator
111
+
112
+ The simulator is the local Debug UI that streams runs and renders traces.
76
113
 
77
- Install the CLI once (uses the published JSR package):
114
+ Install the CLI:
78
115
 
79
- ```sh
116
+ ```
80
117
  deno install -A -n gambit jsr:@bolt-foundry/gambit/cli
81
- gambit run path/to/root.deck.ts --init '"hi"'
82
118
  ```
83
119
 
84
- If you run from a remote URL (e.g., `jsr:@bolt-foundry/gambit/cli`), pass an
85
- explicit deck path; the default REPL deck only exists in a local checkout.
120
+ Start it:
121
+
122
+ ```
123
+ gambit serve <deck> --port 8000
124
+ ```
125
+
126
+ Then open:
127
+
128
+ ```
129
+ http://localhost:8000/
130
+ ```
131
+
132
+ It also serves:
133
+
134
+ ```
135
+ http://localhost:8000/test-bot
136
+ http://localhost:8000/calibrate
137
+ ```
138
+
139
+ The Debug UI shows transcript lanes plus a trace/tools feed. If the deck has an\
140
+ `inputSchema`, the UI renders a schema-driven form with defaults and a raw JSON\
141
+ tab. Local-first state is stored under `.gambit/` (sessions, traces, notes).
142
+
143
+ ## Using the Library
144
+
145
+ Use the library when you want TypeScript decks/cards or custom compute steps.
146
+
147
+ Import the helpers from JSR:
148
+
149
+ ```
150
+ import { defineDeck, defineCard } from "jsr:@bolt-foundry/gambit";
151
+ ```
152
+
153
+ Define `inputSchema`/`outputSchema` with Zod to validate IO, and implement\
154
+ `run`/`execute` for compute decks. To call a child deck from code, use\
155
+ `ctx.spawnAndWait({ path, input })`. Emit structured trace events with\
156
+ `ctx.log(...)`.
157
+
158
+ ---
86
159
 
87
160
  ## Author your first deck
88
161
 
89
- Minimal Markdown deck (model-powered):
162
+ ### Minimal Markdown deck (model-powered):
90
163
 
91
- ```md
164
+ ```
92
165
  +++
93
166
  label = "hello_world"
94
167
 
@@ -102,13 +175,13 @@ You are a concise assistant. Greet the user and echo the input.
102
175
 
103
176
  Run it:
104
177
 
105
- ```sh
178
+ ```
106
179
  deno run -A src/cli.ts run ./hello_world.deck.md --init '"Gambit"' --stream
107
180
  ```
108
181
 
109
- Compute deck in TypeScript (no model call):
182
+ ### Compute deck in TypeScript (no model call):
110
183
 
111
- ```ts
184
+ ```
112
185
  // echo.deck.ts
113
186
  import { defineDeck } from "jsr:@bolt-foundry/gambit";
114
187
  import { z } from "zod";
@@ -125,19 +198,20 @@ export default defineDeck({
125
198
 
126
199
  Run it:
127
200
 
128
- ```sh
201
+ ```
129
202
  deno run -A src/cli.ts run ./echo.deck.ts --init '{"text":"ping"}'
130
203
  ```
131
204
 
132
- Deck with a child action (calls a TypeScript tool):
205
+ ### Deck with a child action (calls a TypeScript tool):
133
206
 
134
- ```md
207
+ ```
135
208
  +++
136
209
  label = "agent_with_time"
137
210
  modelParams = { model = "openai/gpt-4o-mini", temperature = 0 }
138
- actionDecks = [
139
- { name = "get_time", path = "./get_time.deck.ts", description = "Return the current ISO timestamp." },
140
- ]
211
+ [[actionDecks]]
212
+ name = "get_time"
213
+ path = "./get_time.deck.ts"
214
+ description = "Return the current ISO timestamp."
141
215
  +++
142
216
 
143
217
  A tiny agent that calls get_time, then replies with the timestamp and the input.
@@ -145,7 +219,7 @@ A tiny agent that calls get_time, then replies with the timestamp and the input.
145
219
 
146
220
  And the child action:
147
221
 
148
- ```ts
222
+ ```
149
223
  // get_time.deck.ts
150
224
  import { defineDeck } from "jsr:@bolt-foundry/gambit";
151
225
  import { z } from "zod";
@@ -159,78 +233,3 @@ export default defineDeck({
159
233
  },
160
234
  });
161
235
  ```
162
-
163
- ## Repo highlights
164
-
165
- - CLI entry: `src/cli.ts`; runtime: `src/runtime.ts`; definitions: `mod.ts`.
166
- - Examples: `examples/hello_world.deck.md`,
167
- `examples/agent_with_multi_actions/`.
168
- - Debug UI assets: `src/server.ts` (built-in UI now renders schema-driven init
169
- forms beneath the user message box with a raw JSON tab, reconnect button, and
170
- a trace-formatting hook).
171
- - Tests/lint/format: `deno task test`, `deno task lint`, `deno task fmt`;
172
- compile binary: `deno task compile`.
173
- - Docs index: `docs/README.md`; authoring guide: `docs/authoring.md`; prompting
174
- notes: `docs/hourglass.md`; changelog: `CHANGELOG.md`.
175
-
176
- ## Docs
177
-
178
- - Authoring decks/cards: `docs/authoring.md`
179
- - Runtime/guardrails: `docs/runtime.md`
180
- - CLI, REPL, debug UI: `docs/cli.md`
181
- - Examples guide: `docs/examples.md`
182
- - OpenAI Chat Completions compatibility: `docs/openai-compat.md`
183
-
184
- ## OpenAI Chat Completions compatibility
185
-
186
- If you already construct OpenAI Chat Completions-style requests, you can use
187
- Gambit as a drop-in-ish wrapper that applies a deck prompt and can execute only
188
- deck-defined tools (action decks).
189
-
190
- ```ts
191
- import {
192
- chatCompletionsWithDeck,
193
- createOpenRouterProvider,
194
- } from "jsr:@bolt-foundry/gambit";
195
-
196
- const provider = createOpenRouterProvider({
197
- apiKey: Deno.env.get("OPENROUTER_API_KEY")!,
198
- });
199
-
200
- const resp = await chatCompletionsWithDeck({
201
- deckPath: "./path/to/root.deck.md",
202
- modelProvider: provider,
203
- request: {
204
- model: "openai/gpt-4o-mini",
205
- messages: [{ role: "user", content: "hello" }],
206
- },
207
- });
208
-
209
- console.log(resp.choices[0].message);
210
- ```
211
-
212
- ## Handlers (error/busy/idle)
213
-
214
- - Decks may declare `handlers` with `onError`, `onBusy`, and `onIdle`.
215
- `onInterval` is still accepted but deprecated (alias for `onBusy`).
216
- - Busy handler input:
217
- `{kind:"busy", source:{deckPath, actionName}, trigger:{reason:"timeout", elapsedMs}, childInput}`
218
- plus `delayMs`/`repeatMs` knobs.
219
- - Idle handler input:
220
- `{kind:"idle", source:{deckPath}, trigger:{reason:"idle_timeout", elapsedMs}}`
221
- with `delayMs` (and optional `repeatMs` if provided).
222
- - Error handler input: `{kind:"error", source, error:{message}, childInput}` and
223
- should return an envelope `{message?, code?, status?, meta?, payload?}`.
224
- - Example implementations live under `examples/handlers_ts` and
225
- `examples/handlers_md`.
226
- - Debug UI streams handler output in a “status” lane (busy/idle) separate from
227
- assistant turns.
228
-
229
- ## Next steps
230
-
231
- - Swap `modelParams.model` or pass `--model`/`--model-force` to test other
232
- providers.
233
- - Add `actionDecks` to a deck and call child decks; use `spawnAndWait` inside
234
- compute decks.
235
- - Use `--stream` and `--verbose` while iterating; pass `--trace <file>` to
236
- capture JSONL traces.
package/bin/gambit.cjs CHANGED
@@ -7,10 +7,21 @@ const os = require("os");
7
7
  const https = require("https");
8
8
  const http = require("http");
9
9
  const crypto = require("crypto");
10
+ const zlib = require("zlib");
11
+ const { pipeline } = require("stream/promises");
10
12
  const { spawnSync } = require("child_process");
11
13
 
12
14
  const MAX_REDIRECTS = 5;
13
15
 
16
+ const fileExists = async (filePath) => {
17
+ try {
18
+ await fs.promises.access(filePath, fs.constants.F_OK);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ };
24
+
14
25
  const readJson = (filePath) => {
15
26
  const raw = fs.readFileSync(filePath, "utf8");
16
27
  return JSON.parse(raw);
@@ -88,6 +99,17 @@ const downloadFile = async (url, dest) => {
88
99
  await fs.promises.rename(tmpDest, dest);
89
100
  };
90
101
 
102
+ const gunzipFile = async (source, dest) => {
103
+ const tmpDest = `${dest}.tmp-${process.pid}`;
104
+ await fs.promises.mkdir(path.dirname(dest), { recursive: true });
105
+ await pipeline(
106
+ fs.createReadStream(source),
107
+ zlib.createGunzip(),
108
+ fs.createWriteStream(tmpDest, { mode: 0o755 }),
109
+ );
110
+ await fs.promises.rename(tmpDest, dest);
111
+ };
112
+
91
113
  const fetchText = async (url) => {
92
114
  const res = await request(url);
93
115
  const chunks = [];
@@ -139,6 +161,9 @@ const verifyChecksum = async (checksumUrl, assetName, filePath) => {
139
161
 
140
162
  const main = async () => {
141
163
  const version = getPackageVersion();
164
+ if (!process.env.GAMBIT_VERSION) {
165
+ process.env.GAMBIT_VERSION = version;
166
+ }
142
167
  const assetName = process.env.GAMBIT_BINARY_NAME || getPlatformAsset();
143
168
  if (!assetName) {
144
169
  console.error(
@@ -149,38 +174,87 @@ const main = async () => {
149
174
 
150
175
  const baseUrl = process.env.GAMBIT_BINARY_BASE_URL ||
151
176
  `https://github.com/bolt-foundry/gambit/releases/download/v${version}`;
152
- const binaryUrl = process.env.GAMBIT_BINARY_URL || `${baseUrl}/${assetName}`;
177
+ const binaryUrl = process.env.GAMBIT_BINARY_URL ||
178
+ `${baseUrl}/${assetName}.gz`;
153
179
  const checksumUrl = process.env.GAMBIT_BINARY_CHECKSUM_URL ||
154
180
  `${baseUrl}/SHA256SUMS`;
181
+ const downloadName = (() => {
182
+ try {
183
+ return path.posix.basename(new URL(binaryUrl).pathname);
184
+ } catch {
185
+ return path.posix.basename(binaryUrl);
186
+ }
187
+ })();
188
+ const expectsGzip = downloadName.endsWith(".gz");
155
189
 
156
190
  const cacheDir = getCacheDir(version);
157
191
  const binPath = path.join(cacheDir, assetName);
192
+ const archivePath = expectsGzip ? `${binPath}.gz` : binPath;
158
193
  const ensureBinary = async () => {
159
- if (fs.existsSync(binPath)) {
194
+ const hasBin = await fileExists(binPath);
195
+ const hasArchive = await fileExists(archivePath);
196
+ if (!hasBin && expectsGzip && hasArchive) {
160
197
  try {
161
- await verifyChecksum(checksumUrl, assetName, binPath);
198
+ await verifyChecksum(checksumUrl, downloadName, archivePath);
199
+ await gunzipFile(archivePath, binPath);
162
200
  return;
163
201
  } catch (err) {
164
- console.warn(
165
- `Cached binary failed checksum; deleting and re-downloading.`,
166
- );
167
202
  try {
168
- await fs.promises.unlink(binPath);
203
+ await fs.promises.unlink(archivePath);
169
204
  } catch {
170
205
  // ignore
171
206
  }
172
207
  }
173
208
  }
209
+ if (hasBin) {
210
+ if (expectsGzip && !hasArchive) {
211
+ console.warn(`Cached binary missing archive; re-downloading.`);
212
+ } else {
213
+ try {
214
+ await verifyChecksum(checksumUrl, downloadName, archivePath);
215
+ if (expectsGzip) {
216
+ await gunzipFile(archivePath, binPath);
217
+ }
218
+ return;
219
+ } catch (err) {
220
+ console.warn(
221
+ `Cached binary failed checksum; deleting and re-downloading.`,
222
+ );
223
+ try {
224
+ await fs.promises.unlink(binPath);
225
+ } catch {
226
+ // ignore
227
+ }
228
+ if (expectsGzip) {
229
+ try {
230
+ await fs.promises.unlink(archivePath);
231
+ } catch {
232
+ // ignore
233
+ }
234
+ }
235
+ }
236
+ }
237
+ }
174
238
  console.log(`Downloading ${binaryUrl}...`);
175
- await downloadFile(binaryUrl, binPath);
239
+ await downloadFile(binaryUrl, archivePath);
176
240
  try {
177
- await verifyChecksum(checksumUrl, assetName, binPath);
241
+ await verifyChecksum(checksumUrl, downloadName, archivePath);
242
+ if (expectsGzip) {
243
+ await gunzipFile(archivePath, binPath);
244
+ }
178
245
  } catch (err) {
179
246
  try {
180
- await fs.promises.unlink(binPath);
247
+ await fs.promises.unlink(archivePath);
181
248
  } catch {
182
249
  // ignore
183
250
  }
251
+ if (expectsGzip) {
252
+ try {
253
+ await fs.promises.unlink(binPath);
254
+ } catch {
255
+ // ignore
256
+ }
257
+ }
184
258
  throw err;
185
259
  }
186
260
  };