@bandito-ai/sdk 0.1.8 → 0.1.9

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/dist/index.d.mts CHANGED
@@ -70,12 +70,9 @@ declare class BanditoClient {
70
70
  private bandits;
71
71
  private connected;
72
72
  private flushInterval;
73
- private s3FlushInterval;
74
73
  private flushInProgress;
75
74
  private deadUuids;
76
75
  private retryCounts;
77
- private _s3Client;
78
- private _s3Config;
79
76
  constructor(options?: ClientOptions);
80
77
  /**
81
78
  * Bootstrap: authenticate and hydrate in-memory state from cloud.
@@ -108,13 +105,6 @@ declare class BanditoClient {
108
105
  private ensureConnected;
109
106
  private applySync;
110
107
  private flushPending;
111
- /**
112
- * Export un-uploaded events to S3 as raw event JSON. One file per event.
113
- *
114
- * Key pattern: {prefix}/{bandit_name}/{YYYY/MM/DD}/{local_event_uuid}.json
115
- * Body: the raw event object (same structure as SQLite payload column).
116
- */
117
- private dumpPendingToS3;
118
108
  }
119
109
 
120
110
  /**
package/dist/index.d.ts CHANGED
@@ -70,12 +70,9 @@ declare class BanditoClient {
70
70
  private bandits;
71
71
  private connected;
72
72
  private flushInterval;
73
- private s3FlushInterval;
74
73
  private flushInProgress;
75
74
  private deadUuids;
76
75
  private retryCounts;
77
- private _s3Client;
78
- private _s3Config;
79
76
  constructor(options?: ClientOptions);
80
77
  /**
81
78
  * Bootstrap: authenticate and hydrate in-memory state from cloud.
@@ -108,13 +105,6 @@ declare class BanditoClient {
108
105
  private ensureConnected;
109
106
  private applySync;
110
107
  private flushPending;
111
- /**
112
- * Export un-uploaded events to S3 as raw event JSON. One file per event.
113
- *
114
- * Key pattern: {prefix}/{bandit_name}/{YYYY/MM/DD}/{local_event_uuid}.json
115
- * Body: the raw event object (same structure as SQLite payload column).
116
- */
117
- private dumpPendingToS3;
118
108
  }
119
109
 
120
110
  /**
package/dist/index.js CHANGED
@@ -57,13 +57,7 @@ function createEngine(banditJson) {
57
57
  if (!wasmModule) {
58
58
  throw new Error("WASM not initialized \u2014 call initWasm() first");
59
59
  }
60
- const seedBytes = new Uint8Array(8);
61
- globalThis.crypto.getRandomValues(seedBytes);
62
- let seed = 0n;
63
- for (let i = 0; i < 8; i++) {
64
- seed |= BigInt(seedBytes[i]) << BigInt(i * 8);
65
- }
66
- return wasmModule.BanditEngine.newWithSeed(banditJson, seed);
60
+ return new wasmModule.BanditEngine(banditJson);
67
61
  }
68
62
  function updateEngine(engine, banditJson) {
69
63
  engine.updateFromSync(banditJson);
@@ -116,9 +110,7 @@ function loadConfig() {
116
110
  const config = {
117
111
  apiKey: null,
118
112
  baseUrl: DEFAULT_BASE_URL,
119
- dataStorage: "local",
120
- s3: null,
121
- judge: { apiKey: null, model: "gpt-4o-mini" }
113
+ dataStorage: "local"
122
114
  };
123
115
  if (fs.existsSync(CONFIG_FILE)) {
124
116
  try {
@@ -127,20 +119,6 @@ function loadConfig() {
127
119
  if (data.api_key) config.apiKey = data.api_key;
128
120
  if (data.base_url) config.baseUrl = data.base_url;
129
121
  if (data.data_storage) config.dataStorage = data.data_storage;
130
- const s3Data = data.s3;
131
- const s3Bucket = (s3Data?.bucket ?? "").trim();
132
- if (s3Bucket) {
133
- const s3Endpoint = (s3Data?.endpoint ?? "").trim() || void 0;
134
- config.s3 = {
135
- bucket: s3Bucket,
136
- prefix: (s3Data?.prefix ?? "").trim() || "bandito",
137
- region: (s3Data?.region ?? "").trim() || "us-east-1",
138
- ...s3Endpoint ? { endpoint: s3Endpoint } : {}
139
- };
140
- }
141
- const judgeData = data.judge;
142
- if (judgeData?.api_key) config.judge.apiKey = judgeData.api_key;
143
- if (judgeData?.model) config.judge.model = judgeData.model;
144
122
  } catch {
145
123
  }
146
124
  }
@@ -150,28 +128,6 @@ function loadConfig() {
150
128
  if (envUrl) config.baseUrl = envUrl;
151
129
  const envStorage = process.env.BANDITO_DATA_STORAGE;
152
130
  if (envStorage) config.dataStorage = envStorage;
153
- const envS3Bucket = (process.env.BANDITO_S3_BUCKET ?? "").trim();
154
- if (envS3Bucket) {
155
- const envEndpoint = (process.env.BANDITO_S3_ENDPOINT ?? config.s3?.endpoint ?? "").trim() || void 0;
156
- config.s3 = {
157
- bucket: envS3Bucket,
158
- prefix: (process.env.BANDITO_S3_PREFIX ?? config.s3?.prefix ?? "").trim() || "bandito",
159
- region: (process.env.BANDITO_S3_REGION ?? config.s3?.region ?? "").trim() || "us-east-1",
160
- ...envEndpoint ? { endpoint: envEndpoint } : {}
161
- };
162
- if (!process.env.BANDITO_DATA_STORAGE) {
163
- config.dataStorage = "s3";
164
- }
165
- } else if (config.s3) {
166
- const envPrefix = (process.env.BANDITO_S3_PREFIX ?? "").trim();
167
- const envRegion = (process.env.BANDITO_S3_REGION ?? "").trim();
168
- const envEndpoint = (process.env.BANDITO_S3_ENDPOINT ?? "").trim();
169
- if (envPrefix) config.s3.prefix = envPrefix;
170
- if (envRegion) config.s3.region = envRegion;
171
- if (envEndpoint) config.s3.endpoint = envEndpoint;
172
- }
173
- const envJudgeKey = process.env.JUDGE_API_KEY;
174
- if (envJudgeKey) config.judge.apiKey = envJudgeKey;
175
131
  return config;
176
132
  }
177
133
 
@@ -189,17 +145,6 @@ var BanditoHTTP = class {
189
145
  apiKey;
190
146
  timeout;
191
147
  constructor(baseUrl, apiKey, timeout = 1e4) {
192
- let parsed;
193
- try {
194
- parsed = new URL(baseUrl);
195
- } catch {
196
- throw new Error(`Invalid baseUrl: "${baseUrl}"`);
197
- }
198
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
199
- throw new Error(
200
- `baseUrl must use http or https, got "${parsed.protocol}" \u2014 check your config`
201
- );
202
- }
203
148
  this.baseUrl = `${baseUrl.replace(/\/$/, "")}/api/v1`;
204
149
  this.apiKey = apiKey;
205
150
  this.timeout = timeout;
@@ -222,14 +167,13 @@ var BanditoHTTP = class {
222
167
  clearTimeout(timer);
223
168
  if (!resp.ok) {
224
169
  const text = await resp.text().catch(() => "");
225
- const preview = text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
226
170
  if (!isRetryable(resp.status) || attempt === MAX_RETRIES - 1) {
227
171
  throw new Error(
228
- `HTTP ${resp.status} on ${method} ${path3}: ${preview}`
172
+ `HTTP ${resp.status} on ${method} ${path3}: ${text}`
229
173
  );
230
174
  }
231
175
  lastError = new Error(
232
- `HTTP ${resp.status} on ${method} ${path3}: ${preview}`
176
+ `HTTP ${resp.status} on ${method} ${path3}: ${text}`
233
177
  );
234
178
  } else {
235
179
  return await resp.json();
@@ -282,15 +226,13 @@ CREATE TABLE IF NOT EXISTS events (
282
226
  status TEXT NOT NULL DEFAULT 'pending',
283
227
  created_at REAL NOT NULL,
284
228
  human_reward REAL,
285
- graded_at REAL,
286
- s3_exported INTEGER NOT NULL DEFAULT 0
229
+ graded_at REAL
287
230
  );
288
231
  CREATE INDEX IF NOT EXISTS idx_events_status ON events(status);
289
232
  `;
290
233
  var MIGRATION_GRADING = [
291
234
  "ALTER TABLE events ADD COLUMN human_reward REAL",
292
- "ALTER TABLE events ADD COLUMN graded_at REAL",
293
- "ALTER TABLE events ADD COLUMN s3_exported INTEGER NOT NULL DEFAULT 0"
235
+ "ALTER TABLE events ADD COLUMN graded_at REAL"
294
236
  ];
295
237
  var EventStore = class {
296
238
  db;
@@ -309,7 +251,7 @@ var EventStore = class {
309
251
  VALUES (?, ?, ?, ?, 'pending', ?)`
310
252
  );
311
253
  this.pendingStmt = this.db.prepare(
312
- `SELECT local_event_uuid, payload FROM events WHERE status = 'pending'
254
+ `SELECT payload FROM events WHERE status = 'pending'
313
255
  ORDER BY created_at ASC LIMIT ?`
314
256
  );
315
257
  }
@@ -326,22 +268,7 @@ var EventStore = class {
326
268
  /** Return up to `limit` pending events (oldest first). */
327
269
  pending(limit = 50) {
328
270
  const rows = this.pendingStmt.all(limit);
329
- const results = [];
330
- const corrupt = [];
331
- for (const row of rows) {
332
- try {
333
- results.push(JSON.parse(row.payload));
334
- } catch {
335
- console.warn(
336
- `[bandito] Discarding corrupt event ${row.local_event_uuid} from store`
337
- );
338
- corrupt.push(row.local_event_uuid);
339
- }
340
- }
341
- if (corrupt.length > 0) {
342
- this.markFlushed(corrupt);
343
- }
344
- return results;
271
+ return rows.map((row) => JSON.parse(row.payload));
345
272
  }
346
273
  /** Mark events as successfully flushed to cloud. */
347
274
  markFlushed(uuids) {
@@ -359,20 +286,6 @@ var EventStore = class {
359
286
  WHERE local_event_uuid = ?`
360
287
  ).run(reward, Date.now() / 1e3, uuid);
361
288
  }
362
- /** Return un-exported events as {event, ts} for S3 dump. */
363
- pendingS3(limit = 100) {
364
- const rows = this.db.prepare(
365
- `SELECT payload, created_at FROM events WHERE s3_exported = 0
366
- ORDER BY created_at ASC LIMIT ?`
367
- ).all(limit);
368
- return rows.map((r) => ({ event: JSON.parse(r.payload), ts: r.created_at }));
369
- }
370
- /** Mark events as successfully exported to S3. */
371
- markS3Exported(uuids) {
372
- if (uuids.length === 0) return;
373
- const placeholders = uuids.map(() => "?").join(",");
374
- this.db.prepare(`UPDATE events SET s3_exported = 1 WHERE local_event_uuid IN (${placeholders})`).run(...uuids);
375
- }
376
289
  /** Close the database connection. */
377
290
  close() {
378
291
  this.db.close();
@@ -420,14 +333,9 @@ var BanditoClient = class {
420
333
  bandits = /* @__PURE__ */ new Map();
421
334
  connected = false;
422
335
  flushInterval = null;
423
- s3FlushInterval = null;
424
336
  flushInProgress = false;
425
337
  deadUuids = /* @__PURE__ */ new Set();
426
338
  retryCounts = /* @__PURE__ */ new Map();
427
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
428
- _s3Client = null;
429
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
430
- _s3Config = null;
431
339
  constructor(options = {}) {
432
340
  this.apiKey = options.apiKey;
433
341
  this.baseUrl = options.baseUrl;
@@ -466,22 +374,6 @@ var BanditoClient = class {
466
374
  }
467
375
  }
468
376
  this.store = new EventStore(storePath);
469
- this._s3Client = null;
470
- this._s3Config = null;
471
- if (this.dataStorage === "s3" && config.s3) {
472
- try {
473
- const { S3Client } = await import("@aws-sdk/client-s3");
474
- const s3ClientConfig = { region: config.s3.region };
475
- if (config.s3.endpoint) {
476
- s3ClientConfig.endpoint = config.s3.endpoint;
477
- s3ClientConfig.forcePathStyle = true;
478
- }
479
- this._s3Client = new S3Client(s3ClientConfig);
480
- this._s3Config = config.s3;
481
- } catch {
482
- console.warn("[bandito] data_storage='s3' requires @aws-sdk/client-s3");
483
- }
484
- }
485
377
  try {
486
378
  const data = await this.http.connect();
487
379
  this.applySync(data);
@@ -489,17 +381,9 @@ var BanditoClient = class {
489
381
  this.retryCounts.clear();
490
382
  await this.flushPending();
491
383
  this.flushInterval = setInterval(() => {
492
- this.flushPending().catch(
493
- (err) => console.warn("[bandito] Periodic flush error", err)
494
- );
384
+ this.flushPending().catch(() => {
385
+ });
495
386
  }, 3e4);
496
- if (this._s3Client !== null) {
497
- this.s3FlushInterval = setInterval(() => {
498
- this.dumpPendingToS3().catch(
499
- (err) => console.warn("[bandito] Periodic S3 export error", err)
500
- );
501
- }, 3e4);
502
- }
503
387
  this.connected = true;
504
388
  } catch (err) {
505
389
  this.store?.close();
@@ -562,8 +446,7 @@ var BanditoClient = class {
562
446
  bandit_id: pullResult.banditId,
563
447
  arm_id: pullResult.arm.armId,
564
448
  model_name: pullResult.arm.modelName,
565
- model_provider: pullResult.arm.modelProvider,
566
- bandit_name: pullResult.banditName
449
+ model_provider: pullResult.arm.modelProvider
567
450
  };
568
451
  if (options.queryText != null) {
569
452
  event.query_text = options.queryText;
@@ -593,9 +476,8 @@ var BanditoClient = class {
593
476
  event.run_error = true;
594
477
  }
595
478
  this.store.push(event);
596
- this.flushPending().catch(
597
- (err) => console.warn("[bandito] Flush error", err)
598
- );
479
+ this.flushPending().catch(() => {
480
+ });
599
481
  }
600
482
  /**
601
483
  * Send a human grade for an existing event. Async (HTTP).
@@ -628,16 +510,9 @@ var BanditoClient = class {
628
510
  clearInterval(this.flushInterval);
629
511
  this.flushInterval = null;
630
512
  }
631
- if (this.s3FlushInterval) {
632
- clearInterval(this.s3FlushInterval);
633
- this.s3FlushInterval = null;
634
- }
635
513
  if (this.store && this.http) {
636
514
  await this.flushPending();
637
515
  }
638
- if (this._s3Client !== null && this.store) {
639
- await this.dumpPendingToS3();
640
- }
641
516
  this.store?.close();
642
517
  this.store = null;
643
518
  this.http = null;
@@ -717,45 +592,6 @@ var BanditoClient = class {
717
592
  this.flushInProgress = false;
718
593
  }
719
594
  }
720
- /**
721
- * Export un-uploaded events to S3 as raw event JSON. One file per event.
722
- *
723
- * Key pattern: {prefix}/{bandit_name}/{YYYY/MM/DD}/{local_event_uuid}.json
724
- * Body: the raw event object (same structure as SQLite payload column).
725
- */
726
- async dumpPendingToS3() {
727
- const eventsWithTs = this.store.pendingS3(100);
728
- if (eventsWithTs.length === 0) return;
729
- const { PutObjectCommand } = await import("@aws-sdk/client-s3");
730
- const uploaded = [];
731
- try {
732
- for (const { event, ts } of eventsWithTs) {
733
- const uuid = event.local_event_uuid;
734
- const rawName = event.bandit_name || "unknown";
735
- const banditName = rawName.replace(/[^\w\-.]/g, "_");
736
- const date = new Date(ts * 1e3);
737
- const datePath = [
738
- date.getUTCFullYear(),
739
- String(date.getUTCMonth() + 1).padStart(2, "0"),
740
- String(date.getUTCDate()).padStart(2, "0")
741
- ].join("/");
742
- const key = `${this._s3Config.prefix}/${banditName}/${datePath}/${uuid}.json`;
743
- await this._s3Client.send(new PutObjectCommand({
744
- Bucket: this._s3Config.bucket,
745
- Key: key,
746
- Body: JSON.stringify(event),
747
- ContentType: "application/json"
748
- }));
749
- uploaded.push(uuid);
750
- }
751
- } catch (err) {
752
- console.warn("[bandito] S3 export failed \u2014 will retry", err);
753
- } finally {
754
- if (uploaded.length > 0) {
755
- this.store.markS3Exported(uploaded);
756
- }
757
- }
758
- }
759
595
  };
760
596
 
761
597
  // src/index.ts
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/engine.ts","../src/models.ts","../src/config.ts","../src/http.ts","../src/store.ts","../src/worker.ts"],"sourcesContent":["/**\n * Bandito SDK — contextual bandit optimization for LLM selection.\n *\n * Recommended (explicit client):\n * import { BanditoClient } from 'bandito';\n *\n * const client = new BanditoClient({ apiKey: 'bnd_...' });\n * await client.connect();\n * const result = client.pull('my-chatbot', { query: userMessage });\n * // ... call LLM with result.model, result.prompt ...\n * client.update(result, { response: response.text });\n * await client.close();\n *\n * Module-level singleton (convenience):\n * import { connect, pull, update, close } from 'bandito';\n *\n * await connect({ apiKey: 'bnd_...' });\n * const result = pull('my-chatbot', { query: userMessage });\n * update(result, { response: response.text });\n * await close();\n */\n\nexport { BanditoClient, type ClientOptions, type PullOptions, type UpdateOptions } from \"./client.js\";\nexport { type Arm, type PullResult } from \"./models.js\";\n\nimport { BanditoClient, type ClientOptions, type PullOptions, type UpdateOptions } from \"./client.js\";\nimport type { PullResult } from \"./models.js\";\n\nlet _client: BanditoClient | null = null;\n\nfunction getClient(): BanditoClient {\n if (!_client) {\n throw new Error(\"Not connected — call connect() first\");\n }\n return _client;\n}\n\n/** Connect to the Bandito cloud and hydrate local state. */\nexport async function connect(options: ClientOptions = {}): Promise<void> {\n if (_client) {\n await _client.close();\n }\n _client = new BanditoClient(options);\n await _client.connect();\n}\n\n/** Local Thompson Sampling decision. <1ms, no network. */\nexport function pull(banditName: string, options?: PullOptions): PullResult {\n return getClient().pull(banditName, options);\n}\n\n/** Record an LLM call outcome (writes to SQLite, fire-and-forget flush). */\nexport function update(pullResult: PullResult, options?: UpdateOptions): void {\n getClient().update(pullResult, options);\n}\n\n/** Send a human grade for an existing event. */\nexport async function grade(eventId: string, gradeValue: number): Promise<void> {\n await getClient().grade(eventId, gradeValue);\n}\n\n/** Explicit state refresh from cloud. */\nexport async function sync(): Promise<void> {\n await getClient().sync();\n}\n\n/** Shut down: flush events, close connections. */\nexport async function close(): Promise<void> {\n if (_client) {\n await _client.close();\n _client = null;\n }\n}\n","/**\n * BanditoClient — main orchestrator for the JS/TS SDK.\n *\n * Mirrors the Python SDK's sync-first design:\n * - pull() is synchronous (WASM math, <1ms)\n * - connect(), grade(), sync(), close() are async (HTTP I/O)\n * - update() is synchronous (SQLite write + fire-and-forget flush)\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { performance } from \"node:perf_hooks\";\n\nimport { initWasm, createEngine, updateEngine, type BanditEngine, type EnginePullResult } from \"./engine.js\";\nimport {\n type Arm,\n type PullResult,\n type BanditCache,\n type ArmWire,\n createArm,\n createPullResult,\n} from \"./models.js\";\nimport { loadConfig, DEFAULT_BASE_URL } from \"./config.js\";\nimport { BanditoHTTP } from \"./http.js\";\nimport { EventStore, type EventPayload } from \"./store.js\";\nimport { prepareCloudPayload } from \"./worker.js\";\n\nconst DEFAULT_STORE_PATH = path.join(os.homedir(), \".bandito\", \"events.db\");\nconst MAX_EVENT_RETRIES = 5;\n\nexport interface ClientOptions {\n apiKey?: string;\n baseUrl?: string;\n storePath?: string;\n dataStorage?: string;\n}\n\nexport interface PullOptions {\n query?: string;\n exclude?: number[];\n}\n\nexport interface UpdateOptions {\n queryText?: string;\n response?: string | Record<string, unknown>;\n reward?: number;\n cost?: number;\n latency?: number;\n inputTokens?: number;\n outputTokens?: number;\n segment?: Record<string, string>;\n failed?: boolean;\n}\n\nexport class BanditoClient {\n private apiKey: string | undefined;\n private baseUrl: string | undefined;\n private storePath: string | undefined;\n private dataStorageArg: string | undefined;\n private dataStorage: string;\n\n private http: BanditoHTTP | null = null;\n private store: EventStore | null = null;\n private engines: Map<string, BanditEngine> = new Map();\n private bandits: Map<string, BanditCache> = new Map();\n private connected = false;\n private flushInterval: ReturnType<typeof setInterval> | null = null;\n private s3FlushInterval: ReturnType<typeof setInterval> | null = null;\n private flushInProgress = false;\n private deadUuids: Set<string> = new Set();\n private retryCounts: Map<string, number> = new Map();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private _s3Client: any = null;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private _s3Config: any = null;\n\n constructor(options: ClientOptions = {}) {\n this.apiKey = options.apiKey;\n this.baseUrl = options.baseUrl;\n this.storePath = options.storePath;\n this.dataStorageArg = options.dataStorage;\n this.dataStorage = options.dataStorage ?? \"local\";\n }\n\n /**\n * Bootstrap: authenticate and hydrate in-memory state from cloud.\n *\n * Resolves config from: constructor args → env vars → ~/.bandito/config.toml.\n * Initializes WASM, creates HTTP client, SQLite store, fetches full state.\n */\n async connect(): Promise<void> {\n // Tear down previous connection if reconnecting\n if (this.connected) {\n await this.close();\n }\n\n // Init WASM (loads .wasm binary once)\n await initWasm();\n\n // Resolve config\n const config = loadConfig();\n const apiKey = this.apiKey ?? config.apiKey;\n if (!apiKey) {\n throw new Error(\n \"apiKey required — pass it to constructor, set BANDITO_API_KEY, \" +\n \"or run `bandito signup`\",\n );\n }\n\n const baseUrl = this.baseUrl ?? config.baseUrl;\n if (!this.dataStorageArg) {\n this.dataStorage = config.dataStorage;\n }\n\n this.http = new BanditoHTTP(baseUrl, apiKey);\n const storePath = this.storePath ?? DEFAULT_STORE_PATH;\n if (storePath !== \":memory:\") {\n const dir = path.dirname(storePath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n }\n this.store = new EventStore(storePath);\n\n // Init S3 exporter if data_storage is \"s3\"\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this._s3Client = null as any;\n this._s3Config = null;\n if (this.dataStorage === \"s3\" && config.s3) {\n try {\n const { S3Client } = await import(\"@aws-sdk/client-s3\");\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const s3ClientConfig: Record<string, any> = { region: config.s3.region };\n if (config.s3.endpoint) {\n s3ClientConfig.endpoint = config.s3.endpoint;\n s3ClientConfig.forcePathStyle = true; // required for MinIO / path-style S3\n }\n this._s3Client = new S3Client(s3ClientConfig);\n this._s3Config = config.s3;\n } catch {\n console.warn(\"[bandito] data_storage='s3' requires @aws-sdk/client-s3\");\n }\n }\n\n // Bootstrap: fetch state, hydrate cache, flush pending\n try {\n const data = await this.http.connect();\n this.applySync(data);\n\n // Reset retry state\n this.deadUuids.clear();\n this.retryCounts.clear();\n\n // Flush pending events from previous crash\n await this.flushPending();\n\n // Start periodic cloud flush (every 30s)\n this.flushInterval = setInterval(() => {\n this.flushPending().catch((err) =>\n console.warn(\"[bandito] Periodic flush error\", err),\n );\n }, 30_000);\n\n // S3 dumps run on a separate 30s timer, independent of cloud flush\n if (this._s3Client !== null) {\n this.s3FlushInterval = setInterval(() => {\n this.dumpPendingToS3().catch((err) =>\n console.warn(\"[bandito] Periodic S3 export error\", err),\n );\n }, 30_000);\n }\n\n this.connected = true;\n } catch (err) {\n this.store?.close();\n this.store = null;\n this.http = null;\n throw err;\n }\n }\n\n /**\n * Local Thompson Sampling decision. Synchronous, <1ms, no network.\n */\n pull(banditName: string, options: PullOptions = {}): PullResult {\n this.ensureConnected();\n\n const cache = this.bandits.get(banditName);\n if (!cache) {\n const available = [...this.bandits.keys()];\n throw new Error(\n `Unknown bandit '${banditName}'. Available: [${available.join(\", \")}]`,\n );\n }\n\n if (cache.arms.length === 0) {\n throw new Error(`Bandit '${banditName}' has no active arms`);\n }\n\n const engine = this.engines.get(banditName)!;\n const queryLength = options.query?.length ?? undefined;\n const excludeIds = options.exclude ? Int32Array.from(options.exclude) : undefined;\n\n const resultJson = engine.pull(queryLength, excludeIds);\n const raw: EnginePullResult = JSON.parse(resultJson);\n\n // Look up the winning arm from our cached active arms\n const winnerArm = cache.arms.find((a) => a.armId === raw.arm_id);\n if (!winnerArm) {\n throw new Error(\n `Engine selected arm ${raw.arm_id} but it's not in active arm cache for \"${banditName}\". ` +\n \"This is likely a bug — please report it at https://github.com/bandito-ai/bandito/issues\",\n );\n }\n\n return createPullResult({\n arm: winnerArm,\n eventId: randomUUID(),\n banditId: cache.banditId,\n banditName,\n scores: raw.scores,\n pullTime: performance.now(),\n });\n }\n\n /**\n * Record an LLM call outcome. Writes to SQLite first (crash-safe),\n * then fires off a non-blocking flush to cloud.\n */\n update(pullResult: PullResult, options: UpdateOptions = {}): void {\n this.ensureConnected();\n\n let reward = options.reward;\n if (options.failed && reward == null) {\n reward = 0.0;\n }\n\n // Auto-calculate latency from pull timestamp\n let latency = options.latency;\n if (latency == null && pullResult._pullTime > 0) {\n latency = performance.now() - pullResult._pullTime;\n }\n\n // Build event payload (snake_case for wire format)\n const event: EventPayload = {\n local_event_uuid: pullResult.eventId,\n bandit_id: pullResult.banditId,\n arm_id: pullResult.arm.armId,\n model_name: pullResult.arm.modelName,\n model_provider: pullResult.arm.modelProvider,\n bandit_name: pullResult.banditName,\n };\n\n if (options.queryText != null) {\n (event as Record<string, unknown>).query_text = options.queryText;\n }\n if (options.response != null) {\n (event as Record<string, unknown>).response =\n typeof options.response === \"string\"\n ? { response: options.response }\n : options.response;\n }\n if (reward != null) {\n (event as Record<string, unknown>).early_reward = reward;\n }\n if (options.cost != null) {\n (event as Record<string, unknown>).cost = options.cost;\n }\n if (latency != null) {\n (event as Record<string, unknown>).latency = latency;\n }\n if (options.inputTokens != null) {\n (event as Record<string, unknown>).input_tokens = options.inputTokens;\n }\n if (options.outputTokens != null) {\n (event as Record<string, unknown>).output_tokens = options.outputTokens;\n }\n if (options.segment != null) {\n (event as Record<string, unknown>).segment = options.segment;\n }\n if (options.failed) {\n (event as Record<string, unknown>).run_error = true;\n }\n\n // Write to SQLite WAL first — survives crashes\n this.store!.push(event);\n\n // Fire-and-forget flush (errors logged inside flushPending)\n this.flushPending().catch((err) =>\n console.warn(\"[bandito] Flush error\", err),\n );\n }\n\n /**\n * Send a human grade for an existing event. Async (HTTP).\n */\n async grade(eventId: string, grade: number): Promise<void> {\n this.ensureConnected();\n await this.http!.submitGrade(eventId, grade);\n }\n\n /**\n * Explicit state refresh from cloud.\n */\n async sync(): Promise<void> {\n this.ensureConnected();\n const data = await this.http!.heartbeat();\n\n const prevBandits = new Map(this.bandits);\n const prevEngines = new Map(this.engines);\n try {\n this.applySync(data);\n } catch (err) {\n // Rollback on malformed response — keep last-known-good state\n this.bandits = prevBandits;\n this.engines = prevEngines;\n console.warn(\"[bandito] Sync response malformed — keeping last-known-good state\", err);\n }\n }\n\n /**\n * Shut down: clear interval, flush remaining events, close connections.\n */\n async close(): Promise<void> {\n if (this.flushInterval) {\n clearInterval(this.flushInterval);\n this.flushInterval = null;\n }\n\n if (this.s3FlushInterval) {\n clearInterval(this.s3FlushInterval);\n this.s3FlushInterval = null;\n }\n\n // Final cloud flush\n if (this.store && this.http) {\n await this.flushPending();\n }\n\n // Final S3 drain — catches events not yet exported by the periodic timer\n if (this._s3Client !== null && this.store) {\n await this.dumpPendingToS3();\n }\n\n this.store?.close();\n this.store = null;\n this.http = null;\n this.engines.clear();\n this.bandits.clear();\n this.connected = false;\n }\n\n // --- Internal ---\n\n private ensureConnected(): void {\n if (!this.connected) {\n throw new Error(\"Not connected — call connect() first\");\n }\n }\n\n private applySync(data: Record<string, unknown>): void {\n const banditsData = (data.bandits ?? []) as Record<string, unknown>[];\n\n const newBandits = new Map<string, BanditCache>();\n const newEngines = new Map<string, BanditEngine>();\n\n for (const b of banditsData) {\n const arms = (b.arms ?? []) as ArmWire[];\n if (arms.length === 0) continue;\n\n const activeArms: Arm[] = arms\n .filter((a) => a.is_active)\n .map((a) => createArm(a));\n\n const name = b.name as string;\n const cache: BanditCache = {\n banditId: Number(b.bandit_id),\n name,\n arms: activeArms,\n armWire: arms,\n optimizationMode: (b.optimization_mode as string) ?? \"base\",\n avgLatencyLastN: b.avg_latency_last_n as number | null,\n budget: b.budget as number | null,\n totalCost: b.total_cost as number | null,\n };\n\n newBandits.set(name, cache);\n\n // Reuse existing engine (preserves RNG state) or create new one\n const existingEngine = this.engines.get(name);\n const banditJson = JSON.stringify(b);\n if (existingEngine) {\n updateEngine(existingEngine, banditJson);\n newEngines.set(name, existingEngine);\n } else {\n newEngines.set(name, createEngine(banditJson));\n }\n }\n\n this.bandits = newBandits;\n this.engines = newEngines;\n }\n\n private async flushPending(): Promise<void> {\n if (this.flushInProgress || !this.store || !this.http) return;\n this.flushInProgress = true;\n\n try {\n const pending = this.store.pending();\n if (pending.length === 0) return;\n\n // Filter out dead events\n const alive = this.deadUuids.size > 0\n ? pending.filter((e) => !this.deadUuids.has(e.local_event_uuid))\n : pending;\n if (alive.length === 0) return;\n\n // Prepare payload (strip metadata/text as configured)\n const payload = prepareCloudPayload(\n alive as Record<string, unknown>[],\n this.dataStorage !== \"local\",\n );\n\n const result = await this.http.ingestEvents(payload);\n\n // Parse per-event errors\n const errors = (result.errors ?? []) as {\n local_event_uuid?: string;\n reason?: string;\n }[];\n const erroredUuids = new Set(\n errors.map((e) => e.local_event_uuid).filter(Boolean) as string[],\n );\n\n // Update retry counts\n for (const uid of erroredUuids) {\n const count = (this.retryCounts.get(uid) ?? 0) + 1;\n this.retryCounts.set(uid, count);\n if (count >= MAX_EVENT_RETRIES) {\n this.deadUuids.add(uid);\n }\n }\n\n // Mark accepted events as flushed\n const flushedUuids = alive\n .map((e) => e.local_event_uuid)\n .filter((uid) => !erroredUuids.has(uid));\n if (flushedUuids.length > 0 && this.store) {\n this.store.markFlushed(flushedUuids);\n }\n\n } catch (err) {\n // Flush failure is non-fatal — events stay pending for next attempt\n console.warn(\"[bandito] Event flush failed — will retry\", err);\n } finally {\n this.flushInProgress = false;\n }\n }\n\n /**\n * Export un-uploaded events to S3 as raw event JSON. One file per event.\n *\n * Key pattern: {prefix}/{bandit_name}/{YYYY/MM/DD}/{local_event_uuid}.json\n * Body: the raw event object (same structure as SQLite payload column).\n */\n private async dumpPendingToS3(): Promise<void> {\n const eventsWithTs = this.store!.pendingS3(100);\n if (eventsWithTs.length === 0) return;\n\n const { PutObjectCommand } = await import(\"@aws-sdk/client-s3\");\n const uploaded: string[] = [];\n\n try {\n for (const { event, ts } of eventsWithTs) {\n const uuid = event.local_event_uuid as string;\n const rawName = (event.bandit_name as string | undefined) || \"unknown\";\n const banditName = rawName.replace(/[^\\w\\-.]/g, \"_\");\n const date = new Date(ts * 1000);\n const datePath = [\n date.getUTCFullYear(),\n String(date.getUTCMonth() + 1).padStart(2, \"0\"),\n String(date.getUTCDate()).padStart(2, \"0\"),\n ].join(\"/\");\n const key = `${this._s3Config.prefix}/${banditName}/${datePath}/${uuid}.json`;\n\n await this._s3Client.send(new PutObjectCommand({\n Bucket: this._s3Config.bucket,\n Key: key,\n Body: JSON.stringify(event),\n ContentType: \"application/json\",\n }));\n uploaded.push(uuid);\n }\n } catch (err) {\n console.warn(\"[bandito] S3 export failed — will retry\", err);\n } finally {\n if (uploaded.length > 0) {\n this.store!.markS3Exported(uploaded);\n }\n }\n }\n}\n","/**\n * Thin wrapper around the WASM engine import.\n *\n * Handles async WASM initialization (loading .wasm binary happens once)\n * and re-exports the BanditEngine constructor for the client.\n */\n\nimport type { BanditEngine as WasmBanditEngine } from \"../wasm/bandito_engine\";\n\nlet wasmModule: typeof import(\"../wasm/bandito_engine\") | null = null;\n\n/**\n * Initialize the WASM module. Must be called before creating BanditEngine instances.\n * Safe to call multiple times — only loads once.\n */\nexport async function initWasm(): Promise<void> {\n if (wasmModule) return;\n wasmModule = await import(\"../wasm/bandito_engine\");\n}\n\n/**\n * Create a BanditEngine from a sync response JSON string.\n * Seeds the RNG with OS entropy to prevent predictable arm selection.\n * Requires initWasm() to have been called first.\n */\nexport function createEngine(banditJson: string): WasmBanditEngine {\n if (!wasmModule) {\n throw new Error(\"WASM not initialized — call initWasm() first\");\n }\n // Generate 8 bytes of OS entropy and pack into a u64 BigInt seed\n const seedBytes = new Uint8Array(8);\n globalThis.crypto.getRandomValues(seedBytes);\n let seed = 0n;\n for (let i = 0; i < 8; i++) {\n seed |= BigInt(seedBytes[i]) << BigInt(i * 8);\n }\n return wasmModule.BanditEngine.newWithSeed(banditJson, seed);\n}\n\n\n/**\n * Update an existing BanditEngine with new sync response data.\n * Preserves RNG state (avoids the \"always picks same arm\" bug).\n */\nexport function updateEngine(engine: WasmBanditEngine, banditJson: string): void {\n engine.updateFromSync(banditJson);\n}\n\nexport type { WasmBanditEngine as BanditEngine };\n\n/**\n * Pull result parsed from engine JSON output.\n */\nexport interface EnginePullResult {\n arm_index: number;\n arm_id: number;\n scores: Record<number, number>;\n}\n","/**\n * SDK types: Arm, PullResult, and internal cache structures.\n */\n\n/** An arm returned to the user after pull(). Immutable. */\nexport interface Arm {\n readonly armId: number;\n readonly modelName: string;\n readonly modelProvider: string;\n readonly systemPrompt: string;\n readonly isPromptTemplated: boolean;\n /** Convenience alias for modelName. */\n readonly model: string;\n /** Convenience alias for systemPrompt. */\n readonly prompt: string;\n}\n\n/** Returned by pull(), passed to update(). Immutable. */\nexport interface PullResult {\n readonly arm: Arm;\n readonly eventId: string;\n readonly banditId: number;\n readonly banditName: string;\n readonly scores: Readonly<Record<number, number>>;\n /** Convenience reach-through to arm.modelName. */\n readonly model: string;\n /** Convenience reach-through to arm.systemPrompt. */\n readonly prompt: string;\n /** @internal perf timestamp */\n readonly _pullTime: number;\n}\n\n/** Create a frozen Arm from raw wire data. */\nexport function createArm(data: {\n arm_id: number;\n model_name: string;\n model_provider: string;\n system_prompt: string;\n is_prompt_templated?: boolean;\n}): Arm {\n const arm: Arm = {\n armId: data.arm_id,\n modelName: data.model_name,\n modelProvider: data.model_provider,\n systemPrompt: data.system_prompt,\n isPromptTemplated: data.is_prompt_templated ?? false,\n get model() {\n return this.modelName;\n },\n get prompt() {\n return this.systemPrompt;\n },\n };\n return Object.freeze(arm);\n}\n\n/** Create a frozen PullResult. */\nexport function createPullResult(data: {\n arm: Arm;\n eventId: string;\n banditId: number;\n banditName: string;\n scores: Record<number, number>;\n pullTime: number;\n}): PullResult {\n const result: PullResult = {\n arm: data.arm,\n eventId: data.eventId,\n banditId: data.banditId,\n banditName: data.banditName,\n scores: Object.freeze({ ...data.scores }),\n get model() {\n return this.arm.modelName;\n },\n get prompt() {\n return this.arm.systemPrompt;\n },\n _pullTime: data.pullTime,\n };\n return Object.freeze(result);\n}\n\n/** Raw arm data from sync response (snake_case wire format). */\nexport interface ArmWire {\n arm_id: number;\n model_name: string;\n model_provider: string;\n system_prompt: string;\n is_prompt_templated: boolean;\n is_active: boolean;\n avg_latency_last_n: number | null;\n}\n\n/** Internal mutable cache for a bandit's state. */\nexport interface BanditCache {\n banditId: number;\n name: string;\n arms: Arm[]; // active only\n armWire: ArmWire[]; // all arms (for engine JSON)\n optimizationMode: string;\n avgLatencyLastN: number | null;\n budget: number | null;\n totalCost: number | null;\n}\n","/**\n * Config loader — reads ~/.bandito/config.toml and env vars.\n *\n * Priority order (highest wins): constructor args → env vars → TOML → defaults.\n * Same file as the Python SDK uses.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { parse as parseToml } from \"smol-toml\";\n\nexport const DEFAULT_BASE_URL = \"https://bandito-api.onrender.com\";\nconst CONFIG_DIR = path.join(os.homedir(), \".bandito\");\nconst CONFIG_FILE = path.join(CONFIG_DIR, \"config.toml\");\n\nexport interface S3Config {\n bucket: string;\n prefix: string;\n region: string;\n /** Optional custom endpoint for MinIO / LocalStack / other S3-compatible stores. */\n endpoint?: string;\n}\n\nexport interface JudgeConfig {\n apiKey: string | null;\n model: string;\n}\n\nexport interface BanditoConfig {\n apiKey: string | null;\n baseUrl: string;\n dataStorage: string; // \"local\", \"cloud\", or \"s3\"\n s3: S3Config | null;\n judge: JudgeConfig;\n}\n\n/**\n * Load config from TOML file + env var overrides.\n */\nexport function loadConfig(): BanditoConfig {\n const config: BanditoConfig = {\n apiKey: null,\n baseUrl: DEFAULT_BASE_URL,\n dataStorage: \"local\",\n s3: null,\n judge: { apiKey: null, model: \"gpt-4o-mini\" },\n };\n\n // TOML file first\n if (fs.existsSync(CONFIG_FILE)) {\n try {\n const content = fs.readFileSync(CONFIG_FILE, \"utf-8\");\n const data = parseToml(content) as Record<string, unknown>;\n if (data.api_key) config.apiKey = data.api_key as string;\n if (data.base_url) config.baseUrl = data.base_url as string;\n if (data.data_storage) config.dataStorage = data.data_storage as string;\n const s3Data = data.s3 as Record<string, unknown> | undefined;\n const s3Bucket = ((s3Data?.bucket as string | undefined) ?? \"\").trim();\n if (s3Bucket) {\n const s3Endpoint = ((s3Data?.endpoint as string | undefined) ?? \"\").trim() || undefined;\n config.s3 = {\n bucket: s3Bucket,\n prefix: ((s3Data?.prefix as string | undefined) ?? \"\").trim() || \"bandito\",\n region: ((s3Data?.region as string | undefined) ?? \"\").trim() || \"us-east-1\",\n ...(s3Endpoint ? { endpoint: s3Endpoint } : {}),\n };\n }\n const judgeData = data.judge as Record<string, unknown> | undefined;\n if (judgeData?.api_key) config.judge.apiKey = judgeData.api_key as string;\n if (judgeData?.model) config.judge.model = judgeData.model as string;\n } catch {\n // Failed to parse TOML — fall back to env vars\n }\n }\n\n // Env vars override\n const envKey = process.env.BANDITO_API_KEY;\n if (envKey) config.apiKey = envKey;\n\n const envUrl = process.env.BANDITO_BASE_URL;\n if (envUrl) config.baseUrl = envUrl;\n\n const envStorage = process.env.BANDITO_DATA_STORAGE;\n if (envStorage) config.dataStorage = envStorage;\n\n // S3 env vars (useful for container deployments without a config file)\n const envS3Bucket = (process.env.BANDITO_S3_BUCKET ?? \"\").trim();\n if (envS3Bucket) {\n const envEndpoint = (process.env.BANDITO_S3_ENDPOINT ?? config.s3?.endpoint ?? \"\").trim() || undefined;\n config.s3 = {\n bucket: envS3Bucket,\n prefix: (process.env.BANDITO_S3_PREFIX ?? config.s3?.prefix ?? \"\").trim() || \"bandito\",\n region: (process.env.BANDITO_S3_REGION ?? config.s3?.region ?? \"\").trim() || \"us-east-1\",\n ...(envEndpoint ? { endpoint: envEndpoint } : {}),\n };\n // Setting BANDITO_S3_BUCKET implicitly activates s3 mode unless\n // the user has explicitly chosen a different mode via BANDITO_DATA_STORAGE.\n if (!process.env.BANDITO_DATA_STORAGE) {\n config.dataStorage = \"s3\";\n }\n } else if (config.s3) {\n const envPrefix = (process.env.BANDITO_S3_PREFIX ?? \"\").trim();\n const envRegion = (process.env.BANDITO_S3_REGION ?? \"\").trim();\n const envEndpoint = (process.env.BANDITO_S3_ENDPOINT ?? \"\").trim();\n if (envPrefix) config.s3.prefix = envPrefix;\n if (envRegion) config.s3.region = envRegion;\n if (envEndpoint) config.s3.endpoint = envEndpoint;\n }\n\n const envJudgeKey = process.env.JUDGE_API_KEY;\n if (envJudgeKey) config.judge.apiKey = envJudgeKey;\n\n return config;\n}\n","/**\n * HTTP transport — fetch-based client for cloud API.\n *\n * Retry config: 3 attempts, exponential backoff (0.5s, 1s, 2s),\n * retries only 5xx and network errors. Never retries 4xx.\n */\n\nconst MAX_RETRIES = 3;\nconst RETRY_BACKOFF_BASE = 500; // ms — 500, 1000, 2000\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction isRetryable(status: number): boolean {\n return status >= 500;\n}\n\nexport class BanditoHTTP {\n private baseUrl: string;\n private apiKey: string;\n private timeout: number;\n\n constructor(baseUrl: string, apiKey: string, timeout: number = 10_000) {\n // Validate URL scheme — reject non-http(s) protocols (file://, javascript:, etc.)\n let parsed: URL;\n try {\n parsed = new URL(baseUrl);\n } catch {\n throw new Error(`Invalid baseUrl: \"${baseUrl}\"`);\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n throw new Error(\n `baseUrl must use http or https, got \"${parsed.protocol}\" — check your config`,\n );\n }\n this.baseUrl = `${baseUrl.replace(/\\/$/, \"\")}/api/v1`;\n this.apiKey = apiKey;\n this.timeout = timeout;\n }\n\n private async request(\n method: string,\n path: string,\n body?: unknown,\n ): Promise<Record<string, unknown>> {\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const resp = await fetch(`${this.baseUrl}${path}`, {\n method,\n headers: {\n \"X-API-Key\": this.apiKey,\n \"Content-Type\": \"application/json\",\n },\n body: body != null ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timer);\n\n if (!resp.ok) {\n const text = await resp.text().catch(() => \"\");\n // Truncate body to avoid leaking sensitive content into error messages/logs\n const preview = text.length > 200 ? `${text.slice(0, 200)}…` : text;\n if (!isRetryable(resp.status) || attempt === MAX_RETRIES - 1) {\n throw new Error(\n `HTTP ${resp.status} on ${method} ${path}: ${preview}`,\n );\n }\n // Retryable server error — fall through to retry\n lastError = new Error(\n `HTTP ${resp.status} on ${method} ${path}: ${preview}`,\n );\n } else {\n return (await resp.json()) as Record<string, unknown>;\n }\n } catch (err) {\n clearTimeout(timer);\n lastError = err as Error;\n\n // AbortError = timeout, TypeError = network error (in fetch)\n const isNetworkOrTimeout =\n (err as Error).name === \"AbortError\" ||\n (err as Error).name === \"TypeError\";\n if (!isNetworkOrTimeout && attempt < MAX_RETRIES - 1) {\n // Non-retryable (4xx already handled above)\n throw err;\n }\n if (attempt === MAX_RETRIES - 1) {\n throw err;\n }\n }\n\n // Exponential backoff\n const delay = RETRY_BACKOFF_BASE * 2 ** attempt;\n await sleep(delay);\n }\n\n throw lastError!;\n }\n\n /** POST /sync/connect — SDK bootstrap. */\n async connect(): Promise<Record<string, unknown>> {\n return this.request(\"POST\", \"/sync/connect\");\n }\n\n /** POST /sync/heartbeat — periodic state refresh. */\n async heartbeat(): Promise<Record<string, unknown>> {\n return this.request(\"POST\", \"/sync/heartbeat\", {});\n }\n\n /** POST /events — batch event ingestion. */\n async ingestEvents(\n events: Record<string, unknown>[],\n ): Promise<Record<string, unknown>> {\n return this.request(\"POST\", \"/events\", { events });\n }\n\n /** PATCH /events/{uuid}/grade — submit human grade. */\n async submitGrade(\n eventUuid: string,\n grade: number,\n ): Promise<Record<string, unknown>> {\n return this.request(\"PATCH\", `/events/${eventUuid}/grade`, {\n grade,\n is_graded: true,\n });\n }\n}\n","/**\n * SQLite WAL durability layer — crash-safe event storage.\n *\n * Events are written here immediately after update(). Background flush\n * sends them to cloud. If the process crashes, pending events survive\n * and are retried on next connect().\n */\n\nimport Database from \"better-sqlite3\";\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS events (\n local_event_uuid TEXT PRIMARY KEY,\n bandit_id INTEGER NOT NULL,\n arm_id INTEGER NOT NULL,\n payload TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n created_at REAL NOT NULL,\n human_reward REAL,\n graded_at REAL,\n s3_exported INTEGER NOT NULL DEFAULT 0\n);\nCREATE INDEX IF NOT EXISTS idx_events_status ON events(status);\n`;\n\nconst MIGRATION_GRADING = [\n \"ALTER TABLE events ADD COLUMN human_reward REAL\",\n \"ALTER TABLE events ADD COLUMN graded_at REAL\",\n \"ALTER TABLE events ADD COLUMN s3_exported INTEGER NOT NULL DEFAULT 0\",\n];\n\nexport interface EventPayload {\n local_event_uuid: string;\n bandit_id: number;\n arm_id: number;\n model_name: string;\n model_provider: string;\n bandit_name?: string;\n [key: string]: unknown;\n}\n\nexport class EventStore {\n private db: Database.Database;\n private pushStmt: Database.Statement;\n private pendingStmt: Database.Statement;\n\n constructor(dbPath: string = \":memory:\") {\n this.db = new Database(dbPath);\n this.db.pragma(\"journal_mode = WAL\");\n this.db.pragma(\"busy_timeout = 5000\");\n this.db.pragma(\"synchronous = NORMAL\");\n this.db.exec(SCHEMA);\n this.migrate();\n\n // Pre-compile frequently-used statements\n this.pushStmt = this.db.prepare(\n `INSERT OR IGNORE INTO events\n (local_event_uuid, bandit_id, arm_id, payload, status, created_at)\n VALUES (?, ?, ?, ?, 'pending', ?)`,\n );\n this.pendingStmt = this.db.prepare(\n `SELECT local_event_uuid, payload FROM events WHERE status = 'pending'\n ORDER BY created_at ASC LIMIT ?`,\n );\n }\n\n /** Insert a pending event. */\n push(event: EventPayload): void {\n this.pushStmt.run(\n event.local_event_uuid,\n event.bandit_id,\n event.arm_id,\n JSON.stringify(event),\n Date.now() / 1000,\n );\n }\n\n /** Return up to `limit` pending events (oldest first). */\n pending(limit: number = 50): EventPayload[] {\n const rows = this.pendingStmt.all(limit) as {\n local_event_uuid: string;\n payload: string;\n }[];\n const results: EventPayload[] = [];\n const corrupt: string[] = [];\n for (const row of rows) {\n try {\n results.push(JSON.parse(row.payload) as EventPayload);\n } catch {\n console.warn(\n `[bandito] Discarding corrupt event ${row.local_event_uuid} from store`,\n );\n corrupt.push(row.local_event_uuid);\n }\n }\n if (corrupt.length > 0) {\n this.markFlushed(corrupt);\n }\n return results;\n }\n\n /** Mark events as successfully flushed to cloud. */\n markFlushed(uuids: string[]): void {\n if (uuids.length === 0) return;\n const placeholders = uuids.map(() => \"?\").join(\",\");\n this.db\n .prepare(\n `UPDATE events SET status = 'flushed'\n WHERE local_event_uuid IN (${placeholders})`,\n )\n .run(...uuids);\n }\n\n /** Record a human grade locally. */\n markGraded(uuid: string, reward: number): void {\n this.db\n .prepare(\n `UPDATE events SET human_reward = ?, graded_at = ?\n WHERE local_event_uuid = ?`,\n )\n .run(reward, Date.now() / 1000, uuid);\n }\n\n /** Return un-exported events as {event, ts} for S3 dump. */\n pendingS3(limit: number = 100): Array<{ event: Record<string, unknown>; ts: number }> {\n const rows = this.db\n .prepare(\n `SELECT payload, created_at FROM events WHERE s3_exported = 0\n ORDER BY created_at ASC LIMIT ?`,\n )\n .all(limit) as { payload: string; created_at: number }[];\n return rows.map((r) => ({ event: JSON.parse(r.payload) as Record<string, unknown>, ts: r.created_at }));\n }\n\n /** Mark events as successfully exported to S3. */\n markS3Exported(uuids: string[]): void {\n if (uuids.length === 0) return;\n const placeholders = uuids.map(() => \"?\").join(\",\");\n this.db\n .prepare(`UPDATE events SET s3_exported = 1 WHERE local_event_uuid IN (${placeholders})`)\n .run(...uuids);\n }\n\n /** Close the database connection. */\n close(): void {\n this.db.close();\n }\n\n private migrate(): void {\n for (const stmt of MIGRATION_GRADING) {\n try {\n this.db.exec(stmt);\n } catch {\n // Column already exists\n }\n }\n }\n}\n","/**\n * Payload utilities for cloud event ingestion.\n */\n\nconst TEXT_FIELDS = [\"query_text\", \"response\"] as const;\nconst METADATA_FIELDS = [\"model_name\", \"model_provider\"] as const;\n\n/**\n * Return shallow copies of events ready for cloud ingest.\n *\n * Always strips model_name/model_provider (only needed in local SQLite for TUI).\n * Strips query_text/response when includeText is false (dataStorage=\"local\").\n */\nexport function prepareCloudPayload(\n events: Record<string, unknown>[],\n includeText: boolean,\n): Record<string, unknown>[] {\n return events.map((event) => {\n const copy = { ...event };\n for (const field of METADATA_FIELDS) {\n delete copy[field];\n }\n if (!includeText) {\n for (const field of TEXT_FIELDS) {\n delete copy[field];\n }\n }\n return copy;\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACSA,yBAA2B;AAC3B,IAAAA,MAAoB;AACpB,IAAAC,QAAsB;AACtB,IAAAC,MAAoB;AACpB,6BAA4B;;;ACJ5B,IAAI,aAA6D;AAMjE,eAAsB,WAA0B;AAC9C,MAAI,WAAY;AAChB,eAAa,MAAM,OAAO,wBAAwB;AACpD;AAOO,SAAS,aAAa,YAAsC;AACjE,MAAI,CAAC,YAAY;AACf,UAAM,IAAI,MAAM,mDAA8C;AAAA,EAChE;AAEA,QAAM,YAAY,IAAI,WAAW,CAAC;AAClC,aAAW,OAAO,gBAAgB,SAAS;AAC3C,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,YAAQ,OAAO,UAAU,CAAC,CAAC,KAAK,OAAO,IAAI,CAAC;AAAA,EAC9C;AACA,SAAO,WAAW,aAAa,YAAY,YAAY,IAAI;AAC7D;AAOO,SAAS,aAAa,QAA0B,YAA0B;AAC/E,SAAO,eAAe,UAAU;AAClC;;;ACbO,SAAS,UAAU,MAMlB;AACN,QAAM,MAAW;AAAA,IACf,OAAO,KAAK;AAAA,IACZ,WAAW,KAAK;AAAA,IAChB,eAAe,KAAK;AAAA,IACpB,cAAc,KAAK;AAAA,IACnB,mBAAmB,KAAK,uBAAuB;AAAA,IAC/C,IAAI,QAAQ;AACV,aAAO,KAAK;AAAA,IACd;AAAA,IACA,IAAI,SAAS;AACX,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACA,SAAO,OAAO,OAAO,GAAG;AAC1B;AAGO,SAAS,iBAAiB,MAOlB;AACb,QAAM,SAAqB;AAAA,IACzB,KAAK,KAAK;AAAA,IACV,SAAS,KAAK;AAAA,IACd,UAAU,KAAK;AAAA,IACf,YAAY,KAAK;AAAA,IACjB,QAAQ,OAAO,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC;AAAA,IACxC,IAAI,QAAQ;AACV,aAAO,KAAK,IAAI;AAAA,IAClB;AAAA,IACA,IAAI,SAAS;AACX,aAAO,KAAK,IAAI;AAAA,IAClB;AAAA,IACA,WAAW,KAAK;AAAA,EAClB;AACA,SAAO,OAAO,OAAO,MAAM;AAC7B;;;ACzEA,SAAoB;AACpB,WAAsB;AACtB,SAAoB;AACpB,uBAAmC;AAE5B,IAAM,mBAAmB;AAChC,IAAM,aAAkB,UAAQ,WAAQ,GAAG,UAAU;AACrD,IAAM,cAAmB,UAAK,YAAY,aAAa;AA0BhD,SAAS,aAA4B;AAC1C,QAAM,SAAwB;AAAA,IAC5B,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,aAAa;AAAA,IACb,IAAI;AAAA,IACJ,OAAO,EAAE,QAAQ,MAAM,OAAO,cAAc;AAAA,EAC9C;AAGA,MAAO,cAAW,WAAW,GAAG;AAC9B,QAAI;AACF,YAAM,UAAa,gBAAa,aAAa,OAAO;AACpD,YAAM,WAAO,iBAAAC,OAAU,OAAO;AAC9B,UAAI,KAAK,QAAS,QAAO,SAAS,KAAK;AACvC,UAAI,KAAK,SAAU,QAAO,UAAU,KAAK;AACzC,UAAI,KAAK,aAAc,QAAO,cAAc,KAAK;AACjD,YAAM,SAAS,KAAK;AACpB,YAAM,YAAa,QAAQ,UAAiC,IAAI,KAAK;AACrE,UAAI,UAAU;AACZ,cAAM,cAAe,QAAQ,YAAmC,IAAI,KAAK,KAAK;AAC9E,eAAO,KAAK;AAAA,UACV,QAAQ;AAAA,UACR,SAAU,QAAQ,UAAiC,IAAI,KAAK,KAAK;AAAA,UACjE,SAAU,QAAQ,UAAiC,IAAI,KAAK,KAAK;AAAA,UACjE,GAAI,aAAa,EAAE,UAAU,WAAW,IAAI,CAAC;AAAA,QAC/C;AAAA,MACF;AACA,YAAM,YAAY,KAAK;AACvB,UAAI,WAAW,QAAS,QAAO,MAAM,SAAS,UAAU;AACxD,UAAI,WAAW,MAAO,QAAO,MAAM,QAAQ,UAAU;AAAA,IACvD,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,OAAQ,QAAO,SAAS;AAE5B,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,OAAQ,QAAO,UAAU;AAE7B,QAAM,aAAa,QAAQ,IAAI;AAC/B,MAAI,WAAY,QAAO,cAAc;AAGrC,QAAM,eAAe,QAAQ,IAAI,qBAAqB,IAAI,KAAK;AAC/D,MAAI,aAAa;AACf,UAAM,eAAe,QAAQ,IAAI,uBAAuB,OAAO,IAAI,YAAY,IAAI,KAAK,KAAK;AAC7F,WAAO,KAAK;AAAA,MACV,QAAQ;AAAA,MACR,SAAS,QAAQ,IAAI,qBAAqB,OAAO,IAAI,UAAU,IAAI,KAAK,KAAK;AAAA,MAC7E,SAAS,QAAQ,IAAI,qBAAqB,OAAO,IAAI,UAAU,IAAI,KAAK,KAAK;AAAA,MAC7E,GAAI,cAAc,EAAE,UAAU,YAAY,IAAI,CAAC;AAAA,IACjD;AAGA,QAAI,CAAC,QAAQ,IAAI,sBAAsB;AACrC,aAAO,cAAc;AAAA,IACvB;AAAA,EACF,WAAW,OAAO,IAAI;AACpB,UAAM,aAAa,QAAQ,IAAI,qBAAqB,IAAI,KAAK;AAC7D,UAAM,aAAa,QAAQ,IAAI,qBAAqB,IAAI,KAAK;AAC7D,UAAM,eAAe,QAAQ,IAAI,uBAAuB,IAAI,KAAK;AACjE,QAAI,UAAW,QAAO,GAAG,SAAS;AAClC,QAAI,UAAW,QAAO,GAAG,SAAS;AAClC,QAAI,YAAa,QAAO,GAAG,WAAW;AAAA,EACxC;AAEA,QAAM,cAAc,QAAQ,IAAI;AAChC,MAAI,YAAa,QAAO,MAAM,SAAS;AAEvC,SAAO;AACT;;;AC3GA,IAAM,cAAc;AACpB,IAAM,qBAAqB;AAE3B,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAEA,SAAS,YAAY,QAAyB;AAC5C,SAAO,UAAU;AACnB;AAEO,IAAM,cAAN,MAAkB;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,SAAiB,QAAgB,UAAkB,KAAQ;AAErE,QAAI;AACJ,QAAI;AACF,eAAS,IAAI,IAAI,OAAO;AAAA,IAC1B,QAAQ;AACN,YAAM,IAAI,MAAM,qBAAqB,OAAO,GAAG;AAAA,IACjD;AACA,QAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SAAS;AAC/D,YAAM,IAAI;AAAA,QACR,wCAAwC,OAAO,QAAQ;AAAA,MACzD;AAAA,IACF;AACA,SAAK,UAAU,GAAG,QAAQ,QAAQ,OAAO,EAAE,CAAC;AAC5C,SAAK,SAAS;AACd,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAc,QACZ,QACAC,OACA,MACkC;AAClC,QAAI,YAA0B;AAE9B,aAAS,UAAU,GAAG,UAAU,aAAa,WAAW;AACtD,YAAM,aAAa,IAAI,gBAAgB;AACvC,YAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,KAAK,OAAO;AAE/D,UAAI;AACF,cAAM,OAAO,MAAM,MAAM,GAAG,KAAK,OAAO,GAAGA,KAAI,IAAI;AAAA,UACjD;AAAA,UACA,SAAS;AAAA,YACP,aAAa,KAAK;AAAA,YAClB,gBAAgB;AAAA,UAClB;AAAA,UACA,MAAM,QAAQ,OAAO,KAAK,UAAU,IAAI,IAAI;AAAA,UAC5C,QAAQ,WAAW;AAAA,QACrB,CAAC;AAED,qBAAa,KAAK;AAElB,YAAI,CAAC,KAAK,IAAI;AACZ,gBAAM,OAAO,MAAM,KAAK,KAAK,EAAE,MAAM,MAAM,EAAE;AAE7C,gBAAM,UAAU,KAAK,SAAS,MAAM,GAAG,KAAK,MAAM,GAAG,GAAG,CAAC,WAAM;AAC/D,cAAI,CAAC,YAAY,KAAK,MAAM,KAAK,YAAY,cAAc,GAAG;AAC5D,kBAAM,IAAI;AAAA,cACR,QAAQ,KAAK,MAAM,OAAO,MAAM,IAAIA,KAAI,KAAK,OAAO;AAAA,YACtD;AAAA,UACF;AAEA,sBAAY,IAAI;AAAA,YACd,QAAQ,KAAK,MAAM,OAAO,MAAM,IAAIA,KAAI,KAAK,OAAO;AAAA,UACtD;AAAA,QACF,OAAO;AACL,iBAAQ,MAAM,KAAK,KAAK;AAAA,QAC1B;AAAA,MACF,SAAS,KAAK;AACZ,qBAAa,KAAK;AAClB,oBAAY;AAGZ,cAAM,qBACH,IAAc,SAAS,gBACvB,IAAc,SAAS;AAC1B,YAAI,CAAC,sBAAsB,UAAU,cAAc,GAAG;AAEpD,gBAAM;AAAA,QACR;AACA,YAAI,YAAY,cAAc,GAAG;AAC/B,gBAAM;AAAA,QACR;AAAA,MACF;AAGA,YAAM,QAAQ,qBAAqB,KAAK;AACxC,YAAM,MAAM,KAAK;AAAA,IACnB;AAEA,UAAM;AAAA,EACR;AAAA;AAAA,EAGA,MAAM,UAA4C;AAChD,WAAO,KAAK,QAAQ,QAAQ,eAAe;AAAA,EAC7C;AAAA;AAAA,EAGA,MAAM,YAA8C;AAClD,WAAO,KAAK,QAAQ,QAAQ,mBAAmB,CAAC,CAAC;AAAA,EACnD;AAAA;AAAA,EAGA,MAAM,aACJ,QACkC;AAClC,WAAO,KAAK,QAAQ,QAAQ,WAAW,EAAE,OAAO,CAAC;AAAA,EACnD;AAAA;AAAA,EAGA,MAAM,YACJ,WACAC,QACkC;AAClC,WAAO,KAAK,QAAQ,SAAS,WAAW,SAAS,UAAU;AAAA,MACzD,OAAAA;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AAAA,EACH;AACF;;;AC7HA,4BAAqB;AAErB,IAAM,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAef,IAAM,oBAAoB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AACF;AAYO,IAAM,aAAN,MAAiB;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,SAAiB,YAAY;AACvC,SAAK,KAAK,IAAI,sBAAAC,QAAS,MAAM;AAC7B,SAAK,GAAG,OAAO,oBAAoB;AACnC,SAAK,GAAG,OAAO,qBAAqB;AACpC,SAAK,GAAG,OAAO,sBAAsB;AACrC,SAAK,GAAG,KAAK,MAAM;AACnB,SAAK,QAAQ;AAGb,SAAK,WAAW,KAAK,GAAG;AAAA,MACtB;AAAA;AAAA;AAAA,IAGF;AACA,SAAK,cAAc,KAAK,GAAG;AAAA,MACzB;AAAA;AAAA,IAEF;AAAA,EACF;AAAA;AAAA,EAGA,KAAK,OAA2B;AAC9B,SAAK,SAAS;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,KAAK,UAAU,KAAK;AAAA,MACpB,KAAK,IAAI,IAAI;AAAA,IACf;AAAA,EACF;AAAA;AAAA,EAGA,QAAQ,QAAgB,IAAoB;AAC1C,UAAM,OAAO,KAAK,YAAY,IAAI,KAAK;AAIvC,UAAM,UAA0B,CAAC;AACjC,UAAM,UAAoB,CAAC;AAC3B,eAAW,OAAO,MAAM;AACtB,UAAI;AACF,gBAAQ,KAAK,KAAK,MAAM,IAAI,OAAO,CAAiB;AAAA,MACtD,QAAQ;AACN,gBAAQ;AAAA,UACN,sCAAsC,IAAI,gBAAgB;AAAA,QAC5D;AACA,gBAAQ,KAAK,IAAI,gBAAgB;AAAA,MACnC;AAAA,IACF;AACA,QAAI,QAAQ,SAAS,GAAG;AACtB,WAAK,YAAY,OAAO;AAAA,IAC1B;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,YAAY,OAAuB;AACjC,QAAI,MAAM,WAAW,EAAG;AACxB,UAAM,eAAe,MAAM,IAAI,MAAM,GAAG,EAAE,KAAK,GAAG;AAClD,SAAK,GACF;AAAA,MACC;AAAA,sCAC8B,YAAY;AAAA,IAC5C,EACC,IAAI,GAAG,KAAK;AAAA,EACjB;AAAA;AAAA,EAGA,WAAW,MAAc,QAAsB;AAC7C,SAAK,GACF;AAAA,MACC;AAAA;AAAA,IAEF,EACC,IAAI,QAAQ,KAAK,IAAI,IAAI,KAAM,IAAI;AAAA,EACxC;AAAA;AAAA,EAGA,UAAU,QAAgB,KAA4D;AACpF,UAAM,OAAO,KAAK,GACf;AAAA,MACC;AAAA;AAAA,IAEF,EACC,IAAI,KAAK;AACZ,WAAO,KAAK,IAAI,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,EAAE,OAAO,GAA8B,IAAI,EAAE,WAAW,EAAE;AAAA,EACxG;AAAA;AAAA,EAGA,eAAe,OAAuB;AACpC,QAAI,MAAM,WAAW,EAAG;AACxB,UAAM,eAAe,MAAM,IAAI,MAAM,GAAG,EAAE,KAAK,GAAG;AAClD,SAAK,GACF,QAAQ,gEAAgE,YAAY,GAAG,EACvF,IAAI,GAAG,KAAK;AAAA,EACjB;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,GAAG,MAAM;AAAA,EAChB;AAAA,EAEQ,UAAgB;AACtB,eAAW,QAAQ,mBAAmB;AACpC,UAAI;AACF,aAAK,GAAG,KAAK,IAAI;AAAA,MACnB,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;;;ACzJA,IAAM,cAAc,CAAC,cAAc,UAAU;AAC7C,IAAM,kBAAkB,CAAC,cAAc,gBAAgB;AAQhD,SAAS,oBACd,QACA,aAC2B;AAC3B,SAAO,OAAO,IAAI,CAAC,UAAU;AAC3B,UAAM,OAAO,EAAE,GAAG,MAAM;AACxB,eAAW,SAAS,iBAAiB;AACnC,aAAO,KAAK,KAAK;AAAA,IACnB;AACA,QAAI,CAAC,aAAa;AAChB,iBAAW,SAAS,aAAa;AAC/B,eAAO,KAAK,KAAK;AAAA,MACnB;AAAA,IACF;AACA,WAAO;AAAA,EACT,CAAC;AACH;;;ANAA,IAAM,qBAA0B,WAAQ,YAAQ,GAAG,YAAY,WAAW;AAC1E,IAAM,oBAAoB;AA0BnB,IAAM,gBAAN,MAAoB;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,OAA2B;AAAA,EAC3B,QAA2B;AAAA,EAC3B,UAAqC,oBAAI,IAAI;AAAA,EAC7C,UAAoC,oBAAI,IAAI;AAAA,EAC5C,YAAY;AAAA,EACZ,gBAAuD;AAAA,EACvD,kBAAyD;AAAA,EACzD,kBAAkB;AAAA,EAClB,YAAyB,oBAAI,IAAI;AAAA,EACjC,cAAmC,oBAAI,IAAI;AAAA;AAAA,EAE3C,YAAiB;AAAA;AAAA,EAEjB,YAAiB;AAAA,EAEzB,YAAY,UAAyB,CAAC,GAAG;AACvC,SAAK,SAAS,QAAQ;AACtB,SAAK,UAAU,QAAQ;AACvB,SAAK,YAAY,QAAQ;AACzB,SAAK,iBAAiB,QAAQ;AAC9B,SAAK,cAAc,QAAQ,eAAe;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,UAAyB;AAE7B,QAAI,KAAK,WAAW;AAClB,YAAM,KAAK,MAAM;AAAA,IACnB;AAGA,UAAM,SAAS;AAGf,UAAM,SAAS,WAAW;AAC1B,UAAM,SAAS,KAAK,UAAU,OAAO;AACrC,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AAEA,UAAM,UAAU,KAAK,WAAW,OAAO;AACvC,QAAI,CAAC,KAAK,gBAAgB;AACxB,WAAK,cAAc,OAAO;AAAA,IAC5B;AAEA,SAAK,OAAO,IAAI,YAAY,SAAS,MAAM;AAC3C,UAAM,YAAY,KAAK,aAAa;AACpC,QAAI,cAAc,YAAY;AAC5B,YAAM,MAAW,cAAQ,SAAS;AAClC,UAAI,CAAI,eAAW,GAAG,GAAG;AACvB,QAAG,cAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,MACvC;AAAA,IACF;AACA,SAAK,QAAQ,IAAI,WAAW,SAAS;AAIrC,SAAK,YAAY;AACjB,SAAK,YAAY;AACjB,QAAI,KAAK,gBAAgB,QAAQ,OAAO,IAAI;AAC1C,UAAI;AACF,cAAM,EAAE,SAAS,IAAI,MAAM,OAAO,oBAAoB;AAEtD,cAAM,iBAAsC,EAAE,QAAQ,OAAO,GAAG,OAAO;AACvE,YAAI,OAAO,GAAG,UAAU;AACtB,yBAAe,WAAW,OAAO,GAAG;AACpC,yBAAe,iBAAiB;AAAA,QAClC;AACA,aAAK,YAAY,IAAI,SAAS,cAAc;AAC5C,aAAK,YAAY,OAAO;AAAA,MAC1B,QAAQ;AACN,gBAAQ,KAAK,yDAAyD;AAAA,MACxE;AAAA,IACF;AAGA,QAAI;AACF,YAAM,OAAO,MAAM,KAAK,KAAK,QAAQ;AACrC,WAAK,UAAU,IAAI;AAGnB,WAAK,UAAU,MAAM;AACrB,WAAK,YAAY,MAAM;AAGvB,YAAM,KAAK,aAAa;AAGxB,WAAK,gBAAgB,YAAY,MAAM;AACrC,aAAK,aAAa,EAAE;AAAA,UAAM,CAAC,QACzB,QAAQ,KAAK,kCAAkC,GAAG;AAAA,QACpD;AAAA,MACF,GAAG,GAAM;AAGT,UAAI,KAAK,cAAc,MAAM;AAC3B,aAAK,kBAAkB,YAAY,MAAM;AACvC,eAAK,gBAAgB,EAAE;AAAA,YAAM,CAAC,QAC5B,QAAQ,KAAK,sCAAsC,GAAG;AAAA,UACxD;AAAA,QACF,GAAG,GAAM;AAAA,MACX;AAEA,WAAK,YAAY;AAAA,IACnB,SAAS,KAAK;AACZ,WAAK,OAAO,MAAM;AAClB,WAAK,QAAQ;AACb,WAAK,OAAO;AACZ,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,KAAK,YAAoB,UAAuB,CAAC,GAAe;AAC9D,SAAK,gBAAgB;AAErB,UAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU;AACzC,QAAI,CAAC,OAAO;AACV,YAAM,YAAY,CAAC,GAAG,KAAK,QAAQ,KAAK,CAAC;AACzC,YAAM,IAAI;AAAA,QACR,mBAAmB,UAAU,kBAAkB,UAAU,KAAK,IAAI,CAAC;AAAA,MACrE;AAAA,IACF;AAEA,QAAI,MAAM,KAAK,WAAW,GAAG;AAC3B,YAAM,IAAI,MAAM,WAAW,UAAU,sBAAsB;AAAA,IAC7D;AAEA,UAAM,SAAS,KAAK,QAAQ,IAAI,UAAU;AAC1C,UAAM,cAAc,QAAQ,OAAO,UAAU;AAC7C,UAAM,aAAa,QAAQ,UAAU,WAAW,KAAK,QAAQ,OAAO,IAAI;AAExE,UAAM,aAAa,OAAO,KAAK,aAAa,UAAU;AACtD,UAAM,MAAwB,KAAK,MAAM,UAAU;AAGnD,UAAM,YAAY,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,UAAU,IAAI,MAAM;AAC/D,QAAI,CAAC,WAAW;AACd,YAAM,IAAI;AAAA,QACR,uBAAuB,IAAI,MAAM,0CAA0C,UAAU;AAAA,MAEvF;AAAA,IACF;AAEA,WAAO,iBAAiB;AAAA,MACtB,KAAK;AAAA,MACL,aAAS,+BAAW;AAAA,MACpB,UAAU,MAAM;AAAA,MAChB;AAAA,MACA,QAAQ,IAAI;AAAA,MACZ,UAAU,mCAAY,IAAI;AAAA,IAC5B,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,YAAwB,UAAyB,CAAC,GAAS;AAChE,SAAK,gBAAgB;AAErB,QAAI,SAAS,QAAQ;AACrB,QAAI,QAAQ,UAAU,UAAU,MAAM;AACpC,eAAS;AAAA,IACX;AAGA,QAAI,UAAU,QAAQ;AACtB,QAAI,WAAW,QAAQ,WAAW,YAAY,GAAG;AAC/C,gBAAU,mCAAY,IAAI,IAAI,WAAW;AAAA,IAC3C;AAGA,UAAM,QAAsB;AAAA,MAC1B,kBAAkB,WAAW;AAAA,MAC7B,WAAW,WAAW;AAAA,MACtB,QAAQ,WAAW,IAAI;AAAA,MACvB,YAAY,WAAW,IAAI;AAAA,MAC3B,gBAAgB,WAAW,IAAI;AAAA,MAC/B,aAAa,WAAW;AAAA,IAC1B;AAEA,QAAI,QAAQ,aAAa,MAAM;AAC7B,MAAC,MAAkC,aAAa,QAAQ;AAAA,IAC1D;AACA,QAAI,QAAQ,YAAY,MAAM;AAC5B,MAAC,MAAkC,WACjC,OAAO,QAAQ,aAAa,WACxB,EAAE,UAAU,QAAQ,SAAS,IAC7B,QAAQ;AAAA,IAChB;AACA,QAAI,UAAU,MAAM;AAClB,MAAC,MAAkC,eAAe;AAAA,IACpD;AACA,QAAI,QAAQ,QAAQ,MAAM;AACxB,MAAC,MAAkC,OAAO,QAAQ;AAAA,IACpD;AACA,QAAI,WAAW,MAAM;AACnB,MAAC,MAAkC,UAAU;AAAA,IAC/C;AACA,QAAI,QAAQ,eAAe,MAAM;AAC/B,MAAC,MAAkC,eAAe,QAAQ;AAAA,IAC5D;AACA,QAAI,QAAQ,gBAAgB,MAAM;AAChC,MAAC,MAAkC,gBAAgB,QAAQ;AAAA,IAC7D;AACA,QAAI,QAAQ,WAAW,MAAM;AAC3B,MAAC,MAAkC,UAAU,QAAQ;AAAA,IACvD;AACA,QAAI,QAAQ,QAAQ;AAClB,MAAC,MAAkC,YAAY;AAAA,IACjD;AAGA,SAAK,MAAO,KAAK,KAAK;AAGtB,SAAK,aAAa,EAAE;AAAA,MAAM,CAAC,QACzB,QAAQ,KAAK,yBAAyB,GAAG;AAAA,IAC3C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,MAAM,SAAiBC,QAA8B;AACzD,SAAK,gBAAgB;AACrB,UAAM,KAAK,KAAM,YAAY,SAASA,MAAK;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAsB;AAC1B,SAAK,gBAAgB;AACrB,UAAM,OAAO,MAAM,KAAK,KAAM,UAAU;AAExC,UAAM,cAAc,IAAI,IAAI,KAAK,OAAO;AACxC,UAAM,cAAc,IAAI,IAAI,KAAK,OAAO;AACxC,QAAI;AACF,WAAK,UAAU,IAAI;AAAA,IACrB,SAAS,KAAK;AAEZ,WAAK,UAAU;AACf,WAAK,UAAU;AACf,cAAQ,KAAK,0EAAqE,GAAG;AAAA,IACvF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC3B,QAAI,KAAK,eAAe;AACtB,oBAAc,KAAK,aAAa;AAChC,WAAK,gBAAgB;AAAA,IACvB;AAEA,QAAI,KAAK,iBAAiB;AACxB,oBAAc,KAAK,eAAe;AAClC,WAAK,kBAAkB;AAAA,IACzB;AAGA,QAAI,KAAK,SAAS,KAAK,MAAM;AAC3B,YAAM,KAAK,aAAa;AAAA,IAC1B;AAGA,QAAI,KAAK,cAAc,QAAQ,KAAK,OAAO;AACzC,YAAM,KAAK,gBAAgB;AAAA,IAC7B;AAEA,SAAK,OAAO,MAAM;AAClB,SAAK,QAAQ;AACb,SAAK,OAAO;AACZ,SAAK,QAAQ,MAAM;AACnB,SAAK,QAAQ,MAAM;AACnB,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA,EAIQ,kBAAwB;AAC9B,QAAI,CAAC,KAAK,WAAW;AACnB,YAAM,IAAI,MAAM,2CAAsC;AAAA,IACxD;AAAA,EACF;AAAA,EAEQ,UAAU,MAAqC;AACrD,UAAM,cAAe,KAAK,WAAW,CAAC;AAEtC,UAAM,aAAa,oBAAI,IAAyB;AAChD,UAAM,aAAa,oBAAI,IAA0B;AAEjD,eAAW,KAAK,aAAa;AAC3B,YAAM,OAAQ,EAAE,QAAQ,CAAC;AACzB,UAAI,KAAK,WAAW,EAAG;AAEvB,YAAM,aAAoB,KACvB,OAAO,CAAC,MAAM,EAAE,SAAS,EACzB,IAAI,CAAC,MAAM,UAAU,CAAC,CAAC;AAE1B,YAAM,OAAO,EAAE;AACf,YAAM,QAAqB;AAAA,QACzB,UAAU,OAAO,EAAE,SAAS;AAAA,QAC5B;AAAA,QACA,MAAM;AAAA,QACN,SAAS;AAAA,QACT,kBAAmB,EAAE,qBAAgC;AAAA,QACrD,iBAAiB,EAAE;AAAA,QACnB,QAAQ,EAAE;AAAA,QACV,WAAW,EAAE;AAAA,MACf;AAEA,iBAAW,IAAI,MAAM,KAAK;AAG1B,YAAM,iBAAiB,KAAK,QAAQ,IAAI,IAAI;AAC5C,YAAM,aAAa,KAAK,UAAU,CAAC;AACnC,UAAI,gBAAgB;AAClB,qBAAa,gBAAgB,UAAU;AACvC,mBAAW,IAAI,MAAM,cAAc;AAAA,MACrC,OAAO;AACL,mBAAW,IAAI,MAAM,aAAa,UAAU,CAAC;AAAA,MAC/C;AAAA,IACF;AAEA,SAAK,UAAU;AACf,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAc,eAA8B;AAC1C,QAAI,KAAK,mBAAmB,CAAC,KAAK,SAAS,CAAC,KAAK,KAAM;AACvD,SAAK,kBAAkB;AAEvB,QAAI;AACF,YAAM,UAAU,KAAK,MAAM,QAAQ;AACnC,UAAI,QAAQ,WAAW,EAAG;AAG1B,YAAM,QAAQ,KAAK,UAAU,OAAO,IAChC,QAAQ,OAAO,CAAC,MAAM,CAAC,KAAK,UAAU,IAAI,EAAE,gBAAgB,CAAC,IAC7D;AACJ,UAAI,MAAM,WAAW,EAAG;AAGxB,YAAM,UAAU;AAAA,QACd;AAAA,QACA,KAAK,gBAAgB;AAAA,MACvB;AAEA,YAAM,SAAS,MAAM,KAAK,KAAK,aAAa,OAAO;AAGnD,YAAM,SAAU,OAAO,UAAU,CAAC;AAIlC,YAAM,eAAe,IAAI;AAAA,QACvB,OAAO,IAAI,CAAC,MAAM,EAAE,gBAAgB,EAAE,OAAO,OAAO;AAAA,MACtD;AAGA,iBAAW,OAAO,cAAc;AAC9B,cAAM,SAAS,KAAK,YAAY,IAAI,GAAG,KAAK,KAAK;AACjD,aAAK,YAAY,IAAI,KAAK,KAAK;AAC/B,YAAI,SAAS,mBAAmB;AAC9B,eAAK,UAAU,IAAI,GAAG;AAAA,QACxB;AAAA,MACF;AAGA,YAAM,eAAe,MAClB,IAAI,CAAC,MAAM,EAAE,gBAAgB,EAC7B,OAAO,CAAC,QAAQ,CAAC,aAAa,IAAI,GAAG,CAAC;AACzC,UAAI,aAAa,SAAS,KAAK,KAAK,OAAO;AACzC,aAAK,MAAM,YAAY,YAAY;AAAA,MACrC;AAAA,IAEF,SAAS,KAAK;AAEZ,cAAQ,KAAK,kDAA6C,GAAG;AAAA,IAC/D,UAAE;AACA,WAAK,kBAAkB;AAAA,IACzB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,kBAAiC;AAC7C,UAAM,eAAe,KAAK,MAAO,UAAU,GAAG;AAC9C,QAAI,aAAa,WAAW,EAAG;AAE/B,UAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,oBAAoB;AAC9D,UAAM,WAAqB,CAAC;AAE5B,QAAI;AACF,iBAAW,EAAE,OAAO,GAAG,KAAK,cAAc;AACxC,cAAM,OAAO,MAAM;AACnB,cAAM,UAAW,MAAM,eAAsC;AAC7D,cAAM,aAAa,QAAQ,QAAQ,aAAa,GAAG;AACnD,cAAM,OAAO,IAAI,KAAK,KAAK,GAAI;AAC/B,cAAM,WAAW;AAAA,UACf,KAAK,eAAe;AAAA,UACpB,OAAO,KAAK,YAAY,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AAAA,UAC9C,OAAO,KAAK,WAAW,CAAC,EAAE,SAAS,GAAG,GAAG;AAAA,QAC3C,EAAE,KAAK,GAAG;AACV,cAAM,MAAM,GAAG,KAAK,UAAU,MAAM,IAAI,UAAU,IAAI,QAAQ,IAAI,IAAI;AAEtE,cAAM,KAAK,UAAU,KAAK,IAAI,iBAAiB;AAAA,UAC7C,QAAQ,KAAK,UAAU;AAAA,UACvB,KAAK;AAAA,UACL,MAAM,KAAK,UAAU,KAAK;AAAA,UAC1B,aAAa;AAAA,QACf,CAAC,CAAC;AACF,iBAAS,KAAK,IAAI;AAAA,MACpB;AAAA,IACF,SAAS,KAAK;AACZ,cAAQ,KAAK,gDAA2C,GAAG;AAAA,IAC7D,UAAE;AACA,UAAI,SAAS,SAAS,GAAG;AACvB,aAAK,MAAO,eAAe,QAAQ;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AACF;;;AD3dA,IAAI,UAAgC;AAEpC,SAAS,YAA2B;AAClC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,2CAAsC;AAAA,EACxD;AACA,SAAO;AACT;AAGA,eAAsB,QAAQ,UAAyB,CAAC,GAAkB;AACxE,MAAI,SAAS;AACX,UAAM,QAAQ,MAAM;AAAA,EACtB;AACA,YAAU,IAAI,cAAc,OAAO;AACnC,QAAM,QAAQ,QAAQ;AACxB;AAGO,SAAS,KAAK,YAAoB,SAAmC;AAC1E,SAAO,UAAU,EAAE,KAAK,YAAY,OAAO;AAC7C;AAGO,SAAS,OAAO,YAAwB,SAA+B;AAC5E,YAAU,EAAE,OAAO,YAAY,OAAO;AACxC;AAGA,eAAsB,MAAM,SAAiB,YAAmC;AAC9E,QAAM,UAAU,EAAE,MAAM,SAAS,UAAU;AAC7C;AAGA,eAAsB,OAAsB;AAC1C,QAAM,UAAU,EAAE,KAAK;AACzB;AAGA,eAAsB,QAAuB;AAC3C,MAAI,SAAS;AACX,UAAM,QAAQ,MAAM;AACpB,cAAU;AAAA,EACZ;AACF;","names":["fs","path","os","parseToml","path","grade","Database","grade"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/engine.ts","../src/models.ts","../src/config.ts","../src/http.ts","../src/store.ts","../src/worker.ts"],"sourcesContent":["/**\n * Bandito SDK — contextual bandit optimization for LLM selection.\n *\n * Recommended (explicit client):\n * import { BanditoClient } from 'bandito';\n *\n * const client = new BanditoClient({ apiKey: 'bnd_...' });\n * await client.connect();\n * const result = client.pull('my-chatbot', { query: userMessage });\n * // ... call LLM with result.model, result.prompt ...\n * client.update(result, { response: response.text });\n * await client.close();\n *\n * Module-level singleton (convenience):\n * import { connect, pull, update, close } from 'bandito';\n *\n * await connect({ apiKey: 'bnd_...' });\n * const result = pull('my-chatbot', { query: userMessage });\n * update(result, { response: response.text });\n * await close();\n */\n\nexport { BanditoClient, type ClientOptions, type PullOptions, type UpdateOptions } from \"./client.js\";\nexport { type Arm, type PullResult } from \"./models.js\";\n\nimport { BanditoClient, type ClientOptions, type PullOptions, type UpdateOptions } from \"./client.js\";\nimport type { PullResult } from \"./models.js\";\n\nlet _client: BanditoClient | null = null;\n\nfunction getClient(): BanditoClient {\n if (!_client) {\n throw new Error(\"Not connected — call connect() first\");\n }\n return _client;\n}\n\n/** Connect to the Bandito cloud and hydrate local state. */\nexport async function connect(options: ClientOptions = {}): Promise<void> {\n if (_client) {\n await _client.close();\n }\n _client = new BanditoClient(options);\n await _client.connect();\n}\n\n/** Local Thompson Sampling decision. <1ms, no network. */\nexport function pull(banditName: string, options?: PullOptions): PullResult {\n return getClient().pull(banditName, options);\n}\n\n/** Record an LLM call outcome (writes to SQLite, fire-and-forget flush). */\nexport function update(pullResult: PullResult, options?: UpdateOptions): void {\n getClient().update(pullResult, options);\n}\n\n/** Send a human grade for an existing event. */\nexport async function grade(eventId: string, gradeValue: number): Promise<void> {\n await getClient().grade(eventId, gradeValue);\n}\n\n/** Explicit state refresh from cloud. */\nexport async function sync(): Promise<void> {\n await getClient().sync();\n}\n\n/** Shut down: flush events, close connections. */\nexport async function close(): Promise<void> {\n if (_client) {\n await _client.close();\n _client = null;\n }\n}\n","/**\n * BanditoClient — main orchestrator for the JS/TS SDK.\n *\n * Mirrors the Python SDK's sync-first design:\n * - pull() is synchronous (WASM math, <1ms)\n * - connect(), grade(), sync(), close() are async (HTTP I/O)\n * - update() is synchronous (SQLite write + fire-and-forget flush)\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { performance } from \"node:perf_hooks\";\n\nimport { initWasm, createEngine, updateEngine, type BanditEngine, type EnginePullResult } from \"./engine.js\";\nimport {\n type Arm,\n type PullResult,\n type BanditCache,\n type ArmWire,\n createArm,\n createPullResult,\n} from \"./models.js\";\nimport { loadConfig, DEFAULT_BASE_URL } from \"./config.js\";\nimport { BanditoHTTP } from \"./http.js\";\nimport { EventStore, type EventPayload } from \"./store.js\";\nimport { prepareCloudPayload } from \"./worker.js\";\n\nconst DEFAULT_STORE_PATH = path.join(os.homedir(), \".bandito\", \"events.db\");\nconst MAX_EVENT_RETRIES = 5;\n\nexport interface ClientOptions {\n apiKey?: string;\n baseUrl?: string;\n storePath?: string;\n dataStorage?: string;\n}\n\nexport interface PullOptions {\n query?: string;\n exclude?: number[];\n}\n\nexport interface UpdateOptions {\n queryText?: string;\n response?: string | Record<string, unknown>;\n reward?: number;\n cost?: number;\n latency?: number;\n inputTokens?: number;\n outputTokens?: number;\n segment?: Record<string, string>;\n failed?: boolean;\n}\n\nexport class BanditoClient {\n private apiKey: string | undefined;\n private baseUrl: string | undefined;\n private storePath: string | undefined;\n private dataStorageArg: string | undefined;\n private dataStorage: string;\n\n private http: BanditoHTTP | null = null;\n private store: EventStore | null = null;\n private engines: Map<string, BanditEngine> = new Map();\n private bandits: Map<string, BanditCache> = new Map();\n private connected = false;\n private flushInterval: ReturnType<typeof setInterval> | null = null;\n private flushInProgress = false;\n private deadUuids: Set<string> = new Set();\n private retryCounts: Map<string, number> = new Map();\n\n constructor(options: ClientOptions = {}) {\n this.apiKey = options.apiKey;\n this.baseUrl = options.baseUrl;\n this.storePath = options.storePath;\n this.dataStorageArg = options.dataStorage;\n this.dataStorage = options.dataStorage ?? \"local\";\n }\n\n /**\n * Bootstrap: authenticate and hydrate in-memory state from cloud.\n *\n * Resolves config from: constructor args → env vars → ~/.bandito/config.toml.\n * Initializes WASM, creates HTTP client, SQLite store, fetches full state.\n */\n async connect(): Promise<void> {\n // Tear down previous connection if reconnecting\n if (this.connected) {\n await this.close();\n }\n\n // Init WASM (loads .wasm binary once)\n await initWasm();\n\n // Resolve config\n const config = loadConfig();\n const apiKey = this.apiKey ?? config.apiKey;\n if (!apiKey) {\n throw new Error(\n \"apiKey required — pass it to constructor, set BANDITO_API_KEY, \" +\n \"or run `bandito signup`\",\n );\n }\n\n const baseUrl = this.baseUrl ?? config.baseUrl;\n if (!this.dataStorageArg) {\n this.dataStorage = config.dataStorage;\n }\n\n this.http = new BanditoHTTP(baseUrl, apiKey);\n const storePath = this.storePath ?? DEFAULT_STORE_PATH;\n if (storePath !== \":memory:\") {\n const dir = path.dirname(storePath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n }\n this.store = new EventStore(storePath);\n\n // Bootstrap: fetch state, hydrate cache, flush pending\n try {\n const data = await this.http.connect();\n this.applySync(data);\n\n // Reset retry state\n this.deadUuids.clear();\n this.retryCounts.clear();\n\n // Flush pending events from previous crash\n await this.flushPending();\n\n // Start periodic flush (every 30s)\n this.flushInterval = setInterval(() => {\n this.flushPending().catch(() => {});\n }, 30_000);\n\n this.connected = true;\n } catch (err) {\n this.store?.close();\n this.store = null;\n this.http = null;\n throw err;\n }\n }\n\n /**\n * Local Thompson Sampling decision. Synchronous, <1ms, no network.\n */\n pull(banditName: string, options: PullOptions = {}): PullResult {\n this.ensureConnected();\n\n const cache = this.bandits.get(banditName);\n if (!cache) {\n const available = [...this.bandits.keys()];\n throw new Error(\n `Unknown bandit '${banditName}'. Available: [${available.join(\", \")}]`,\n );\n }\n\n if (cache.arms.length === 0) {\n throw new Error(`Bandit '${banditName}' has no active arms`);\n }\n\n const engine = this.engines.get(banditName)!;\n const queryLength = options.query?.length ?? undefined;\n const excludeIds = options.exclude ? Int32Array.from(options.exclude) : undefined;\n\n const resultJson = engine.pull(queryLength, excludeIds);\n const raw: EnginePullResult = JSON.parse(resultJson);\n\n // Look up the winning arm from our cached active arms\n const winnerArm = cache.arms.find((a) => a.armId === raw.arm_id);\n if (!winnerArm) {\n throw new Error(\n `Engine selected arm ${raw.arm_id} but it's not in active arm cache for \"${banditName}\". ` +\n \"This is likely a bug — please report it at https://github.com/bandito-ai/bandito/issues\",\n );\n }\n\n return createPullResult({\n arm: winnerArm,\n eventId: randomUUID(),\n banditId: cache.banditId,\n banditName,\n scores: raw.scores,\n pullTime: performance.now(),\n });\n }\n\n /**\n * Record an LLM call outcome. Writes to SQLite first (crash-safe),\n * then fires off a non-blocking flush to cloud.\n */\n update(pullResult: PullResult, options: UpdateOptions = {}): void {\n this.ensureConnected();\n\n let reward = options.reward;\n if (options.failed && reward == null) {\n reward = 0.0;\n }\n\n // Auto-calculate latency from pull timestamp\n let latency = options.latency;\n if (latency == null && pullResult._pullTime > 0) {\n latency = performance.now() - pullResult._pullTime;\n }\n\n // Build event payload (snake_case for wire format)\n const event: EventPayload = {\n local_event_uuid: pullResult.eventId,\n bandit_id: pullResult.banditId,\n arm_id: pullResult.arm.armId,\n model_name: pullResult.arm.modelName,\n model_provider: pullResult.arm.modelProvider,\n };\n\n if (options.queryText != null) {\n (event as Record<string, unknown>).query_text = options.queryText;\n }\n if (options.response != null) {\n (event as Record<string, unknown>).response =\n typeof options.response === \"string\"\n ? { response: options.response }\n : options.response;\n }\n if (reward != null) {\n (event as Record<string, unknown>).early_reward = reward;\n }\n if (options.cost != null) {\n (event as Record<string, unknown>).cost = options.cost;\n }\n if (latency != null) {\n (event as Record<string, unknown>).latency = latency;\n }\n if (options.inputTokens != null) {\n (event as Record<string, unknown>).input_tokens = options.inputTokens;\n }\n if (options.outputTokens != null) {\n (event as Record<string, unknown>).output_tokens = options.outputTokens;\n }\n if (options.segment != null) {\n (event as Record<string, unknown>).segment = options.segment;\n }\n if (options.failed) {\n (event as Record<string, unknown>).run_error = true;\n }\n\n // Write to SQLite WAL first — survives crashes\n this.store!.push(event);\n\n // Fire-and-forget flush (errors logged inside flushPending)\n this.flushPending().catch(() => {});\n }\n\n /**\n * Send a human grade for an existing event. Async (HTTP).\n */\n async grade(eventId: string, grade: number): Promise<void> {\n this.ensureConnected();\n await this.http!.submitGrade(eventId, grade);\n }\n\n /**\n * Explicit state refresh from cloud.\n */\n async sync(): Promise<void> {\n this.ensureConnected();\n const data = await this.http!.heartbeat();\n\n const prevBandits = new Map(this.bandits);\n const prevEngines = new Map(this.engines);\n try {\n this.applySync(data);\n } catch (err) {\n // Rollback on malformed response — keep last-known-good state\n this.bandits = prevBandits;\n this.engines = prevEngines;\n console.warn(\"[bandito] Sync response malformed — keeping last-known-good state\", err);\n }\n }\n\n /**\n * Shut down: clear interval, flush remaining events, close connections.\n */\n async close(): Promise<void> {\n if (this.flushInterval) {\n clearInterval(this.flushInterval);\n this.flushInterval = null;\n }\n\n // Final flush\n if (this.store && this.http) {\n await this.flushPending();\n }\n\n this.store?.close();\n this.store = null;\n this.http = null;\n this.engines.clear();\n this.bandits.clear();\n this.connected = false;\n }\n\n // --- Internal ---\n\n private ensureConnected(): void {\n if (!this.connected) {\n throw new Error(\"Not connected — call connect() first\");\n }\n }\n\n private applySync(data: Record<string, unknown>): void {\n const banditsData = (data.bandits ?? []) as Record<string, unknown>[];\n\n const newBandits = new Map<string, BanditCache>();\n const newEngines = new Map<string, BanditEngine>();\n\n for (const b of banditsData) {\n const arms = (b.arms ?? []) as ArmWire[];\n if (arms.length === 0) continue;\n\n const activeArms: Arm[] = arms\n .filter((a) => a.is_active)\n .map((a) => createArm(a));\n\n const name = b.name as string;\n const cache: BanditCache = {\n banditId: Number(b.bandit_id),\n name,\n arms: activeArms,\n armWire: arms,\n optimizationMode: (b.optimization_mode as string) ?? \"base\",\n avgLatencyLastN: b.avg_latency_last_n as number | null,\n budget: b.budget as number | null,\n totalCost: b.total_cost as number | null,\n };\n\n newBandits.set(name, cache);\n\n // Reuse existing engine (preserves RNG state) or create new one\n const existingEngine = this.engines.get(name);\n const banditJson = JSON.stringify(b);\n if (existingEngine) {\n updateEngine(existingEngine, banditJson);\n newEngines.set(name, existingEngine);\n } else {\n newEngines.set(name, createEngine(banditJson));\n }\n }\n\n this.bandits = newBandits;\n this.engines = newEngines;\n }\n\n private async flushPending(): Promise<void> {\n if (this.flushInProgress || !this.store || !this.http) return;\n this.flushInProgress = true;\n\n try {\n const pending = this.store.pending();\n if (pending.length === 0) return;\n\n // Filter out dead events\n const alive = this.deadUuids.size > 0\n ? pending.filter((e) => !this.deadUuids.has(e.local_event_uuid))\n : pending;\n if (alive.length === 0) return;\n\n // Prepare payload (strip metadata/text as configured)\n const payload = prepareCloudPayload(\n alive as Record<string, unknown>[],\n this.dataStorage !== \"local\",\n );\n\n const result = await this.http.ingestEvents(payload);\n\n // Parse per-event errors\n const errors = (result.errors ?? []) as {\n local_event_uuid?: string;\n reason?: string;\n }[];\n const erroredUuids = new Set(\n errors.map((e) => e.local_event_uuid).filter(Boolean) as string[],\n );\n\n // Update retry counts\n for (const uid of erroredUuids) {\n const count = (this.retryCounts.get(uid) ?? 0) + 1;\n this.retryCounts.set(uid, count);\n if (count >= MAX_EVENT_RETRIES) {\n this.deadUuids.add(uid);\n }\n }\n\n // Mark accepted events as flushed\n const flushedUuids = alive\n .map((e) => e.local_event_uuid)\n .filter((uid) => !erroredUuids.has(uid));\n if (flushedUuids.length > 0 && this.store) {\n this.store.markFlushed(flushedUuids);\n }\n } catch (err) {\n // Flush failure is non-fatal — events stay pending for next attempt\n console.warn(\"[bandito] Event flush failed — will retry\", err);\n } finally {\n this.flushInProgress = false;\n }\n }\n}\n","/**\n * Thin wrapper around the WASM engine import.\n *\n * Handles async WASM initialization (loading .wasm binary happens once)\n * and re-exports the BanditEngine constructor for the client.\n */\n\nimport type { BanditEngine as WasmBanditEngine } from \"../wasm/bandito_engine\";\n\nlet wasmModule: typeof import(\"../wasm/bandito_engine\") | null = null;\n\n/**\n * Initialize the WASM module. Must be called before creating BanditEngine instances.\n * Safe to call multiple times — only loads once.\n */\nexport async function initWasm(): Promise<void> {\n if (wasmModule) return;\n wasmModule = await import(\"../wasm/bandito_engine\");\n}\n\n/**\n * Create a BanditEngine from a sync response JSON string.\n * Requires initWasm() to have been called first.\n */\nexport function createEngine(banditJson: string): WasmBanditEngine {\n if (!wasmModule) {\n throw new Error(\"WASM not initialized — call initWasm() first\");\n }\n return new wasmModule.BanditEngine(banditJson);\n}\n\n/**\n * Update an existing BanditEngine with new sync response data.\n * Preserves RNG state (avoids the \"always picks same arm\" bug).\n */\nexport function updateEngine(engine: WasmBanditEngine, banditJson: string): void {\n engine.updateFromSync(banditJson);\n}\n\nexport type { WasmBanditEngine as BanditEngine };\n\n/**\n * Pull result parsed from engine JSON output.\n */\nexport interface EnginePullResult {\n arm_index: number;\n arm_id: number;\n scores: Record<number, number>;\n}\n","/**\n * SDK types: Arm, PullResult, and internal cache structures.\n */\n\n/** An arm returned to the user after pull(). Immutable. */\nexport interface Arm {\n readonly armId: number;\n readonly modelName: string;\n readonly modelProvider: string;\n readonly systemPrompt: string;\n readonly isPromptTemplated: boolean;\n /** Convenience alias for modelName. */\n readonly model: string;\n /** Convenience alias for systemPrompt. */\n readonly prompt: string;\n}\n\n/** Returned by pull(), passed to update(). Immutable. */\nexport interface PullResult {\n readonly arm: Arm;\n readonly eventId: string;\n readonly banditId: number;\n readonly banditName: string;\n readonly scores: Readonly<Record<number, number>>;\n /** Convenience reach-through to arm.modelName. */\n readonly model: string;\n /** Convenience reach-through to arm.systemPrompt. */\n readonly prompt: string;\n /** @internal perf timestamp */\n readonly _pullTime: number;\n}\n\n/** Create a frozen Arm from raw wire data. */\nexport function createArm(data: {\n arm_id: number;\n model_name: string;\n model_provider: string;\n system_prompt: string;\n is_prompt_templated?: boolean;\n}): Arm {\n const arm: Arm = {\n armId: data.arm_id,\n modelName: data.model_name,\n modelProvider: data.model_provider,\n systemPrompt: data.system_prompt,\n isPromptTemplated: data.is_prompt_templated ?? false,\n get model() {\n return this.modelName;\n },\n get prompt() {\n return this.systemPrompt;\n },\n };\n return Object.freeze(arm);\n}\n\n/** Create a frozen PullResult. */\nexport function createPullResult(data: {\n arm: Arm;\n eventId: string;\n banditId: number;\n banditName: string;\n scores: Record<number, number>;\n pullTime: number;\n}): PullResult {\n const result: PullResult = {\n arm: data.arm,\n eventId: data.eventId,\n banditId: data.banditId,\n banditName: data.banditName,\n scores: Object.freeze({ ...data.scores }),\n get model() {\n return this.arm.modelName;\n },\n get prompt() {\n return this.arm.systemPrompt;\n },\n _pullTime: data.pullTime,\n };\n return Object.freeze(result);\n}\n\n/** Raw arm data from sync response (snake_case wire format). */\nexport interface ArmWire {\n arm_id: number;\n model_name: string;\n model_provider: string;\n system_prompt: string;\n is_prompt_templated: boolean;\n is_active: boolean;\n avg_latency_last_n: number | null;\n}\n\n/** Internal mutable cache for a bandit's state. */\nexport interface BanditCache {\n banditId: number;\n name: string;\n arms: Arm[]; // active only\n armWire: ArmWire[]; // all arms (for engine JSON)\n optimizationMode: string;\n avgLatencyLastN: number | null;\n budget: number | null;\n totalCost: number | null;\n}\n","/**\n * Config loader — reads ~/.bandito/config.toml and env vars.\n *\n * Resolution order: constructor args → env vars → TOML → defaults.\n * Same file as the Python SDK uses.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { parse as parseToml } from \"smol-toml\";\n\nexport const DEFAULT_BASE_URL = \"https://bandito-api.onrender.com\";\nconst CONFIG_DIR = path.join(os.homedir(), \".bandito\");\nconst CONFIG_FILE = path.join(CONFIG_DIR, \"config.toml\");\n\nexport interface BanditoConfig {\n apiKey: string | null;\n baseUrl: string;\n dataStorage: string; // \"local\" or \"cloud\"\n}\n\n/**\n * Load config from TOML file + env var overrides.\n */\nexport function loadConfig(): BanditoConfig {\n const config: BanditoConfig = {\n apiKey: null,\n baseUrl: DEFAULT_BASE_URL,\n dataStorage: \"local\",\n };\n\n // TOML file first\n if (fs.existsSync(CONFIG_FILE)) {\n try {\n const content = fs.readFileSync(CONFIG_FILE, \"utf-8\");\n const data = parseToml(content);\n if (data.api_key) config.apiKey = data.api_key;\n if (data.base_url) config.baseUrl = data.base_url;\n if (data.data_storage) config.dataStorage = data.data_storage;\n } catch {\n // Failed to parse TOML — fall back to env vars\n }\n }\n\n // Env vars override\n const envKey = process.env.BANDITO_API_KEY;\n if (envKey) config.apiKey = envKey;\n\n const envUrl = process.env.BANDITO_BASE_URL;\n if (envUrl) config.baseUrl = envUrl;\n\n const envStorage = process.env.BANDITO_DATA_STORAGE;\n if (envStorage) config.dataStorage = envStorage;\n\n return config;\n}\n","/**\n * HTTP transport — fetch-based client for cloud API.\n *\n * Retry config: 3 attempts, exponential backoff (0.5s, 1s, 2s),\n * retries only 5xx and network errors. Never retries 4xx.\n */\n\nconst MAX_RETRIES = 3;\nconst RETRY_BACKOFF_BASE = 500; // ms — 500, 1000, 2000\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction isRetryable(status: number): boolean {\n return status >= 500;\n}\n\nexport class BanditoHTTP {\n private baseUrl: string;\n private apiKey: string;\n private timeout: number;\n\n constructor(baseUrl: string, apiKey: string, timeout: number = 10_000) {\n this.baseUrl = `${baseUrl.replace(/\\/$/, \"\")}/api/v1`;\n this.apiKey = apiKey;\n this.timeout = timeout;\n }\n\n private async request(\n method: string,\n path: string,\n body?: unknown,\n ): Promise<Record<string, unknown>> {\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const resp = await fetch(`${this.baseUrl}${path}`, {\n method,\n headers: {\n \"X-API-Key\": this.apiKey,\n \"Content-Type\": \"application/json\",\n },\n body: body != null ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timer);\n\n if (!resp.ok) {\n const text = await resp.text().catch(() => \"\");\n if (!isRetryable(resp.status) || attempt === MAX_RETRIES - 1) {\n throw new Error(\n `HTTP ${resp.status} on ${method} ${path}: ${text}`,\n );\n }\n // Retryable server error — fall through to retry\n lastError = new Error(\n `HTTP ${resp.status} on ${method} ${path}: ${text}`,\n );\n } else {\n return (await resp.json()) as Record<string, unknown>;\n }\n } catch (err) {\n clearTimeout(timer);\n lastError = err as Error;\n\n // AbortError = timeout, TypeError = network error (in fetch)\n const isNetworkOrTimeout =\n (err as Error).name === \"AbortError\" ||\n (err as Error).name === \"TypeError\";\n if (!isNetworkOrTimeout && attempt < MAX_RETRIES - 1) {\n // Non-retryable (4xx already handled above)\n throw err;\n }\n if (attempt === MAX_RETRIES - 1) {\n throw err;\n }\n }\n\n // Exponential backoff\n const delay = RETRY_BACKOFF_BASE * 2 ** attempt;\n await sleep(delay);\n }\n\n throw lastError!;\n }\n\n /** POST /sync/connect — SDK bootstrap. */\n async connect(): Promise<Record<string, unknown>> {\n return this.request(\"POST\", \"/sync/connect\");\n }\n\n /** POST /sync/heartbeat — periodic state refresh. */\n async heartbeat(): Promise<Record<string, unknown>> {\n return this.request(\"POST\", \"/sync/heartbeat\", {});\n }\n\n /** POST /events — batch event ingestion. */\n async ingestEvents(\n events: Record<string, unknown>[],\n ): Promise<Record<string, unknown>> {\n return this.request(\"POST\", \"/events\", { events });\n }\n\n /** PATCH /events/{uuid}/grade — submit human grade. */\n async submitGrade(\n eventUuid: string,\n grade: number,\n ): Promise<Record<string, unknown>> {\n return this.request(\"PATCH\", `/events/${eventUuid}/grade`, {\n grade,\n is_graded: true,\n });\n }\n}\n","/**\n * SQLite WAL durability layer — crash-safe event storage.\n *\n * Events are written here immediately after update(). Background flush\n * sends them to cloud. If the process crashes, pending events survive\n * and are retried on next connect().\n */\n\nimport Database from \"better-sqlite3\";\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS events (\n local_event_uuid TEXT PRIMARY KEY,\n bandit_id INTEGER NOT NULL,\n arm_id INTEGER NOT NULL,\n payload TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n created_at REAL NOT NULL,\n human_reward REAL,\n graded_at REAL\n);\nCREATE INDEX IF NOT EXISTS idx_events_status ON events(status);\n`;\n\nconst MIGRATION_GRADING = [\n \"ALTER TABLE events ADD COLUMN human_reward REAL\",\n \"ALTER TABLE events ADD COLUMN graded_at REAL\",\n];\n\nexport interface EventPayload {\n local_event_uuid: string;\n bandit_id: number;\n arm_id: number;\n [key: string]: unknown;\n}\n\nexport class EventStore {\n private db: Database.Database;\n private pushStmt: Database.Statement;\n private pendingStmt: Database.Statement;\n\n constructor(dbPath: string = \":memory:\") {\n this.db = new Database(dbPath);\n this.db.pragma(\"journal_mode = WAL\");\n this.db.pragma(\"busy_timeout = 5000\");\n this.db.pragma(\"synchronous = NORMAL\");\n this.db.exec(SCHEMA);\n this.migrate();\n\n // Pre-compile frequently-used statements\n this.pushStmt = this.db.prepare(\n `INSERT OR IGNORE INTO events\n (local_event_uuid, bandit_id, arm_id, payload, status, created_at)\n VALUES (?, ?, ?, ?, 'pending', ?)`,\n );\n this.pendingStmt = this.db.prepare(\n `SELECT payload FROM events WHERE status = 'pending'\n ORDER BY created_at ASC LIMIT ?`,\n );\n }\n\n /** Insert a pending event. */\n push(event: EventPayload): void {\n this.pushStmt.run(\n event.local_event_uuid,\n event.bandit_id,\n event.arm_id,\n JSON.stringify(event),\n Date.now() / 1000,\n );\n }\n\n /** Return up to `limit` pending events (oldest first). */\n pending(limit: number = 50): EventPayload[] {\n const rows = this.pendingStmt.all(limit) as { payload: string }[];\n return rows.map((row) => JSON.parse(row.payload));\n }\n\n /** Mark events as successfully flushed to cloud. */\n markFlushed(uuids: string[]): void {\n if (uuids.length === 0) return;\n const placeholders = uuids.map(() => \"?\").join(\",\");\n this.db\n .prepare(\n `UPDATE events SET status = 'flushed'\n WHERE local_event_uuid IN (${placeholders})`,\n )\n .run(...uuids);\n }\n\n /** Record a human grade locally. */\n markGraded(uuid: string, reward: number): void {\n this.db\n .prepare(\n `UPDATE events SET human_reward = ?, graded_at = ?\n WHERE local_event_uuid = ?`,\n )\n .run(reward, Date.now() / 1000, uuid);\n }\n\n /** Close the database connection. */\n close(): void {\n this.db.close();\n }\n\n private migrate(): void {\n for (const stmt of MIGRATION_GRADING) {\n try {\n this.db.exec(stmt);\n } catch {\n // Column already exists\n }\n }\n }\n}\n","/**\n * Payload utilities for cloud event ingestion.\n */\n\nconst TEXT_FIELDS = [\"query_text\", \"response\"] as const;\nconst METADATA_FIELDS = [\"model_name\", \"model_provider\"] as const;\n\n/**\n * Return shallow copies of events ready for cloud ingest.\n *\n * Always strips model_name/model_provider (only needed in local SQLite for TUI).\n * Strips query_text/response when includeText is false (dataStorage=\"local\").\n */\nexport function prepareCloudPayload(\n events: Record<string, unknown>[],\n includeText: boolean,\n): Record<string, unknown>[] {\n return events.map((event) => {\n const copy = { ...event };\n for (const field of METADATA_FIELDS) {\n delete copy[field];\n }\n if (!includeText) {\n for (const field of TEXT_FIELDS) {\n delete copy[field];\n }\n }\n return copy;\n });\n}\n"],"mappings":"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","names":["fs","path","os","parseToml","path","grade","Database","grade"]}
package/dist/index.mjs CHANGED
@@ -15,13 +15,7 @@ function createEngine(banditJson) {
15
15
  if (!wasmModule) {
16
16
  throw new Error("WASM not initialized \u2014 call initWasm() first");
17
17
  }
18
- const seedBytes = new Uint8Array(8);
19
- globalThis.crypto.getRandomValues(seedBytes);
20
- let seed = 0n;
21
- for (let i = 0; i < 8; i++) {
22
- seed |= BigInt(seedBytes[i]) << BigInt(i * 8);
23
- }
24
- return wasmModule.BanditEngine.newWithSeed(banditJson, seed);
18
+ return new wasmModule.BanditEngine(banditJson);
25
19
  }
26
20
  function updateEngine(engine, banditJson) {
27
21
  engine.updateFromSync(banditJson);
@@ -74,9 +68,7 @@ function loadConfig() {
74
68
  const config = {
75
69
  apiKey: null,
76
70
  baseUrl: DEFAULT_BASE_URL,
77
- dataStorage: "local",
78
- s3: null,
79
- judge: { apiKey: null, model: "gpt-4o-mini" }
71
+ dataStorage: "local"
80
72
  };
81
73
  if (fs.existsSync(CONFIG_FILE)) {
82
74
  try {
@@ -85,20 +77,6 @@ function loadConfig() {
85
77
  if (data.api_key) config.apiKey = data.api_key;
86
78
  if (data.base_url) config.baseUrl = data.base_url;
87
79
  if (data.data_storage) config.dataStorage = data.data_storage;
88
- const s3Data = data.s3;
89
- const s3Bucket = (s3Data?.bucket ?? "").trim();
90
- if (s3Bucket) {
91
- const s3Endpoint = (s3Data?.endpoint ?? "").trim() || void 0;
92
- config.s3 = {
93
- bucket: s3Bucket,
94
- prefix: (s3Data?.prefix ?? "").trim() || "bandito",
95
- region: (s3Data?.region ?? "").trim() || "us-east-1",
96
- ...s3Endpoint ? { endpoint: s3Endpoint } : {}
97
- };
98
- }
99
- const judgeData = data.judge;
100
- if (judgeData?.api_key) config.judge.apiKey = judgeData.api_key;
101
- if (judgeData?.model) config.judge.model = judgeData.model;
102
80
  } catch {
103
81
  }
104
82
  }
@@ -108,28 +86,6 @@ function loadConfig() {
108
86
  if (envUrl) config.baseUrl = envUrl;
109
87
  const envStorage = process.env.BANDITO_DATA_STORAGE;
110
88
  if (envStorage) config.dataStorage = envStorage;
111
- const envS3Bucket = (process.env.BANDITO_S3_BUCKET ?? "").trim();
112
- if (envS3Bucket) {
113
- const envEndpoint = (process.env.BANDITO_S3_ENDPOINT ?? config.s3?.endpoint ?? "").trim() || void 0;
114
- config.s3 = {
115
- bucket: envS3Bucket,
116
- prefix: (process.env.BANDITO_S3_PREFIX ?? config.s3?.prefix ?? "").trim() || "bandito",
117
- region: (process.env.BANDITO_S3_REGION ?? config.s3?.region ?? "").trim() || "us-east-1",
118
- ...envEndpoint ? { endpoint: envEndpoint } : {}
119
- };
120
- if (!process.env.BANDITO_DATA_STORAGE) {
121
- config.dataStorage = "s3";
122
- }
123
- } else if (config.s3) {
124
- const envPrefix = (process.env.BANDITO_S3_PREFIX ?? "").trim();
125
- const envRegion = (process.env.BANDITO_S3_REGION ?? "").trim();
126
- const envEndpoint = (process.env.BANDITO_S3_ENDPOINT ?? "").trim();
127
- if (envPrefix) config.s3.prefix = envPrefix;
128
- if (envRegion) config.s3.region = envRegion;
129
- if (envEndpoint) config.s3.endpoint = envEndpoint;
130
- }
131
- const envJudgeKey = process.env.JUDGE_API_KEY;
132
- if (envJudgeKey) config.judge.apiKey = envJudgeKey;
133
89
  return config;
134
90
  }
135
91
 
@@ -147,17 +103,6 @@ var BanditoHTTP = class {
147
103
  apiKey;
148
104
  timeout;
149
105
  constructor(baseUrl, apiKey, timeout = 1e4) {
150
- let parsed;
151
- try {
152
- parsed = new URL(baseUrl);
153
- } catch {
154
- throw new Error(`Invalid baseUrl: "${baseUrl}"`);
155
- }
156
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
157
- throw new Error(
158
- `baseUrl must use http or https, got "${parsed.protocol}" \u2014 check your config`
159
- );
160
- }
161
106
  this.baseUrl = `${baseUrl.replace(/\/$/, "")}/api/v1`;
162
107
  this.apiKey = apiKey;
163
108
  this.timeout = timeout;
@@ -180,14 +125,13 @@ var BanditoHTTP = class {
180
125
  clearTimeout(timer);
181
126
  if (!resp.ok) {
182
127
  const text = await resp.text().catch(() => "");
183
- const preview = text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
184
128
  if (!isRetryable(resp.status) || attempt === MAX_RETRIES - 1) {
185
129
  throw new Error(
186
- `HTTP ${resp.status} on ${method} ${path3}: ${preview}`
130
+ `HTTP ${resp.status} on ${method} ${path3}: ${text}`
187
131
  );
188
132
  }
189
133
  lastError = new Error(
190
- `HTTP ${resp.status} on ${method} ${path3}: ${preview}`
134
+ `HTTP ${resp.status} on ${method} ${path3}: ${text}`
191
135
  );
192
136
  } else {
193
137
  return await resp.json();
@@ -240,15 +184,13 @@ CREATE TABLE IF NOT EXISTS events (
240
184
  status TEXT NOT NULL DEFAULT 'pending',
241
185
  created_at REAL NOT NULL,
242
186
  human_reward REAL,
243
- graded_at REAL,
244
- s3_exported INTEGER NOT NULL DEFAULT 0
187
+ graded_at REAL
245
188
  );
246
189
  CREATE INDEX IF NOT EXISTS idx_events_status ON events(status);
247
190
  `;
248
191
  var MIGRATION_GRADING = [
249
192
  "ALTER TABLE events ADD COLUMN human_reward REAL",
250
- "ALTER TABLE events ADD COLUMN graded_at REAL",
251
- "ALTER TABLE events ADD COLUMN s3_exported INTEGER NOT NULL DEFAULT 0"
193
+ "ALTER TABLE events ADD COLUMN graded_at REAL"
252
194
  ];
253
195
  var EventStore = class {
254
196
  db;
@@ -267,7 +209,7 @@ var EventStore = class {
267
209
  VALUES (?, ?, ?, ?, 'pending', ?)`
268
210
  );
269
211
  this.pendingStmt = this.db.prepare(
270
- `SELECT local_event_uuid, payload FROM events WHERE status = 'pending'
212
+ `SELECT payload FROM events WHERE status = 'pending'
271
213
  ORDER BY created_at ASC LIMIT ?`
272
214
  );
273
215
  }
@@ -284,22 +226,7 @@ var EventStore = class {
284
226
  /** Return up to `limit` pending events (oldest first). */
285
227
  pending(limit = 50) {
286
228
  const rows = this.pendingStmt.all(limit);
287
- const results = [];
288
- const corrupt = [];
289
- for (const row of rows) {
290
- try {
291
- results.push(JSON.parse(row.payload));
292
- } catch {
293
- console.warn(
294
- `[bandito] Discarding corrupt event ${row.local_event_uuid} from store`
295
- );
296
- corrupt.push(row.local_event_uuid);
297
- }
298
- }
299
- if (corrupt.length > 0) {
300
- this.markFlushed(corrupt);
301
- }
302
- return results;
229
+ return rows.map((row) => JSON.parse(row.payload));
303
230
  }
304
231
  /** Mark events as successfully flushed to cloud. */
305
232
  markFlushed(uuids) {
@@ -317,20 +244,6 @@ var EventStore = class {
317
244
  WHERE local_event_uuid = ?`
318
245
  ).run(reward, Date.now() / 1e3, uuid);
319
246
  }
320
- /** Return un-exported events as {event, ts} for S3 dump. */
321
- pendingS3(limit = 100) {
322
- const rows = this.db.prepare(
323
- `SELECT payload, created_at FROM events WHERE s3_exported = 0
324
- ORDER BY created_at ASC LIMIT ?`
325
- ).all(limit);
326
- return rows.map((r) => ({ event: JSON.parse(r.payload), ts: r.created_at }));
327
- }
328
- /** Mark events as successfully exported to S3. */
329
- markS3Exported(uuids) {
330
- if (uuids.length === 0) return;
331
- const placeholders = uuids.map(() => "?").join(",");
332
- this.db.prepare(`UPDATE events SET s3_exported = 1 WHERE local_event_uuid IN (${placeholders})`).run(...uuids);
333
- }
334
247
  /** Close the database connection. */
335
248
  close() {
336
249
  this.db.close();
@@ -378,14 +291,9 @@ var BanditoClient = class {
378
291
  bandits = /* @__PURE__ */ new Map();
379
292
  connected = false;
380
293
  flushInterval = null;
381
- s3FlushInterval = null;
382
294
  flushInProgress = false;
383
295
  deadUuids = /* @__PURE__ */ new Set();
384
296
  retryCounts = /* @__PURE__ */ new Map();
385
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
386
- _s3Client = null;
387
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
388
- _s3Config = null;
389
297
  constructor(options = {}) {
390
298
  this.apiKey = options.apiKey;
391
299
  this.baseUrl = options.baseUrl;
@@ -424,22 +332,6 @@ var BanditoClient = class {
424
332
  }
425
333
  }
426
334
  this.store = new EventStore(storePath);
427
- this._s3Client = null;
428
- this._s3Config = null;
429
- if (this.dataStorage === "s3" && config.s3) {
430
- try {
431
- const { S3Client } = await import("@aws-sdk/client-s3");
432
- const s3ClientConfig = { region: config.s3.region };
433
- if (config.s3.endpoint) {
434
- s3ClientConfig.endpoint = config.s3.endpoint;
435
- s3ClientConfig.forcePathStyle = true;
436
- }
437
- this._s3Client = new S3Client(s3ClientConfig);
438
- this._s3Config = config.s3;
439
- } catch {
440
- console.warn("[bandito] data_storage='s3' requires @aws-sdk/client-s3");
441
- }
442
- }
443
335
  try {
444
336
  const data = await this.http.connect();
445
337
  this.applySync(data);
@@ -447,17 +339,9 @@ var BanditoClient = class {
447
339
  this.retryCounts.clear();
448
340
  await this.flushPending();
449
341
  this.flushInterval = setInterval(() => {
450
- this.flushPending().catch(
451
- (err) => console.warn("[bandito] Periodic flush error", err)
452
- );
342
+ this.flushPending().catch(() => {
343
+ });
453
344
  }, 3e4);
454
- if (this._s3Client !== null) {
455
- this.s3FlushInterval = setInterval(() => {
456
- this.dumpPendingToS3().catch(
457
- (err) => console.warn("[bandito] Periodic S3 export error", err)
458
- );
459
- }, 3e4);
460
- }
461
345
  this.connected = true;
462
346
  } catch (err) {
463
347
  this.store?.close();
@@ -520,8 +404,7 @@ var BanditoClient = class {
520
404
  bandit_id: pullResult.banditId,
521
405
  arm_id: pullResult.arm.armId,
522
406
  model_name: pullResult.arm.modelName,
523
- model_provider: pullResult.arm.modelProvider,
524
- bandit_name: pullResult.banditName
407
+ model_provider: pullResult.arm.modelProvider
525
408
  };
526
409
  if (options.queryText != null) {
527
410
  event.query_text = options.queryText;
@@ -551,9 +434,8 @@ var BanditoClient = class {
551
434
  event.run_error = true;
552
435
  }
553
436
  this.store.push(event);
554
- this.flushPending().catch(
555
- (err) => console.warn("[bandito] Flush error", err)
556
- );
437
+ this.flushPending().catch(() => {
438
+ });
557
439
  }
558
440
  /**
559
441
  * Send a human grade for an existing event. Async (HTTP).
@@ -586,16 +468,9 @@ var BanditoClient = class {
586
468
  clearInterval(this.flushInterval);
587
469
  this.flushInterval = null;
588
470
  }
589
- if (this.s3FlushInterval) {
590
- clearInterval(this.s3FlushInterval);
591
- this.s3FlushInterval = null;
592
- }
593
471
  if (this.store && this.http) {
594
472
  await this.flushPending();
595
473
  }
596
- if (this._s3Client !== null && this.store) {
597
- await this.dumpPendingToS3();
598
- }
599
474
  this.store?.close();
600
475
  this.store = null;
601
476
  this.http = null;
@@ -675,45 +550,6 @@ var BanditoClient = class {
675
550
  this.flushInProgress = false;
676
551
  }
677
552
  }
678
- /**
679
- * Export un-uploaded events to S3 as raw event JSON. One file per event.
680
- *
681
- * Key pattern: {prefix}/{bandit_name}/{YYYY/MM/DD}/{local_event_uuid}.json
682
- * Body: the raw event object (same structure as SQLite payload column).
683
- */
684
- async dumpPendingToS3() {
685
- const eventsWithTs = this.store.pendingS3(100);
686
- if (eventsWithTs.length === 0) return;
687
- const { PutObjectCommand } = await import("@aws-sdk/client-s3");
688
- const uploaded = [];
689
- try {
690
- for (const { event, ts } of eventsWithTs) {
691
- const uuid = event.local_event_uuid;
692
- const rawName = event.bandit_name || "unknown";
693
- const banditName = rawName.replace(/[^\w\-.]/g, "_");
694
- const date = new Date(ts * 1e3);
695
- const datePath = [
696
- date.getUTCFullYear(),
697
- String(date.getUTCMonth() + 1).padStart(2, "0"),
698
- String(date.getUTCDate()).padStart(2, "0")
699
- ].join("/");
700
- const key = `${this._s3Config.prefix}/${banditName}/${datePath}/${uuid}.json`;
701
- await this._s3Client.send(new PutObjectCommand({
702
- Bucket: this._s3Config.bucket,
703
- Key: key,
704
- Body: JSON.stringify(event),
705
- ContentType: "application/json"
706
- }));
707
- uploaded.push(uuid);
708
- }
709
- } catch (err) {
710
- console.warn("[bandito] S3 export failed \u2014 will retry", err);
711
- } finally {
712
- if (uploaded.length > 0) {
713
- this.store.markS3Exported(uploaded);
714
- }
715
- }
716
- }
717
553
  };
718
554
 
719
555
  // src/index.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/engine.ts","../src/models.ts","../src/config.ts","../src/http.ts","../src/store.ts","../src/worker.ts","../src/index.ts"],"sourcesContent":["/**\n * BanditoClient — main orchestrator for the JS/TS SDK.\n *\n * Mirrors the Python SDK's sync-first design:\n * - pull() is synchronous (WASM math, <1ms)\n * - connect(), grade(), sync(), close() are async (HTTP I/O)\n * - update() is synchronous (SQLite write + fire-and-forget flush)\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { performance } from \"node:perf_hooks\";\n\nimport { initWasm, createEngine, updateEngine, type BanditEngine, type EnginePullResult } from \"./engine.js\";\nimport {\n type Arm,\n type PullResult,\n type BanditCache,\n type ArmWire,\n createArm,\n createPullResult,\n} from \"./models.js\";\nimport { loadConfig, DEFAULT_BASE_URL } from \"./config.js\";\nimport { BanditoHTTP } from \"./http.js\";\nimport { EventStore, type EventPayload } from \"./store.js\";\nimport { prepareCloudPayload } from \"./worker.js\";\n\nconst DEFAULT_STORE_PATH = path.join(os.homedir(), \".bandito\", \"events.db\");\nconst MAX_EVENT_RETRIES = 5;\n\nexport interface ClientOptions {\n apiKey?: string;\n baseUrl?: string;\n storePath?: string;\n dataStorage?: string;\n}\n\nexport interface PullOptions {\n query?: string;\n exclude?: number[];\n}\n\nexport interface UpdateOptions {\n queryText?: string;\n response?: string | Record<string, unknown>;\n reward?: number;\n cost?: number;\n latency?: number;\n inputTokens?: number;\n outputTokens?: number;\n segment?: Record<string, string>;\n failed?: boolean;\n}\n\nexport class BanditoClient {\n private apiKey: string | undefined;\n private baseUrl: string | undefined;\n private storePath: string | undefined;\n private dataStorageArg: string | undefined;\n private dataStorage: string;\n\n private http: BanditoHTTP | null = null;\n private store: EventStore | null = null;\n private engines: Map<string, BanditEngine> = new Map();\n private bandits: Map<string, BanditCache> = new Map();\n private connected = false;\n private flushInterval: ReturnType<typeof setInterval> | null = null;\n private s3FlushInterval: ReturnType<typeof setInterval> | null = null;\n private flushInProgress = false;\n private deadUuids: Set<string> = new Set();\n private retryCounts: Map<string, number> = new Map();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private _s3Client: any = null;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private _s3Config: any = null;\n\n constructor(options: ClientOptions = {}) {\n this.apiKey = options.apiKey;\n this.baseUrl = options.baseUrl;\n this.storePath = options.storePath;\n this.dataStorageArg = options.dataStorage;\n this.dataStorage = options.dataStorage ?? \"local\";\n }\n\n /**\n * Bootstrap: authenticate and hydrate in-memory state from cloud.\n *\n * Resolves config from: constructor args → env vars → ~/.bandito/config.toml.\n * Initializes WASM, creates HTTP client, SQLite store, fetches full state.\n */\n async connect(): Promise<void> {\n // Tear down previous connection if reconnecting\n if (this.connected) {\n await this.close();\n }\n\n // Init WASM (loads .wasm binary once)\n await initWasm();\n\n // Resolve config\n const config = loadConfig();\n const apiKey = this.apiKey ?? config.apiKey;\n if (!apiKey) {\n throw new Error(\n \"apiKey required — pass it to constructor, set BANDITO_API_KEY, \" +\n \"or run `bandito signup`\",\n );\n }\n\n const baseUrl = this.baseUrl ?? config.baseUrl;\n if (!this.dataStorageArg) {\n this.dataStorage = config.dataStorage;\n }\n\n this.http = new BanditoHTTP(baseUrl, apiKey);\n const storePath = this.storePath ?? DEFAULT_STORE_PATH;\n if (storePath !== \":memory:\") {\n const dir = path.dirname(storePath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n }\n this.store = new EventStore(storePath);\n\n // Init S3 exporter if data_storage is \"s3\"\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this._s3Client = null as any;\n this._s3Config = null;\n if (this.dataStorage === \"s3\" && config.s3) {\n try {\n const { S3Client } = await import(\"@aws-sdk/client-s3\");\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const s3ClientConfig: Record<string, any> = { region: config.s3.region };\n if (config.s3.endpoint) {\n s3ClientConfig.endpoint = config.s3.endpoint;\n s3ClientConfig.forcePathStyle = true; // required for MinIO / path-style S3\n }\n this._s3Client = new S3Client(s3ClientConfig);\n this._s3Config = config.s3;\n } catch {\n console.warn(\"[bandito] data_storage='s3' requires @aws-sdk/client-s3\");\n }\n }\n\n // Bootstrap: fetch state, hydrate cache, flush pending\n try {\n const data = await this.http.connect();\n this.applySync(data);\n\n // Reset retry state\n this.deadUuids.clear();\n this.retryCounts.clear();\n\n // Flush pending events from previous crash\n await this.flushPending();\n\n // Start periodic cloud flush (every 30s)\n this.flushInterval = setInterval(() => {\n this.flushPending().catch((err) =>\n console.warn(\"[bandito] Periodic flush error\", err),\n );\n }, 30_000);\n\n // S3 dumps run on a separate 30s timer, independent of cloud flush\n if (this._s3Client !== null) {\n this.s3FlushInterval = setInterval(() => {\n this.dumpPendingToS3().catch((err) =>\n console.warn(\"[bandito] Periodic S3 export error\", err),\n );\n }, 30_000);\n }\n\n this.connected = true;\n } catch (err) {\n this.store?.close();\n this.store = null;\n this.http = null;\n throw err;\n }\n }\n\n /**\n * Local Thompson Sampling decision. Synchronous, <1ms, no network.\n */\n pull(banditName: string, options: PullOptions = {}): PullResult {\n this.ensureConnected();\n\n const cache = this.bandits.get(banditName);\n if (!cache) {\n const available = [...this.bandits.keys()];\n throw new Error(\n `Unknown bandit '${banditName}'. Available: [${available.join(\", \")}]`,\n );\n }\n\n if (cache.arms.length === 0) {\n throw new Error(`Bandit '${banditName}' has no active arms`);\n }\n\n const engine = this.engines.get(banditName)!;\n const queryLength = options.query?.length ?? undefined;\n const excludeIds = options.exclude ? Int32Array.from(options.exclude) : undefined;\n\n const resultJson = engine.pull(queryLength, excludeIds);\n const raw: EnginePullResult = JSON.parse(resultJson);\n\n // Look up the winning arm from our cached active arms\n const winnerArm = cache.arms.find((a) => a.armId === raw.arm_id);\n if (!winnerArm) {\n throw new Error(\n `Engine selected arm ${raw.arm_id} but it's not in active arm cache for \"${banditName}\". ` +\n \"This is likely a bug — please report it at https://github.com/bandito-ai/bandito/issues\",\n );\n }\n\n return createPullResult({\n arm: winnerArm,\n eventId: randomUUID(),\n banditId: cache.banditId,\n banditName,\n scores: raw.scores,\n pullTime: performance.now(),\n });\n }\n\n /**\n * Record an LLM call outcome. Writes to SQLite first (crash-safe),\n * then fires off a non-blocking flush to cloud.\n */\n update(pullResult: PullResult, options: UpdateOptions = {}): void {\n this.ensureConnected();\n\n let reward = options.reward;\n if (options.failed && reward == null) {\n reward = 0.0;\n }\n\n // Auto-calculate latency from pull timestamp\n let latency = options.latency;\n if (latency == null && pullResult._pullTime > 0) {\n latency = performance.now() - pullResult._pullTime;\n }\n\n // Build event payload (snake_case for wire format)\n const event: EventPayload = {\n local_event_uuid: pullResult.eventId,\n bandit_id: pullResult.banditId,\n arm_id: pullResult.arm.armId,\n model_name: pullResult.arm.modelName,\n model_provider: pullResult.arm.modelProvider,\n bandit_name: pullResult.banditName,\n };\n\n if (options.queryText != null) {\n (event as Record<string, unknown>).query_text = options.queryText;\n }\n if (options.response != null) {\n (event as Record<string, unknown>).response =\n typeof options.response === \"string\"\n ? { response: options.response }\n : options.response;\n }\n if (reward != null) {\n (event as Record<string, unknown>).early_reward = reward;\n }\n if (options.cost != null) {\n (event as Record<string, unknown>).cost = options.cost;\n }\n if (latency != null) {\n (event as Record<string, unknown>).latency = latency;\n }\n if (options.inputTokens != null) {\n (event as Record<string, unknown>).input_tokens = options.inputTokens;\n }\n if (options.outputTokens != null) {\n (event as Record<string, unknown>).output_tokens = options.outputTokens;\n }\n if (options.segment != null) {\n (event as Record<string, unknown>).segment = options.segment;\n }\n if (options.failed) {\n (event as Record<string, unknown>).run_error = true;\n }\n\n // Write to SQLite WAL first — survives crashes\n this.store!.push(event);\n\n // Fire-and-forget flush (errors logged inside flushPending)\n this.flushPending().catch((err) =>\n console.warn(\"[bandito] Flush error\", err),\n );\n }\n\n /**\n * Send a human grade for an existing event. Async (HTTP).\n */\n async grade(eventId: string, grade: number): Promise<void> {\n this.ensureConnected();\n await this.http!.submitGrade(eventId, grade);\n }\n\n /**\n * Explicit state refresh from cloud.\n */\n async sync(): Promise<void> {\n this.ensureConnected();\n const data = await this.http!.heartbeat();\n\n const prevBandits = new Map(this.bandits);\n const prevEngines = new Map(this.engines);\n try {\n this.applySync(data);\n } catch (err) {\n // Rollback on malformed response — keep last-known-good state\n this.bandits = prevBandits;\n this.engines = prevEngines;\n console.warn(\"[bandito] Sync response malformed — keeping last-known-good state\", err);\n }\n }\n\n /**\n * Shut down: clear interval, flush remaining events, close connections.\n */\n async close(): Promise<void> {\n if (this.flushInterval) {\n clearInterval(this.flushInterval);\n this.flushInterval = null;\n }\n\n if (this.s3FlushInterval) {\n clearInterval(this.s3FlushInterval);\n this.s3FlushInterval = null;\n }\n\n // Final cloud flush\n if (this.store && this.http) {\n await this.flushPending();\n }\n\n // Final S3 drain — catches events not yet exported by the periodic timer\n if (this._s3Client !== null && this.store) {\n await this.dumpPendingToS3();\n }\n\n this.store?.close();\n this.store = null;\n this.http = null;\n this.engines.clear();\n this.bandits.clear();\n this.connected = false;\n }\n\n // --- Internal ---\n\n private ensureConnected(): void {\n if (!this.connected) {\n throw new Error(\"Not connected — call connect() first\");\n }\n }\n\n private applySync(data: Record<string, unknown>): void {\n const banditsData = (data.bandits ?? []) as Record<string, unknown>[];\n\n const newBandits = new Map<string, BanditCache>();\n const newEngines = new Map<string, BanditEngine>();\n\n for (const b of banditsData) {\n const arms = (b.arms ?? []) as ArmWire[];\n if (arms.length === 0) continue;\n\n const activeArms: Arm[] = arms\n .filter((a) => a.is_active)\n .map((a) => createArm(a));\n\n const name = b.name as string;\n const cache: BanditCache = {\n banditId: Number(b.bandit_id),\n name,\n arms: activeArms,\n armWire: arms,\n optimizationMode: (b.optimization_mode as string) ?? \"base\",\n avgLatencyLastN: b.avg_latency_last_n as number | null,\n budget: b.budget as number | null,\n totalCost: b.total_cost as number | null,\n };\n\n newBandits.set(name, cache);\n\n // Reuse existing engine (preserves RNG state) or create new one\n const existingEngine = this.engines.get(name);\n const banditJson = JSON.stringify(b);\n if (existingEngine) {\n updateEngine(existingEngine, banditJson);\n newEngines.set(name, existingEngine);\n } else {\n newEngines.set(name, createEngine(banditJson));\n }\n }\n\n this.bandits = newBandits;\n this.engines = newEngines;\n }\n\n private async flushPending(): Promise<void> {\n if (this.flushInProgress || !this.store || !this.http) return;\n this.flushInProgress = true;\n\n try {\n const pending = this.store.pending();\n if (pending.length === 0) return;\n\n // Filter out dead events\n const alive = this.deadUuids.size > 0\n ? pending.filter((e) => !this.deadUuids.has(e.local_event_uuid))\n : pending;\n if (alive.length === 0) return;\n\n // Prepare payload (strip metadata/text as configured)\n const payload = prepareCloudPayload(\n alive as Record<string, unknown>[],\n this.dataStorage !== \"local\",\n );\n\n const result = await this.http.ingestEvents(payload);\n\n // Parse per-event errors\n const errors = (result.errors ?? []) as {\n local_event_uuid?: string;\n reason?: string;\n }[];\n const erroredUuids = new Set(\n errors.map((e) => e.local_event_uuid).filter(Boolean) as string[],\n );\n\n // Update retry counts\n for (const uid of erroredUuids) {\n const count = (this.retryCounts.get(uid) ?? 0) + 1;\n this.retryCounts.set(uid, count);\n if (count >= MAX_EVENT_RETRIES) {\n this.deadUuids.add(uid);\n }\n }\n\n // Mark accepted events as flushed\n const flushedUuids = alive\n .map((e) => e.local_event_uuid)\n .filter((uid) => !erroredUuids.has(uid));\n if (flushedUuids.length > 0 && this.store) {\n this.store.markFlushed(flushedUuids);\n }\n\n } catch (err) {\n // Flush failure is non-fatal — events stay pending for next attempt\n console.warn(\"[bandito] Event flush failed — will retry\", err);\n } finally {\n this.flushInProgress = false;\n }\n }\n\n /**\n * Export un-uploaded events to S3 as raw event JSON. One file per event.\n *\n * Key pattern: {prefix}/{bandit_name}/{YYYY/MM/DD}/{local_event_uuid}.json\n * Body: the raw event object (same structure as SQLite payload column).\n */\n private async dumpPendingToS3(): Promise<void> {\n const eventsWithTs = this.store!.pendingS3(100);\n if (eventsWithTs.length === 0) return;\n\n const { PutObjectCommand } = await import(\"@aws-sdk/client-s3\");\n const uploaded: string[] = [];\n\n try {\n for (const { event, ts } of eventsWithTs) {\n const uuid = event.local_event_uuid as string;\n const rawName = (event.bandit_name as string | undefined) || \"unknown\";\n const banditName = rawName.replace(/[^\\w\\-.]/g, \"_\");\n const date = new Date(ts * 1000);\n const datePath = [\n date.getUTCFullYear(),\n String(date.getUTCMonth() + 1).padStart(2, \"0\"),\n String(date.getUTCDate()).padStart(2, \"0\"),\n ].join(\"/\");\n const key = `${this._s3Config.prefix}/${banditName}/${datePath}/${uuid}.json`;\n\n await this._s3Client.send(new PutObjectCommand({\n Bucket: this._s3Config.bucket,\n Key: key,\n Body: JSON.stringify(event),\n ContentType: \"application/json\",\n }));\n uploaded.push(uuid);\n }\n } catch (err) {\n console.warn(\"[bandito] S3 export failed — will retry\", err);\n } finally {\n if (uploaded.length > 0) {\n this.store!.markS3Exported(uploaded);\n }\n }\n }\n}\n","/**\n * Thin wrapper around the WASM engine import.\n *\n * Handles async WASM initialization (loading .wasm binary happens once)\n * and re-exports the BanditEngine constructor for the client.\n */\n\nimport type { BanditEngine as WasmBanditEngine } from \"../wasm/bandito_engine\";\n\nlet wasmModule: typeof import(\"../wasm/bandito_engine\") | null = null;\n\n/**\n * Initialize the WASM module. Must be called before creating BanditEngine instances.\n * Safe to call multiple times — only loads once.\n */\nexport async function initWasm(): Promise<void> {\n if (wasmModule) return;\n wasmModule = await import(\"../wasm/bandito_engine\");\n}\n\n/**\n * Create a BanditEngine from a sync response JSON string.\n * Seeds the RNG with OS entropy to prevent predictable arm selection.\n * Requires initWasm() to have been called first.\n */\nexport function createEngine(banditJson: string): WasmBanditEngine {\n if (!wasmModule) {\n throw new Error(\"WASM not initialized — call initWasm() first\");\n }\n // Generate 8 bytes of OS entropy and pack into a u64 BigInt seed\n const seedBytes = new Uint8Array(8);\n globalThis.crypto.getRandomValues(seedBytes);\n let seed = 0n;\n for (let i = 0; i < 8; i++) {\n seed |= BigInt(seedBytes[i]) << BigInt(i * 8);\n }\n return wasmModule.BanditEngine.newWithSeed(banditJson, seed);\n}\n\n\n/**\n * Update an existing BanditEngine with new sync response data.\n * Preserves RNG state (avoids the \"always picks same arm\" bug).\n */\nexport function updateEngine(engine: WasmBanditEngine, banditJson: string): void {\n engine.updateFromSync(banditJson);\n}\n\nexport type { WasmBanditEngine as BanditEngine };\n\n/**\n * Pull result parsed from engine JSON output.\n */\nexport interface EnginePullResult {\n arm_index: number;\n arm_id: number;\n scores: Record<number, number>;\n}\n","/**\n * SDK types: Arm, PullResult, and internal cache structures.\n */\n\n/** An arm returned to the user after pull(). Immutable. */\nexport interface Arm {\n readonly armId: number;\n readonly modelName: string;\n readonly modelProvider: string;\n readonly systemPrompt: string;\n readonly isPromptTemplated: boolean;\n /** Convenience alias for modelName. */\n readonly model: string;\n /** Convenience alias for systemPrompt. */\n readonly prompt: string;\n}\n\n/** Returned by pull(), passed to update(). Immutable. */\nexport interface PullResult {\n readonly arm: Arm;\n readonly eventId: string;\n readonly banditId: number;\n readonly banditName: string;\n readonly scores: Readonly<Record<number, number>>;\n /** Convenience reach-through to arm.modelName. */\n readonly model: string;\n /** Convenience reach-through to arm.systemPrompt. */\n readonly prompt: string;\n /** @internal perf timestamp */\n readonly _pullTime: number;\n}\n\n/** Create a frozen Arm from raw wire data. */\nexport function createArm(data: {\n arm_id: number;\n model_name: string;\n model_provider: string;\n system_prompt: string;\n is_prompt_templated?: boolean;\n}): Arm {\n const arm: Arm = {\n armId: data.arm_id,\n modelName: data.model_name,\n modelProvider: data.model_provider,\n systemPrompt: data.system_prompt,\n isPromptTemplated: data.is_prompt_templated ?? false,\n get model() {\n return this.modelName;\n },\n get prompt() {\n return this.systemPrompt;\n },\n };\n return Object.freeze(arm);\n}\n\n/** Create a frozen PullResult. */\nexport function createPullResult(data: {\n arm: Arm;\n eventId: string;\n banditId: number;\n banditName: string;\n scores: Record<number, number>;\n pullTime: number;\n}): PullResult {\n const result: PullResult = {\n arm: data.arm,\n eventId: data.eventId,\n banditId: data.banditId,\n banditName: data.banditName,\n scores: Object.freeze({ ...data.scores }),\n get model() {\n return this.arm.modelName;\n },\n get prompt() {\n return this.arm.systemPrompt;\n },\n _pullTime: data.pullTime,\n };\n return Object.freeze(result);\n}\n\n/** Raw arm data from sync response (snake_case wire format). */\nexport interface ArmWire {\n arm_id: number;\n model_name: string;\n model_provider: string;\n system_prompt: string;\n is_prompt_templated: boolean;\n is_active: boolean;\n avg_latency_last_n: number | null;\n}\n\n/** Internal mutable cache for a bandit's state. */\nexport interface BanditCache {\n banditId: number;\n name: string;\n arms: Arm[]; // active only\n armWire: ArmWire[]; // all arms (for engine JSON)\n optimizationMode: string;\n avgLatencyLastN: number | null;\n budget: number | null;\n totalCost: number | null;\n}\n","/**\n * Config loader — reads ~/.bandito/config.toml and env vars.\n *\n * Priority order (highest wins): constructor args → env vars → TOML → defaults.\n * Same file as the Python SDK uses.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { parse as parseToml } from \"smol-toml\";\n\nexport const DEFAULT_BASE_URL = \"https://bandito-api.onrender.com\";\nconst CONFIG_DIR = path.join(os.homedir(), \".bandito\");\nconst CONFIG_FILE = path.join(CONFIG_DIR, \"config.toml\");\n\nexport interface S3Config {\n bucket: string;\n prefix: string;\n region: string;\n /** Optional custom endpoint for MinIO / LocalStack / other S3-compatible stores. */\n endpoint?: string;\n}\n\nexport interface JudgeConfig {\n apiKey: string | null;\n model: string;\n}\n\nexport interface BanditoConfig {\n apiKey: string | null;\n baseUrl: string;\n dataStorage: string; // \"local\", \"cloud\", or \"s3\"\n s3: S3Config | null;\n judge: JudgeConfig;\n}\n\n/**\n * Load config from TOML file + env var overrides.\n */\nexport function loadConfig(): BanditoConfig {\n const config: BanditoConfig = {\n apiKey: null,\n baseUrl: DEFAULT_BASE_URL,\n dataStorage: \"local\",\n s3: null,\n judge: { apiKey: null, model: \"gpt-4o-mini\" },\n };\n\n // TOML file first\n if (fs.existsSync(CONFIG_FILE)) {\n try {\n const content = fs.readFileSync(CONFIG_FILE, \"utf-8\");\n const data = parseToml(content) as Record<string, unknown>;\n if (data.api_key) config.apiKey = data.api_key as string;\n if (data.base_url) config.baseUrl = data.base_url as string;\n if (data.data_storage) config.dataStorage = data.data_storage as string;\n const s3Data = data.s3 as Record<string, unknown> | undefined;\n const s3Bucket = ((s3Data?.bucket as string | undefined) ?? \"\").trim();\n if (s3Bucket) {\n const s3Endpoint = ((s3Data?.endpoint as string | undefined) ?? \"\").trim() || undefined;\n config.s3 = {\n bucket: s3Bucket,\n prefix: ((s3Data?.prefix as string | undefined) ?? \"\").trim() || \"bandito\",\n region: ((s3Data?.region as string | undefined) ?? \"\").trim() || \"us-east-1\",\n ...(s3Endpoint ? { endpoint: s3Endpoint } : {}),\n };\n }\n const judgeData = data.judge as Record<string, unknown> | undefined;\n if (judgeData?.api_key) config.judge.apiKey = judgeData.api_key as string;\n if (judgeData?.model) config.judge.model = judgeData.model as string;\n } catch {\n // Failed to parse TOML — fall back to env vars\n }\n }\n\n // Env vars override\n const envKey = process.env.BANDITO_API_KEY;\n if (envKey) config.apiKey = envKey;\n\n const envUrl = process.env.BANDITO_BASE_URL;\n if (envUrl) config.baseUrl = envUrl;\n\n const envStorage = process.env.BANDITO_DATA_STORAGE;\n if (envStorage) config.dataStorage = envStorage;\n\n // S3 env vars (useful for container deployments without a config file)\n const envS3Bucket = (process.env.BANDITO_S3_BUCKET ?? \"\").trim();\n if (envS3Bucket) {\n const envEndpoint = (process.env.BANDITO_S3_ENDPOINT ?? config.s3?.endpoint ?? \"\").trim() || undefined;\n config.s3 = {\n bucket: envS3Bucket,\n prefix: (process.env.BANDITO_S3_PREFIX ?? config.s3?.prefix ?? \"\").trim() || \"bandito\",\n region: (process.env.BANDITO_S3_REGION ?? config.s3?.region ?? \"\").trim() || \"us-east-1\",\n ...(envEndpoint ? { endpoint: envEndpoint } : {}),\n };\n // Setting BANDITO_S3_BUCKET implicitly activates s3 mode unless\n // the user has explicitly chosen a different mode via BANDITO_DATA_STORAGE.\n if (!process.env.BANDITO_DATA_STORAGE) {\n config.dataStorage = \"s3\";\n }\n } else if (config.s3) {\n const envPrefix = (process.env.BANDITO_S3_PREFIX ?? \"\").trim();\n const envRegion = (process.env.BANDITO_S3_REGION ?? \"\").trim();\n const envEndpoint = (process.env.BANDITO_S3_ENDPOINT ?? \"\").trim();\n if (envPrefix) config.s3.prefix = envPrefix;\n if (envRegion) config.s3.region = envRegion;\n if (envEndpoint) config.s3.endpoint = envEndpoint;\n }\n\n const envJudgeKey = process.env.JUDGE_API_KEY;\n if (envJudgeKey) config.judge.apiKey = envJudgeKey;\n\n return config;\n}\n","/**\n * HTTP transport — fetch-based client for cloud API.\n *\n * Retry config: 3 attempts, exponential backoff (0.5s, 1s, 2s),\n * retries only 5xx and network errors. Never retries 4xx.\n */\n\nconst MAX_RETRIES = 3;\nconst RETRY_BACKOFF_BASE = 500; // ms — 500, 1000, 2000\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction isRetryable(status: number): boolean {\n return status >= 500;\n}\n\nexport class BanditoHTTP {\n private baseUrl: string;\n private apiKey: string;\n private timeout: number;\n\n constructor(baseUrl: string, apiKey: string, timeout: number = 10_000) {\n // Validate URL scheme — reject non-http(s) protocols (file://, javascript:, etc.)\n let parsed: URL;\n try {\n parsed = new URL(baseUrl);\n } catch {\n throw new Error(`Invalid baseUrl: \"${baseUrl}\"`);\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n throw new Error(\n `baseUrl must use http or https, got \"${parsed.protocol}\" — check your config`,\n );\n }\n this.baseUrl = `${baseUrl.replace(/\\/$/, \"\")}/api/v1`;\n this.apiKey = apiKey;\n this.timeout = timeout;\n }\n\n private async request(\n method: string,\n path: string,\n body?: unknown,\n ): Promise<Record<string, unknown>> {\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const resp = await fetch(`${this.baseUrl}${path}`, {\n method,\n headers: {\n \"X-API-Key\": this.apiKey,\n \"Content-Type\": \"application/json\",\n },\n body: body != null ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timer);\n\n if (!resp.ok) {\n const text = await resp.text().catch(() => \"\");\n // Truncate body to avoid leaking sensitive content into error messages/logs\n const preview = text.length > 200 ? `${text.slice(0, 200)}…` : text;\n if (!isRetryable(resp.status) || attempt === MAX_RETRIES - 1) {\n throw new Error(\n `HTTP ${resp.status} on ${method} ${path}: ${preview}`,\n );\n }\n // Retryable server error — fall through to retry\n lastError = new Error(\n `HTTP ${resp.status} on ${method} ${path}: ${preview}`,\n );\n } else {\n return (await resp.json()) as Record<string, unknown>;\n }\n } catch (err) {\n clearTimeout(timer);\n lastError = err as Error;\n\n // AbortError = timeout, TypeError = network error (in fetch)\n const isNetworkOrTimeout =\n (err as Error).name === \"AbortError\" ||\n (err as Error).name === \"TypeError\";\n if (!isNetworkOrTimeout && attempt < MAX_RETRIES - 1) {\n // Non-retryable (4xx already handled above)\n throw err;\n }\n if (attempt === MAX_RETRIES - 1) {\n throw err;\n }\n }\n\n // Exponential backoff\n const delay = RETRY_BACKOFF_BASE * 2 ** attempt;\n await sleep(delay);\n }\n\n throw lastError!;\n }\n\n /** POST /sync/connect — SDK bootstrap. */\n async connect(): Promise<Record<string, unknown>> {\n return this.request(\"POST\", \"/sync/connect\");\n }\n\n /** POST /sync/heartbeat — periodic state refresh. */\n async heartbeat(): Promise<Record<string, unknown>> {\n return this.request(\"POST\", \"/sync/heartbeat\", {});\n }\n\n /** POST /events — batch event ingestion. */\n async ingestEvents(\n events: Record<string, unknown>[],\n ): Promise<Record<string, unknown>> {\n return this.request(\"POST\", \"/events\", { events });\n }\n\n /** PATCH /events/{uuid}/grade — submit human grade. */\n async submitGrade(\n eventUuid: string,\n grade: number,\n ): Promise<Record<string, unknown>> {\n return this.request(\"PATCH\", `/events/${eventUuid}/grade`, {\n grade,\n is_graded: true,\n });\n }\n}\n","/**\n * SQLite WAL durability layer — crash-safe event storage.\n *\n * Events are written here immediately after update(). Background flush\n * sends them to cloud. If the process crashes, pending events survive\n * and are retried on next connect().\n */\n\nimport Database from \"better-sqlite3\";\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS events (\n local_event_uuid TEXT PRIMARY KEY,\n bandit_id INTEGER NOT NULL,\n arm_id INTEGER NOT NULL,\n payload TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n created_at REAL NOT NULL,\n human_reward REAL,\n graded_at REAL,\n s3_exported INTEGER NOT NULL DEFAULT 0\n);\nCREATE INDEX IF NOT EXISTS idx_events_status ON events(status);\n`;\n\nconst MIGRATION_GRADING = [\n \"ALTER TABLE events ADD COLUMN human_reward REAL\",\n \"ALTER TABLE events ADD COLUMN graded_at REAL\",\n \"ALTER TABLE events ADD COLUMN s3_exported INTEGER NOT NULL DEFAULT 0\",\n];\n\nexport interface EventPayload {\n local_event_uuid: string;\n bandit_id: number;\n arm_id: number;\n model_name: string;\n model_provider: string;\n bandit_name?: string;\n [key: string]: unknown;\n}\n\nexport class EventStore {\n private db: Database.Database;\n private pushStmt: Database.Statement;\n private pendingStmt: Database.Statement;\n\n constructor(dbPath: string = \":memory:\") {\n this.db = new Database(dbPath);\n this.db.pragma(\"journal_mode = WAL\");\n this.db.pragma(\"busy_timeout = 5000\");\n this.db.pragma(\"synchronous = NORMAL\");\n this.db.exec(SCHEMA);\n this.migrate();\n\n // Pre-compile frequently-used statements\n this.pushStmt = this.db.prepare(\n `INSERT OR IGNORE INTO events\n (local_event_uuid, bandit_id, arm_id, payload, status, created_at)\n VALUES (?, ?, ?, ?, 'pending', ?)`,\n );\n this.pendingStmt = this.db.prepare(\n `SELECT local_event_uuid, payload FROM events WHERE status = 'pending'\n ORDER BY created_at ASC LIMIT ?`,\n );\n }\n\n /** Insert a pending event. */\n push(event: EventPayload): void {\n this.pushStmt.run(\n event.local_event_uuid,\n event.bandit_id,\n event.arm_id,\n JSON.stringify(event),\n Date.now() / 1000,\n );\n }\n\n /** Return up to `limit` pending events (oldest first). */\n pending(limit: number = 50): EventPayload[] {\n const rows = this.pendingStmt.all(limit) as {\n local_event_uuid: string;\n payload: string;\n }[];\n const results: EventPayload[] = [];\n const corrupt: string[] = [];\n for (const row of rows) {\n try {\n results.push(JSON.parse(row.payload) as EventPayload);\n } catch {\n console.warn(\n `[bandito] Discarding corrupt event ${row.local_event_uuid} from store`,\n );\n corrupt.push(row.local_event_uuid);\n }\n }\n if (corrupt.length > 0) {\n this.markFlushed(corrupt);\n }\n return results;\n }\n\n /** Mark events as successfully flushed to cloud. */\n markFlushed(uuids: string[]): void {\n if (uuids.length === 0) return;\n const placeholders = uuids.map(() => \"?\").join(\",\");\n this.db\n .prepare(\n `UPDATE events SET status = 'flushed'\n WHERE local_event_uuid IN (${placeholders})`,\n )\n .run(...uuids);\n }\n\n /** Record a human grade locally. */\n markGraded(uuid: string, reward: number): void {\n this.db\n .prepare(\n `UPDATE events SET human_reward = ?, graded_at = ?\n WHERE local_event_uuid = ?`,\n )\n .run(reward, Date.now() / 1000, uuid);\n }\n\n /** Return un-exported events as {event, ts} for S3 dump. */\n pendingS3(limit: number = 100): Array<{ event: Record<string, unknown>; ts: number }> {\n const rows = this.db\n .prepare(\n `SELECT payload, created_at FROM events WHERE s3_exported = 0\n ORDER BY created_at ASC LIMIT ?`,\n )\n .all(limit) as { payload: string; created_at: number }[];\n return rows.map((r) => ({ event: JSON.parse(r.payload) as Record<string, unknown>, ts: r.created_at }));\n }\n\n /** Mark events as successfully exported to S3. */\n markS3Exported(uuids: string[]): void {\n if (uuids.length === 0) return;\n const placeholders = uuids.map(() => \"?\").join(\",\");\n this.db\n .prepare(`UPDATE events SET s3_exported = 1 WHERE local_event_uuid IN (${placeholders})`)\n .run(...uuids);\n }\n\n /** Close the database connection. */\n close(): void {\n this.db.close();\n }\n\n private migrate(): void {\n for (const stmt of MIGRATION_GRADING) {\n try {\n this.db.exec(stmt);\n } catch {\n // Column already exists\n }\n }\n }\n}\n","/**\n * Payload utilities for cloud event ingestion.\n */\n\nconst TEXT_FIELDS = [\"query_text\", \"response\"] as const;\nconst METADATA_FIELDS = [\"model_name\", \"model_provider\"] as const;\n\n/**\n * Return shallow copies of events ready for cloud ingest.\n *\n * Always strips model_name/model_provider (only needed in local SQLite for TUI).\n * Strips query_text/response when includeText is false (dataStorage=\"local\").\n */\nexport function prepareCloudPayload(\n events: Record<string, unknown>[],\n includeText: boolean,\n): Record<string, unknown>[] {\n return events.map((event) => {\n const copy = { ...event };\n for (const field of METADATA_FIELDS) {\n delete copy[field];\n }\n if (!includeText) {\n for (const field of TEXT_FIELDS) {\n delete copy[field];\n }\n }\n return copy;\n });\n}\n","/**\n * Bandito SDK — contextual bandit optimization for LLM selection.\n *\n * Recommended (explicit client):\n * import { BanditoClient } from 'bandito';\n *\n * const client = new BanditoClient({ apiKey: 'bnd_...' });\n * await client.connect();\n * const result = client.pull('my-chatbot', { query: userMessage });\n * // ... call LLM with result.model, result.prompt ...\n * client.update(result, { response: response.text });\n * await client.close();\n *\n * Module-level singleton (convenience):\n * import { connect, pull, update, close } from 'bandito';\n *\n * await connect({ apiKey: 'bnd_...' });\n * const result = pull('my-chatbot', { query: userMessage });\n * update(result, { response: response.text });\n * await close();\n */\n\nexport { BanditoClient, type ClientOptions, type PullOptions, type UpdateOptions } from \"./client.js\";\nexport { type Arm, type PullResult } from \"./models.js\";\n\nimport { BanditoClient, type ClientOptions, type PullOptions, type UpdateOptions } from \"./client.js\";\nimport type { PullResult } from \"./models.js\";\n\nlet _client: BanditoClient | null = null;\n\nfunction getClient(): BanditoClient {\n if (!_client) {\n throw new Error(\"Not connected — call connect() first\");\n }\n return _client;\n}\n\n/** Connect to the Bandito cloud and hydrate local state. */\nexport async function connect(options: ClientOptions = {}): Promise<void> {\n if (_client) {\n await _client.close();\n }\n _client = new BanditoClient(options);\n await _client.connect();\n}\n\n/** Local Thompson Sampling decision. <1ms, no network. */\nexport function pull(banditName: string, options?: PullOptions): PullResult {\n return getClient().pull(banditName, options);\n}\n\n/** Record an LLM call outcome (writes to SQLite, fire-and-forget flush). */\nexport function update(pullResult: PullResult, options?: UpdateOptions): void {\n getClient().update(pullResult, options);\n}\n\n/** Send a human grade for an existing event. */\nexport async function grade(eventId: string, gradeValue: number): Promise<void> {\n await getClient().grade(eventId, gradeValue);\n}\n\n/** Explicit state refresh from cloud. */\nexport async function sync(): Promise<void> {\n await getClient().sync();\n}\n\n/** Shut down: flush events, close connections. */\nexport async function close(): Promise<void> {\n if (_client) {\n await _client.close();\n _client = null;\n }\n}\n"],"mappings":"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","names":["fs","path","os","path","grade","grade"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/engine.ts","../src/models.ts","../src/config.ts","../src/http.ts","../src/store.ts","../src/worker.ts","../src/index.ts"],"sourcesContent":["/**\n * BanditoClient — main orchestrator for the JS/TS SDK.\n *\n * Mirrors the Python SDK's sync-first design:\n * - pull() is synchronous (WASM math, <1ms)\n * - connect(), grade(), sync(), close() are async (HTTP I/O)\n * - update() is synchronous (SQLite write + fire-and-forget flush)\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { performance } from \"node:perf_hooks\";\n\nimport { initWasm, createEngine, updateEngine, type BanditEngine, type EnginePullResult } from \"./engine.js\";\nimport {\n type Arm,\n type PullResult,\n type BanditCache,\n type ArmWire,\n createArm,\n createPullResult,\n} from \"./models.js\";\nimport { loadConfig, DEFAULT_BASE_URL } from \"./config.js\";\nimport { BanditoHTTP } from \"./http.js\";\nimport { EventStore, type EventPayload } from \"./store.js\";\nimport { prepareCloudPayload } from \"./worker.js\";\n\nconst DEFAULT_STORE_PATH = path.join(os.homedir(), \".bandito\", \"events.db\");\nconst MAX_EVENT_RETRIES = 5;\n\nexport interface ClientOptions {\n apiKey?: string;\n baseUrl?: string;\n storePath?: string;\n dataStorage?: string;\n}\n\nexport interface PullOptions {\n query?: string;\n exclude?: number[];\n}\n\nexport interface UpdateOptions {\n queryText?: string;\n response?: string | Record<string, unknown>;\n reward?: number;\n cost?: number;\n latency?: number;\n inputTokens?: number;\n outputTokens?: number;\n segment?: Record<string, string>;\n failed?: boolean;\n}\n\nexport class BanditoClient {\n private apiKey: string | undefined;\n private baseUrl: string | undefined;\n private storePath: string | undefined;\n private dataStorageArg: string | undefined;\n private dataStorage: string;\n\n private http: BanditoHTTP | null = null;\n private store: EventStore | null = null;\n private engines: Map<string, BanditEngine> = new Map();\n private bandits: Map<string, BanditCache> = new Map();\n private connected = false;\n private flushInterval: ReturnType<typeof setInterval> | null = null;\n private flushInProgress = false;\n private deadUuids: Set<string> = new Set();\n private retryCounts: Map<string, number> = new Map();\n\n constructor(options: ClientOptions = {}) {\n this.apiKey = options.apiKey;\n this.baseUrl = options.baseUrl;\n this.storePath = options.storePath;\n this.dataStorageArg = options.dataStorage;\n this.dataStorage = options.dataStorage ?? \"local\";\n }\n\n /**\n * Bootstrap: authenticate and hydrate in-memory state from cloud.\n *\n * Resolves config from: constructor args → env vars → ~/.bandito/config.toml.\n * Initializes WASM, creates HTTP client, SQLite store, fetches full state.\n */\n async connect(): Promise<void> {\n // Tear down previous connection if reconnecting\n if (this.connected) {\n await this.close();\n }\n\n // Init WASM (loads .wasm binary once)\n await initWasm();\n\n // Resolve config\n const config = loadConfig();\n const apiKey = this.apiKey ?? config.apiKey;\n if (!apiKey) {\n throw new Error(\n \"apiKey required — pass it to constructor, set BANDITO_API_KEY, \" +\n \"or run `bandito signup`\",\n );\n }\n\n const baseUrl = this.baseUrl ?? config.baseUrl;\n if (!this.dataStorageArg) {\n this.dataStorage = config.dataStorage;\n }\n\n this.http = new BanditoHTTP(baseUrl, apiKey);\n const storePath = this.storePath ?? DEFAULT_STORE_PATH;\n if (storePath !== \":memory:\") {\n const dir = path.dirname(storePath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n }\n this.store = new EventStore(storePath);\n\n // Bootstrap: fetch state, hydrate cache, flush pending\n try {\n const data = await this.http.connect();\n this.applySync(data);\n\n // Reset retry state\n this.deadUuids.clear();\n this.retryCounts.clear();\n\n // Flush pending events from previous crash\n await this.flushPending();\n\n // Start periodic flush (every 30s)\n this.flushInterval = setInterval(() => {\n this.flushPending().catch(() => {});\n }, 30_000);\n\n this.connected = true;\n } catch (err) {\n this.store?.close();\n this.store = null;\n this.http = null;\n throw err;\n }\n }\n\n /**\n * Local Thompson Sampling decision. Synchronous, <1ms, no network.\n */\n pull(banditName: string, options: PullOptions = {}): PullResult {\n this.ensureConnected();\n\n const cache = this.bandits.get(banditName);\n if (!cache) {\n const available = [...this.bandits.keys()];\n throw new Error(\n `Unknown bandit '${banditName}'. Available: [${available.join(\", \")}]`,\n );\n }\n\n if (cache.arms.length === 0) {\n throw new Error(`Bandit '${banditName}' has no active arms`);\n }\n\n const engine = this.engines.get(banditName)!;\n const queryLength = options.query?.length ?? undefined;\n const excludeIds = options.exclude ? Int32Array.from(options.exclude) : undefined;\n\n const resultJson = engine.pull(queryLength, excludeIds);\n const raw: EnginePullResult = JSON.parse(resultJson);\n\n // Look up the winning arm from our cached active arms\n const winnerArm = cache.arms.find((a) => a.armId === raw.arm_id);\n if (!winnerArm) {\n throw new Error(\n `Engine selected arm ${raw.arm_id} but it's not in active arm cache for \"${banditName}\". ` +\n \"This is likely a bug — please report it at https://github.com/bandito-ai/bandito/issues\",\n );\n }\n\n return createPullResult({\n arm: winnerArm,\n eventId: randomUUID(),\n banditId: cache.banditId,\n banditName,\n scores: raw.scores,\n pullTime: performance.now(),\n });\n }\n\n /**\n * Record an LLM call outcome. Writes to SQLite first (crash-safe),\n * then fires off a non-blocking flush to cloud.\n */\n update(pullResult: PullResult, options: UpdateOptions = {}): void {\n this.ensureConnected();\n\n let reward = options.reward;\n if (options.failed && reward == null) {\n reward = 0.0;\n }\n\n // Auto-calculate latency from pull timestamp\n let latency = options.latency;\n if (latency == null && pullResult._pullTime > 0) {\n latency = performance.now() - pullResult._pullTime;\n }\n\n // Build event payload (snake_case for wire format)\n const event: EventPayload = {\n local_event_uuid: pullResult.eventId,\n bandit_id: pullResult.banditId,\n arm_id: pullResult.arm.armId,\n model_name: pullResult.arm.modelName,\n model_provider: pullResult.arm.modelProvider,\n };\n\n if (options.queryText != null) {\n (event as Record<string, unknown>).query_text = options.queryText;\n }\n if (options.response != null) {\n (event as Record<string, unknown>).response =\n typeof options.response === \"string\"\n ? { response: options.response }\n : options.response;\n }\n if (reward != null) {\n (event as Record<string, unknown>).early_reward = reward;\n }\n if (options.cost != null) {\n (event as Record<string, unknown>).cost = options.cost;\n }\n if (latency != null) {\n (event as Record<string, unknown>).latency = latency;\n }\n if (options.inputTokens != null) {\n (event as Record<string, unknown>).input_tokens = options.inputTokens;\n }\n if (options.outputTokens != null) {\n (event as Record<string, unknown>).output_tokens = options.outputTokens;\n }\n if (options.segment != null) {\n (event as Record<string, unknown>).segment = options.segment;\n }\n if (options.failed) {\n (event as Record<string, unknown>).run_error = true;\n }\n\n // Write to SQLite WAL first — survives crashes\n this.store!.push(event);\n\n // Fire-and-forget flush (errors logged inside flushPending)\n this.flushPending().catch(() => {});\n }\n\n /**\n * Send a human grade for an existing event. Async (HTTP).\n */\n async grade(eventId: string, grade: number): Promise<void> {\n this.ensureConnected();\n await this.http!.submitGrade(eventId, grade);\n }\n\n /**\n * Explicit state refresh from cloud.\n */\n async sync(): Promise<void> {\n this.ensureConnected();\n const data = await this.http!.heartbeat();\n\n const prevBandits = new Map(this.bandits);\n const prevEngines = new Map(this.engines);\n try {\n this.applySync(data);\n } catch (err) {\n // Rollback on malformed response — keep last-known-good state\n this.bandits = prevBandits;\n this.engines = prevEngines;\n console.warn(\"[bandito] Sync response malformed — keeping last-known-good state\", err);\n }\n }\n\n /**\n * Shut down: clear interval, flush remaining events, close connections.\n */\n async close(): Promise<void> {\n if (this.flushInterval) {\n clearInterval(this.flushInterval);\n this.flushInterval = null;\n }\n\n // Final flush\n if (this.store && this.http) {\n await this.flushPending();\n }\n\n this.store?.close();\n this.store = null;\n this.http = null;\n this.engines.clear();\n this.bandits.clear();\n this.connected = false;\n }\n\n // --- Internal ---\n\n private ensureConnected(): void {\n if (!this.connected) {\n throw new Error(\"Not connected — call connect() first\");\n }\n }\n\n private applySync(data: Record<string, unknown>): void {\n const banditsData = (data.bandits ?? []) as Record<string, unknown>[];\n\n const newBandits = new Map<string, BanditCache>();\n const newEngines = new Map<string, BanditEngine>();\n\n for (const b of banditsData) {\n const arms = (b.arms ?? []) as ArmWire[];\n if (arms.length === 0) continue;\n\n const activeArms: Arm[] = arms\n .filter((a) => a.is_active)\n .map((a) => createArm(a));\n\n const name = b.name as string;\n const cache: BanditCache = {\n banditId: Number(b.bandit_id),\n name,\n arms: activeArms,\n armWire: arms,\n optimizationMode: (b.optimization_mode as string) ?? \"base\",\n avgLatencyLastN: b.avg_latency_last_n as number | null,\n budget: b.budget as number | null,\n totalCost: b.total_cost as number | null,\n };\n\n newBandits.set(name, cache);\n\n // Reuse existing engine (preserves RNG state) or create new one\n const existingEngine = this.engines.get(name);\n const banditJson = JSON.stringify(b);\n if (existingEngine) {\n updateEngine(existingEngine, banditJson);\n newEngines.set(name, existingEngine);\n } else {\n newEngines.set(name, createEngine(banditJson));\n }\n }\n\n this.bandits = newBandits;\n this.engines = newEngines;\n }\n\n private async flushPending(): Promise<void> {\n if (this.flushInProgress || !this.store || !this.http) return;\n this.flushInProgress = true;\n\n try {\n const pending = this.store.pending();\n if (pending.length === 0) return;\n\n // Filter out dead events\n const alive = this.deadUuids.size > 0\n ? pending.filter((e) => !this.deadUuids.has(e.local_event_uuid))\n : pending;\n if (alive.length === 0) return;\n\n // Prepare payload (strip metadata/text as configured)\n const payload = prepareCloudPayload(\n alive as Record<string, unknown>[],\n this.dataStorage !== \"local\",\n );\n\n const result = await this.http.ingestEvents(payload);\n\n // Parse per-event errors\n const errors = (result.errors ?? []) as {\n local_event_uuid?: string;\n reason?: string;\n }[];\n const erroredUuids = new Set(\n errors.map((e) => e.local_event_uuid).filter(Boolean) as string[],\n );\n\n // Update retry counts\n for (const uid of erroredUuids) {\n const count = (this.retryCounts.get(uid) ?? 0) + 1;\n this.retryCounts.set(uid, count);\n if (count >= MAX_EVENT_RETRIES) {\n this.deadUuids.add(uid);\n }\n }\n\n // Mark accepted events as flushed\n const flushedUuids = alive\n .map((e) => e.local_event_uuid)\n .filter((uid) => !erroredUuids.has(uid));\n if (flushedUuids.length > 0 && this.store) {\n this.store.markFlushed(flushedUuids);\n }\n } catch (err) {\n // Flush failure is non-fatal — events stay pending for next attempt\n console.warn(\"[bandito] Event flush failed — will retry\", err);\n } finally {\n this.flushInProgress = false;\n }\n }\n}\n","/**\n * Thin wrapper around the WASM engine import.\n *\n * Handles async WASM initialization (loading .wasm binary happens once)\n * and re-exports the BanditEngine constructor for the client.\n */\n\nimport type { BanditEngine as WasmBanditEngine } from \"../wasm/bandito_engine\";\n\nlet wasmModule: typeof import(\"../wasm/bandito_engine\") | null = null;\n\n/**\n * Initialize the WASM module. Must be called before creating BanditEngine instances.\n * Safe to call multiple times — only loads once.\n */\nexport async function initWasm(): Promise<void> {\n if (wasmModule) return;\n wasmModule = await import(\"../wasm/bandito_engine\");\n}\n\n/**\n * Create a BanditEngine from a sync response JSON string.\n * Requires initWasm() to have been called first.\n */\nexport function createEngine(banditJson: string): WasmBanditEngine {\n if (!wasmModule) {\n throw new Error(\"WASM not initialized — call initWasm() first\");\n }\n return new wasmModule.BanditEngine(banditJson);\n}\n\n/**\n * Update an existing BanditEngine with new sync response data.\n * Preserves RNG state (avoids the \"always picks same arm\" bug).\n */\nexport function updateEngine(engine: WasmBanditEngine, banditJson: string): void {\n engine.updateFromSync(banditJson);\n}\n\nexport type { WasmBanditEngine as BanditEngine };\n\n/**\n * Pull result parsed from engine JSON output.\n */\nexport interface EnginePullResult {\n arm_index: number;\n arm_id: number;\n scores: Record<number, number>;\n}\n","/**\n * SDK types: Arm, PullResult, and internal cache structures.\n */\n\n/** An arm returned to the user after pull(). Immutable. */\nexport interface Arm {\n readonly armId: number;\n readonly modelName: string;\n readonly modelProvider: string;\n readonly systemPrompt: string;\n readonly isPromptTemplated: boolean;\n /** Convenience alias for modelName. */\n readonly model: string;\n /** Convenience alias for systemPrompt. */\n readonly prompt: string;\n}\n\n/** Returned by pull(), passed to update(). Immutable. */\nexport interface PullResult {\n readonly arm: Arm;\n readonly eventId: string;\n readonly banditId: number;\n readonly banditName: string;\n readonly scores: Readonly<Record<number, number>>;\n /** Convenience reach-through to arm.modelName. */\n readonly model: string;\n /** Convenience reach-through to arm.systemPrompt. */\n readonly prompt: string;\n /** @internal perf timestamp */\n readonly _pullTime: number;\n}\n\n/** Create a frozen Arm from raw wire data. */\nexport function createArm(data: {\n arm_id: number;\n model_name: string;\n model_provider: string;\n system_prompt: string;\n is_prompt_templated?: boolean;\n}): Arm {\n const arm: Arm = {\n armId: data.arm_id,\n modelName: data.model_name,\n modelProvider: data.model_provider,\n systemPrompt: data.system_prompt,\n isPromptTemplated: data.is_prompt_templated ?? false,\n get model() {\n return this.modelName;\n },\n get prompt() {\n return this.systemPrompt;\n },\n };\n return Object.freeze(arm);\n}\n\n/** Create a frozen PullResult. */\nexport function createPullResult(data: {\n arm: Arm;\n eventId: string;\n banditId: number;\n banditName: string;\n scores: Record<number, number>;\n pullTime: number;\n}): PullResult {\n const result: PullResult = {\n arm: data.arm,\n eventId: data.eventId,\n banditId: data.banditId,\n banditName: data.banditName,\n scores: Object.freeze({ ...data.scores }),\n get model() {\n return this.arm.modelName;\n },\n get prompt() {\n return this.arm.systemPrompt;\n },\n _pullTime: data.pullTime,\n };\n return Object.freeze(result);\n}\n\n/** Raw arm data from sync response (snake_case wire format). */\nexport interface ArmWire {\n arm_id: number;\n model_name: string;\n model_provider: string;\n system_prompt: string;\n is_prompt_templated: boolean;\n is_active: boolean;\n avg_latency_last_n: number | null;\n}\n\n/** Internal mutable cache for a bandit's state. */\nexport interface BanditCache {\n banditId: number;\n name: string;\n arms: Arm[]; // active only\n armWire: ArmWire[]; // all arms (for engine JSON)\n optimizationMode: string;\n avgLatencyLastN: number | null;\n budget: number | null;\n totalCost: number | null;\n}\n","/**\n * Config loader — reads ~/.bandito/config.toml and env vars.\n *\n * Resolution order: constructor args → env vars → TOML → defaults.\n * Same file as the Python SDK uses.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { parse as parseToml } from \"smol-toml\";\n\nexport const DEFAULT_BASE_URL = \"https://bandito-api.onrender.com\";\nconst CONFIG_DIR = path.join(os.homedir(), \".bandito\");\nconst CONFIG_FILE = path.join(CONFIG_DIR, \"config.toml\");\n\nexport interface BanditoConfig {\n apiKey: string | null;\n baseUrl: string;\n dataStorage: string; // \"local\" or \"cloud\"\n}\n\n/**\n * Load config from TOML file + env var overrides.\n */\nexport function loadConfig(): BanditoConfig {\n const config: BanditoConfig = {\n apiKey: null,\n baseUrl: DEFAULT_BASE_URL,\n dataStorage: \"local\",\n };\n\n // TOML file first\n if (fs.existsSync(CONFIG_FILE)) {\n try {\n const content = fs.readFileSync(CONFIG_FILE, \"utf-8\");\n const data = parseToml(content);\n if (data.api_key) config.apiKey = data.api_key;\n if (data.base_url) config.baseUrl = data.base_url;\n if (data.data_storage) config.dataStorage = data.data_storage;\n } catch {\n // Failed to parse TOML — fall back to env vars\n }\n }\n\n // Env vars override\n const envKey = process.env.BANDITO_API_KEY;\n if (envKey) config.apiKey = envKey;\n\n const envUrl = process.env.BANDITO_BASE_URL;\n if (envUrl) config.baseUrl = envUrl;\n\n const envStorage = process.env.BANDITO_DATA_STORAGE;\n if (envStorage) config.dataStorage = envStorage;\n\n return config;\n}\n","/**\n * HTTP transport — fetch-based client for cloud API.\n *\n * Retry config: 3 attempts, exponential backoff (0.5s, 1s, 2s),\n * retries only 5xx and network errors. Never retries 4xx.\n */\n\nconst MAX_RETRIES = 3;\nconst RETRY_BACKOFF_BASE = 500; // ms — 500, 1000, 2000\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction isRetryable(status: number): boolean {\n return status >= 500;\n}\n\nexport class BanditoHTTP {\n private baseUrl: string;\n private apiKey: string;\n private timeout: number;\n\n constructor(baseUrl: string, apiKey: string, timeout: number = 10_000) {\n this.baseUrl = `${baseUrl.replace(/\\/$/, \"\")}/api/v1`;\n this.apiKey = apiKey;\n this.timeout = timeout;\n }\n\n private async request(\n method: string,\n path: string,\n body?: unknown,\n ): Promise<Record<string, unknown>> {\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const resp = await fetch(`${this.baseUrl}${path}`, {\n method,\n headers: {\n \"X-API-Key\": this.apiKey,\n \"Content-Type\": \"application/json\",\n },\n body: body != null ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timer);\n\n if (!resp.ok) {\n const text = await resp.text().catch(() => \"\");\n if (!isRetryable(resp.status) || attempt === MAX_RETRIES - 1) {\n throw new Error(\n `HTTP ${resp.status} on ${method} ${path}: ${text}`,\n );\n }\n // Retryable server error — fall through to retry\n lastError = new Error(\n `HTTP ${resp.status} on ${method} ${path}: ${text}`,\n );\n } else {\n return (await resp.json()) as Record<string, unknown>;\n }\n } catch (err) {\n clearTimeout(timer);\n lastError = err as Error;\n\n // AbortError = timeout, TypeError = network error (in fetch)\n const isNetworkOrTimeout =\n (err as Error).name === \"AbortError\" ||\n (err as Error).name === \"TypeError\";\n if (!isNetworkOrTimeout && attempt < MAX_RETRIES - 1) {\n // Non-retryable (4xx already handled above)\n throw err;\n }\n if (attempt === MAX_RETRIES - 1) {\n throw err;\n }\n }\n\n // Exponential backoff\n const delay = RETRY_BACKOFF_BASE * 2 ** attempt;\n await sleep(delay);\n }\n\n throw lastError!;\n }\n\n /** POST /sync/connect — SDK bootstrap. */\n async connect(): Promise<Record<string, unknown>> {\n return this.request(\"POST\", \"/sync/connect\");\n }\n\n /** POST /sync/heartbeat — periodic state refresh. */\n async heartbeat(): Promise<Record<string, unknown>> {\n return this.request(\"POST\", \"/sync/heartbeat\", {});\n }\n\n /** POST /events — batch event ingestion. */\n async ingestEvents(\n events: Record<string, unknown>[],\n ): Promise<Record<string, unknown>> {\n return this.request(\"POST\", \"/events\", { events });\n }\n\n /** PATCH /events/{uuid}/grade — submit human grade. */\n async submitGrade(\n eventUuid: string,\n grade: number,\n ): Promise<Record<string, unknown>> {\n return this.request(\"PATCH\", `/events/${eventUuid}/grade`, {\n grade,\n is_graded: true,\n });\n }\n}\n","/**\n * SQLite WAL durability layer — crash-safe event storage.\n *\n * Events are written here immediately after update(). Background flush\n * sends them to cloud. If the process crashes, pending events survive\n * and are retried on next connect().\n */\n\nimport Database from \"better-sqlite3\";\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS events (\n local_event_uuid TEXT PRIMARY KEY,\n bandit_id INTEGER NOT NULL,\n arm_id INTEGER NOT NULL,\n payload TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n created_at REAL NOT NULL,\n human_reward REAL,\n graded_at REAL\n);\nCREATE INDEX IF NOT EXISTS idx_events_status ON events(status);\n`;\n\nconst MIGRATION_GRADING = [\n \"ALTER TABLE events ADD COLUMN human_reward REAL\",\n \"ALTER TABLE events ADD COLUMN graded_at REAL\",\n];\n\nexport interface EventPayload {\n local_event_uuid: string;\n bandit_id: number;\n arm_id: number;\n [key: string]: unknown;\n}\n\nexport class EventStore {\n private db: Database.Database;\n private pushStmt: Database.Statement;\n private pendingStmt: Database.Statement;\n\n constructor(dbPath: string = \":memory:\") {\n this.db = new Database(dbPath);\n this.db.pragma(\"journal_mode = WAL\");\n this.db.pragma(\"busy_timeout = 5000\");\n this.db.pragma(\"synchronous = NORMAL\");\n this.db.exec(SCHEMA);\n this.migrate();\n\n // Pre-compile frequently-used statements\n this.pushStmt = this.db.prepare(\n `INSERT OR IGNORE INTO events\n (local_event_uuid, bandit_id, arm_id, payload, status, created_at)\n VALUES (?, ?, ?, ?, 'pending', ?)`,\n );\n this.pendingStmt = this.db.prepare(\n `SELECT payload FROM events WHERE status = 'pending'\n ORDER BY created_at ASC LIMIT ?`,\n );\n }\n\n /** Insert a pending event. */\n push(event: EventPayload): void {\n this.pushStmt.run(\n event.local_event_uuid,\n event.bandit_id,\n event.arm_id,\n JSON.stringify(event),\n Date.now() / 1000,\n );\n }\n\n /** Return up to `limit` pending events (oldest first). */\n pending(limit: number = 50): EventPayload[] {\n const rows = this.pendingStmt.all(limit) as { payload: string }[];\n return rows.map((row) => JSON.parse(row.payload));\n }\n\n /** Mark events as successfully flushed to cloud. */\n markFlushed(uuids: string[]): void {\n if (uuids.length === 0) return;\n const placeholders = uuids.map(() => \"?\").join(\",\");\n this.db\n .prepare(\n `UPDATE events SET status = 'flushed'\n WHERE local_event_uuid IN (${placeholders})`,\n )\n .run(...uuids);\n }\n\n /** Record a human grade locally. */\n markGraded(uuid: string, reward: number): void {\n this.db\n .prepare(\n `UPDATE events SET human_reward = ?, graded_at = ?\n WHERE local_event_uuid = ?`,\n )\n .run(reward, Date.now() / 1000, uuid);\n }\n\n /** Close the database connection. */\n close(): void {\n this.db.close();\n }\n\n private migrate(): void {\n for (const stmt of MIGRATION_GRADING) {\n try {\n this.db.exec(stmt);\n } catch {\n // Column already exists\n }\n }\n }\n}\n","/**\n * Payload utilities for cloud event ingestion.\n */\n\nconst TEXT_FIELDS = [\"query_text\", \"response\"] as const;\nconst METADATA_FIELDS = [\"model_name\", \"model_provider\"] as const;\n\n/**\n * Return shallow copies of events ready for cloud ingest.\n *\n * Always strips model_name/model_provider (only needed in local SQLite for TUI).\n * Strips query_text/response when includeText is false (dataStorage=\"local\").\n */\nexport function prepareCloudPayload(\n events: Record<string, unknown>[],\n includeText: boolean,\n): Record<string, unknown>[] {\n return events.map((event) => {\n const copy = { ...event };\n for (const field of METADATA_FIELDS) {\n delete copy[field];\n }\n if (!includeText) {\n for (const field of TEXT_FIELDS) {\n delete copy[field];\n }\n }\n return copy;\n });\n}\n","/**\n * Bandito SDK — contextual bandit optimization for LLM selection.\n *\n * Recommended (explicit client):\n * import { BanditoClient } from 'bandito';\n *\n * const client = new BanditoClient({ apiKey: 'bnd_...' });\n * await client.connect();\n * const result = client.pull('my-chatbot', { query: userMessage });\n * // ... call LLM with result.model, result.prompt ...\n * client.update(result, { response: response.text });\n * await client.close();\n *\n * Module-level singleton (convenience):\n * import { connect, pull, update, close } from 'bandito';\n *\n * await connect({ apiKey: 'bnd_...' });\n * const result = pull('my-chatbot', { query: userMessage });\n * update(result, { response: response.text });\n * await close();\n */\n\nexport { BanditoClient, type ClientOptions, type PullOptions, type UpdateOptions } from \"./client.js\";\nexport { type Arm, type PullResult } from \"./models.js\";\n\nimport { BanditoClient, type ClientOptions, type PullOptions, type UpdateOptions } from \"./client.js\";\nimport type { PullResult } from \"./models.js\";\n\nlet _client: BanditoClient | null = null;\n\nfunction getClient(): BanditoClient {\n if (!_client) {\n throw new Error(\"Not connected — call connect() first\");\n }\n return _client;\n}\n\n/** Connect to the Bandito cloud and hydrate local state. */\nexport async function connect(options: ClientOptions = {}): Promise<void> {\n if (_client) {\n await _client.close();\n }\n _client = new BanditoClient(options);\n await _client.connect();\n}\n\n/** Local Thompson Sampling decision. <1ms, no network. */\nexport function pull(banditName: string, options?: PullOptions): PullResult {\n return getClient().pull(banditName, options);\n}\n\n/** Record an LLM call outcome (writes to SQLite, fire-and-forget flush). */\nexport function update(pullResult: PullResult, options?: UpdateOptions): void {\n getClient().update(pullResult, options);\n}\n\n/** Send a human grade for an existing event. */\nexport async function grade(eventId: string, gradeValue: number): Promise<void> {\n await getClient().grade(eventId, gradeValue);\n}\n\n/** Explicit state refresh from cloud. */\nexport async function sync(): Promise<void> {\n await getClient().sync();\n}\n\n/** Shut down: flush events, close connections. */\nexport async function close(): Promise<void> {\n if (_client) {\n await _client.close();\n _client = null;\n }\n}\n"],"mappings":"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","names":["fs","path","os","path","grade","grade"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bandito-ai/sdk",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Contextual bandit optimizer for LLM model and prompt selection",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -21,36 +21,12 @@
21
21
  "smol-toml": "^1.3.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@aws-sdk/client-s3": "^3.0.0",
25
- "@opentelemetry/api": "^1.9.0",
26
- "@opentelemetry/exporter-trace-otlp-http": "^0.53.0",
27
- "@opentelemetry/sdk-trace-node": "^1.26.0",
28
24
  "@types/better-sqlite3": "^7.6.0",
29
25
  "msw": "^2.0.0",
30
26
  "tsup": "^8.0.0",
31
27
  "typescript": "^5.7.0",
32
28
  "vitest": "^3.0.0"
33
29
  },
34
- "peerDependencies": {
35
- "@aws-sdk/client-s3": "^3.0.0",
36
- "@opentelemetry/api": ">=1.9.0",
37
- "@opentelemetry/exporter-trace-otlp-http": ">=0.53.0",
38
- "@opentelemetry/sdk-trace-node": ">=1.26.0"
39
- },
40
- "peerDependenciesMeta": {
41
- "@aws-sdk/client-s3": {
42
- "optional": true
43
- },
44
- "@opentelemetry/api": {
45
- "optional": true
46
- },
47
- "@opentelemetry/exporter-trace-otlp-http": {
48
- "optional": true
49
- },
50
- "@opentelemetry/sdk-trace-node": {
51
- "optional": true
52
- }
53
- },
54
30
  "engines": {
55
31
  "node": ">=18.0.0"
56
32
  },
@@ -21,7 +21,6 @@ export class BanditEngine {
21
21
  static newWithSeed(bandit_json: string, seed?: bigint | null): BanditEngine;
22
22
  /**
23
23
  * Pure math pull — returns JSON { arm_index, arm_id, scores }.
24
- * exclude_ids uses i32 (JS number) at the WASM boundary; cast to i64 internally.
25
24
  */
26
25
  pull(query_length?: number | null, exclude_ids?: Int32Array | null): string;
27
26
  /**
@@ -109,7 +109,6 @@ class BanditEngine {
109
109
  }
110
110
  /**
111
111
  * Pure math pull — returns JSON { arm_index, arm_id, scores }.
112
- * exclude_ids uses i32 (JS number) at the WASM boundary; cast to i64 internally.
113
112
  * @param {number | null} [query_length]
114
113
  * @param {Int32Array | null} [exclude_ids]
115
114
  * @returns {string}
Binary file