@bandito-ai/sdk 0.1.7 → 0.1.8
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 +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +177 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +177 -13
- package/dist/index.mjs.map +1 -1
- package/package.json +25 -1
- package/wasm/bandito_engine.d.ts +1 -0
- package/wasm/bandito_engine.js +1 -0
- package/wasm/bandito_engine_bg.wasm +0 -0
package/dist/index.d.mts
CHANGED
|
@@ -70,9 +70,12 @@ declare class BanditoClient {
|
|
|
70
70
|
private bandits;
|
|
71
71
|
private connected;
|
|
72
72
|
private flushInterval;
|
|
73
|
+
private s3FlushInterval;
|
|
73
74
|
private flushInProgress;
|
|
74
75
|
private deadUuids;
|
|
75
76
|
private retryCounts;
|
|
77
|
+
private _s3Client;
|
|
78
|
+
private _s3Config;
|
|
76
79
|
constructor(options?: ClientOptions);
|
|
77
80
|
/**
|
|
78
81
|
* Bootstrap: authenticate and hydrate in-memory state from cloud.
|
|
@@ -105,6 +108,13 @@ declare class BanditoClient {
|
|
|
105
108
|
private ensureConnected;
|
|
106
109
|
private applySync;
|
|
107
110
|
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;
|
|
108
118
|
}
|
|
109
119
|
|
|
110
120
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -70,9 +70,12 @@ declare class BanditoClient {
|
|
|
70
70
|
private bandits;
|
|
71
71
|
private connected;
|
|
72
72
|
private flushInterval;
|
|
73
|
+
private s3FlushInterval;
|
|
73
74
|
private flushInProgress;
|
|
74
75
|
private deadUuids;
|
|
75
76
|
private retryCounts;
|
|
77
|
+
private _s3Client;
|
|
78
|
+
private _s3Config;
|
|
76
79
|
constructor(options?: ClientOptions);
|
|
77
80
|
/**
|
|
78
81
|
* Bootstrap: authenticate and hydrate in-memory state from cloud.
|
|
@@ -105,6 +108,13 @@ declare class BanditoClient {
|
|
|
105
108
|
private ensureConnected;
|
|
106
109
|
private applySync;
|
|
107
110
|
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;
|
|
108
118
|
}
|
|
109
119
|
|
|
110
120
|
/**
|
package/dist/index.js
CHANGED
|
@@ -57,7 +57,13 @@ function createEngine(banditJson) {
|
|
|
57
57
|
if (!wasmModule) {
|
|
58
58
|
throw new Error("WASM not initialized \u2014 call initWasm() first");
|
|
59
59
|
}
|
|
60
|
-
|
|
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);
|
|
61
67
|
}
|
|
62
68
|
function updateEngine(engine, banditJson) {
|
|
63
69
|
engine.updateFromSync(banditJson);
|
|
@@ -110,7 +116,9 @@ function loadConfig() {
|
|
|
110
116
|
const config = {
|
|
111
117
|
apiKey: null,
|
|
112
118
|
baseUrl: DEFAULT_BASE_URL,
|
|
113
|
-
dataStorage: "local"
|
|
119
|
+
dataStorage: "local",
|
|
120
|
+
s3: null,
|
|
121
|
+
judge: { apiKey: null, model: "gpt-4o-mini" }
|
|
114
122
|
};
|
|
115
123
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
116
124
|
try {
|
|
@@ -119,6 +127,20 @@ function loadConfig() {
|
|
|
119
127
|
if (data.api_key) config.apiKey = data.api_key;
|
|
120
128
|
if (data.base_url) config.baseUrl = data.base_url;
|
|
121
129
|
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;
|
|
122
144
|
} catch {
|
|
123
145
|
}
|
|
124
146
|
}
|
|
@@ -128,6 +150,28 @@ function loadConfig() {
|
|
|
128
150
|
if (envUrl) config.baseUrl = envUrl;
|
|
129
151
|
const envStorage = process.env.BANDITO_DATA_STORAGE;
|
|
130
152
|
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;
|
|
131
175
|
return config;
|
|
132
176
|
}
|
|
133
177
|
|
|
@@ -145,6 +189,17 @@ var BanditoHTTP = class {
|
|
|
145
189
|
apiKey;
|
|
146
190
|
timeout;
|
|
147
191
|
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
|
+
}
|
|
148
203
|
this.baseUrl = `${baseUrl.replace(/\/$/, "")}/api/v1`;
|
|
149
204
|
this.apiKey = apiKey;
|
|
150
205
|
this.timeout = timeout;
|
|
@@ -167,13 +222,14 @@ var BanditoHTTP = class {
|
|
|
167
222
|
clearTimeout(timer);
|
|
168
223
|
if (!resp.ok) {
|
|
169
224
|
const text = await resp.text().catch(() => "");
|
|
225
|
+
const preview = text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
|
|
170
226
|
if (!isRetryable(resp.status) || attempt === MAX_RETRIES - 1) {
|
|
171
227
|
throw new Error(
|
|
172
|
-
`HTTP ${resp.status} on ${method} ${path3}: ${
|
|
228
|
+
`HTTP ${resp.status} on ${method} ${path3}: ${preview}`
|
|
173
229
|
);
|
|
174
230
|
}
|
|
175
231
|
lastError = new Error(
|
|
176
|
-
`HTTP ${resp.status} on ${method} ${path3}: ${
|
|
232
|
+
`HTTP ${resp.status} on ${method} ${path3}: ${preview}`
|
|
177
233
|
);
|
|
178
234
|
} else {
|
|
179
235
|
return await resp.json();
|
|
@@ -226,13 +282,15 @@ CREATE TABLE IF NOT EXISTS events (
|
|
|
226
282
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
227
283
|
created_at REAL NOT NULL,
|
|
228
284
|
human_reward REAL,
|
|
229
|
-
graded_at REAL
|
|
285
|
+
graded_at REAL,
|
|
286
|
+
s3_exported INTEGER NOT NULL DEFAULT 0
|
|
230
287
|
);
|
|
231
288
|
CREATE INDEX IF NOT EXISTS idx_events_status ON events(status);
|
|
232
289
|
`;
|
|
233
290
|
var MIGRATION_GRADING = [
|
|
234
291
|
"ALTER TABLE events ADD COLUMN human_reward REAL",
|
|
235
|
-
"ALTER TABLE events ADD COLUMN graded_at REAL"
|
|
292
|
+
"ALTER TABLE events ADD COLUMN graded_at REAL",
|
|
293
|
+
"ALTER TABLE events ADD COLUMN s3_exported INTEGER NOT NULL DEFAULT 0"
|
|
236
294
|
];
|
|
237
295
|
var EventStore = class {
|
|
238
296
|
db;
|
|
@@ -251,7 +309,7 @@ var EventStore = class {
|
|
|
251
309
|
VALUES (?, ?, ?, ?, 'pending', ?)`
|
|
252
310
|
);
|
|
253
311
|
this.pendingStmt = this.db.prepare(
|
|
254
|
-
`SELECT payload FROM events WHERE status = 'pending'
|
|
312
|
+
`SELECT local_event_uuid, payload FROM events WHERE status = 'pending'
|
|
255
313
|
ORDER BY created_at ASC LIMIT ?`
|
|
256
314
|
);
|
|
257
315
|
}
|
|
@@ -268,7 +326,22 @@ var EventStore = class {
|
|
|
268
326
|
/** Return up to `limit` pending events (oldest first). */
|
|
269
327
|
pending(limit = 50) {
|
|
270
328
|
const rows = this.pendingStmt.all(limit);
|
|
271
|
-
|
|
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;
|
|
272
345
|
}
|
|
273
346
|
/** Mark events as successfully flushed to cloud. */
|
|
274
347
|
markFlushed(uuids) {
|
|
@@ -286,6 +359,20 @@ var EventStore = class {
|
|
|
286
359
|
WHERE local_event_uuid = ?`
|
|
287
360
|
).run(reward, Date.now() / 1e3, uuid);
|
|
288
361
|
}
|
|
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
|
+
}
|
|
289
376
|
/** Close the database connection. */
|
|
290
377
|
close() {
|
|
291
378
|
this.db.close();
|
|
@@ -333,9 +420,14 @@ var BanditoClient = class {
|
|
|
333
420
|
bandits = /* @__PURE__ */ new Map();
|
|
334
421
|
connected = false;
|
|
335
422
|
flushInterval = null;
|
|
423
|
+
s3FlushInterval = null;
|
|
336
424
|
flushInProgress = false;
|
|
337
425
|
deadUuids = /* @__PURE__ */ new Set();
|
|
338
426
|
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;
|
|
339
431
|
constructor(options = {}) {
|
|
340
432
|
this.apiKey = options.apiKey;
|
|
341
433
|
this.baseUrl = options.baseUrl;
|
|
@@ -374,6 +466,22 @@ var BanditoClient = class {
|
|
|
374
466
|
}
|
|
375
467
|
}
|
|
376
468
|
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
|
+
}
|
|
377
485
|
try {
|
|
378
486
|
const data = await this.http.connect();
|
|
379
487
|
this.applySync(data);
|
|
@@ -381,9 +489,17 @@ var BanditoClient = class {
|
|
|
381
489
|
this.retryCounts.clear();
|
|
382
490
|
await this.flushPending();
|
|
383
491
|
this.flushInterval = setInterval(() => {
|
|
384
|
-
this.flushPending().catch(
|
|
385
|
-
|
|
492
|
+
this.flushPending().catch(
|
|
493
|
+
(err) => console.warn("[bandito] Periodic flush error", err)
|
|
494
|
+
);
|
|
386
495
|
}, 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
|
+
}
|
|
387
503
|
this.connected = true;
|
|
388
504
|
} catch (err) {
|
|
389
505
|
this.store?.close();
|
|
@@ -446,7 +562,8 @@ var BanditoClient = class {
|
|
|
446
562
|
bandit_id: pullResult.banditId,
|
|
447
563
|
arm_id: pullResult.arm.armId,
|
|
448
564
|
model_name: pullResult.arm.modelName,
|
|
449
|
-
model_provider: pullResult.arm.modelProvider
|
|
565
|
+
model_provider: pullResult.arm.modelProvider,
|
|
566
|
+
bandit_name: pullResult.banditName
|
|
450
567
|
};
|
|
451
568
|
if (options.queryText != null) {
|
|
452
569
|
event.query_text = options.queryText;
|
|
@@ -476,8 +593,9 @@ var BanditoClient = class {
|
|
|
476
593
|
event.run_error = true;
|
|
477
594
|
}
|
|
478
595
|
this.store.push(event);
|
|
479
|
-
this.flushPending().catch(
|
|
480
|
-
|
|
596
|
+
this.flushPending().catch(
|
|
597
|
+
(err) => console.warn("[bandito] Flush error", err)
|
|
598
|
+
);
|
|
481
599
|
}
|
|
482
600
|
/**
|
|
483
601
|
* Send a human grade for an existing event. Async (HTTP).
|
|
@@ -510,9 +628,16 @@ var BanditoClient = class {
|
|
|
510
628
|
clearInterval(this.flushInterval);
|
|
511
629
|
this.flushInterval = null;
|
|
512
630
|
}
|
|
631
|
+
if (this.s3FlushInterval) {
|
|
632
|
+
clearInterval(this.s3FlushInterval);
|
|
633
|
+
this.s3FlushInterval = null;
|
|
634
|
+
}
|
|
513
635
|
if (this.store && this.http) {
|
|
514
636
|
await this.flushPending();
|
|
515
637
|
}
|
|
638
|
+
if (this._s3Client !== null && this.store) {
|
|
639
|
+
await this.dumpPendingToS3();
|
|
640
|
+
}
|
|
516
641
|
this.store?.close();
|
|
517
642
|
this.store = null;
|
|
518
643
|
this.http = null;
|
|
@@ -592,6 +717,45 @@ var BanditoClient = class {
|
|
|
592
717
|
this.flushInProgress = false;
|
|
593
718
|
}
|
|
594
719
|
}
|
|
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
|
+
}
|
|
595
759
|
};
|
|
596
760
|
|
|
597
761
|
// 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 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"]}
|
|
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":"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","names":["fs","path","os","parseToml","path","grade","Database","grade"]}
|
package/dist/index.mjs
CHANGED
|
@@ -15,7 +15,13 @@ function createEngine(banditJson) {
|
|
|
15
15
|
if (!wasmModule) {
|
|
16
16
|
throw new Error("WASM not initialized \u2014 call initWasm() first");
|
|
17
17
|
}
|
|
18
|
-
|
|
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);
|
|
19
25
|
}
|
|
20
26
|
function updateEngine(engine, banditJson) {
|
|
21
27
|
engine.updateFromSync(banditJson);
|
|
@@ -68,7 +74,9 @@ function loadConfig() {
|
|
|
68
74
|
const config = {
|
|
69
75
|
apiKey: null,
|
|
70
76
|
baseUrl: DEFAULT_BASE_URL,
|
|
71
|
-
dataStorage: "local"
|
|
77
|
+
dataStorage: "local",
|
|
78
|
+
s3: null,
|
|
79
|
+
judge: { apiKey: null, model: "gpt-4o-mini" }
|
|
72
80
|
};
|
|
73
81
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
74
82
|
try {
|
|
@@ -77,6 +85,20 @@ function loadConfig() {
|
|
|
77
85
|
if (data.api_key) config.apiKey = data.api_key;
|
|
78
86
|
if (data.base_url) config.baseUrl = data.base_url;
|
|
79
87
|
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;
|
|
80
102
|
} catch {
|
|
81
103
|
}
|
|
82
104
|
}
|
|
@@ -86,6 +108,28 @@ function loadConfig() {
|
|
|
86
108
|
if (envUrl) config.baseUrl = envUrl;
|
|
87
109
|
const envStorage = process.env.BANDITO_DATA_STORAGE;
|
|
88
110
|
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;
|
|
89
133
|
return config;
|
|
90
134
|
}
|
|
91
135
|
|
|
@@ -103,6 +147,17 @@ var BanditoHTTP = class {
|
|
|
103
147
|
apiKey;
|
|
104
148
|
timeout;
|
|
105
149
|
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
|
+
}
|
|
106
161
|
this.baseUrl = `${baseUrl.replace(/\/$/, "")}/api/v1`;
|
|
107
162
|
this.apiKey = apiKey;
|
|
108
163
|
this.timeout = timeout;
|
|
@@ -125,13 +180,14 @@ var BanditoHTTP = class {
|
|
|
125
180
|
clearTimeout(timer);
|
|
126
181
|
if (!resp.ok) {
|
|
127
182
|
const text = await resp.text().catch(() => "");
|
|
183
|
+
const preview = text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
|
|
128
184
|
if (!isRetryable(resp.status) || attempt === MAX_RETRIES - 1) {
|
|
129
185
|
throw new Error(
|
|
130
|
-
`HTTP ${resp.status} on ${method} ${path3}: ${
|
|
186
|
+
`HTTP ${resp.status} on ${method} ${path3}: ${preview}`
|
|
131
187
|
);
|
|
132
188
|
}
|
|
133
189
|
lastError = new Error(
|
|
134
|
-
`HTTP ${resp.status} on ${method} ${path3}: ${
|
|
190
|
+
`HTTP ${resp.status} on ${method} ${path3}: ${preview}`
|
|
135
191
|
);
|
|
136
192
|
} else {
|
|
137
193
|
return await resp.json();
|
|
@@ -184,13 +240,15 @@ CREATE TABLE IF NOT EXISTS events (
|
|
|
184
240
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
185
241
|
created_at REAL NOT NULL,
|
|
186
242
|
human_reward REAL,
|
|
187
|
-
graded_at REAL
|
|
243
|
+
graded_at REAL,
|
|
244
|
+
s3_exported INTEGER NOT NULL DEFAULT 0
|
|
188
245
|
);
|
|
189
246
|
CREATE INDEX IF NOT EXISTS idx_events_status ON events(status);
|
|
190
247
|
`;
|
|
191
248
|
var MIGRATION_GRADING = [
|
|
192
249
|
"ALTER TABLE events ADD COLUMN human_reward REAL",
|
|
193
|
-
"ALTER TABLE events ADD COLUMN graded_at REAL"
|
|
250
|
+
"ALTER TABLE events ADD COLUMN graded_at REAL",
|
|
251
|
+
"ALTER TABLE events ADD COLUMN s3_exported INTEGER NOT NULL DEFAULT 0"
|
|
194
252
|
];
|
|
195
253
|
var EventStore = class {
|
|
196
254
|
db;
|
|
@@ -209,7 +267,7 @@ var EventStore = class {
|
|
|
209
267
|
VALUES (?, ?, ?, ?, 'pending', ?)`
|
|
210
268
|
);
|
|
211
269
|
this.pendingStmt = this.db.prepare(
|
|
212
|
-
`SELECT payload FROM events WHERE status = 'pending'
|
|
270
|
+
`SELECT local_event_uuid, payload FROM events WHERE status = 'pending'
|
|
213
271
|
ORDER BY created_at ASC LIMIT ?`
|
|
214
272
|
);
|
|
215
273
|
}
|
|
@@ -226,7 +284,22 @@ var EventStore = class {
|
|
|
226
284
|
/** Return up to `limit` pending events (oldest first). */
|
|
227
285
|
pending(limit = 50) {
|
|
228
286
|
const rows = this.pendingStmt.all(limit);
|
|
229
|
-
|
|
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;
|
|
230
303
|
}
|
|
231
304
|
/** Mark events as successfully flushed to cloud. */
|
|
232
305
|
markFlushed(uuids) {
|
|
@@ -244,6 +317,20 @@ var EventStore = class {
|
|
|
244
317
|
WHERE local_event_uuid = ?`
|
|
245
318
|
).run(reward, Date.now() / 1e3, uuid);
|
|
246
319
|
}
|
|
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
|
+
}
|
|
247
334
|
/** Close the database connection. */
|
|
248
335
|
close() {
|
|
249
336
|
this.db.close();
|
|
@@ -291,9 +378,14 @@ var BanditoClient = class {
|
|
|
291
378
|
bandits = /* @__PURE__ */ new Map();
|
|
292
379
|
connected = false;
|
|
293
380
|
flushInterval = null;
|
|
381
|
+
s3FlushInterval = null;
|
|
294
382
|
flushInProgress = false;
|
|
295
383
|
deadUuids = /* @__PURE__ */ new Set();
|
|
296
384
|
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;
|
|
297
389
|
constructor(options = {}) {
|
|
298
390
|
this.apiKey = options.apiKey;
|
|
299
391
|
this.baseUrl = options.baseUrl;
|
|
@@ -332,6 +424,22 @@ var BanditoClient = class {
|
|
|
332
424
|
}
|
|
333
425
|
}
|
|
334
426
|
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
|
+
}
|
|
335
443
|
try {
|
|
336
444
|
const data = await this.http.connect();
|
|
337
445
|
this.applySync(data);
|
|
@@ -339,9 +447,17 @@ var BanditoClient = class {
|
|
|
339
447
|
this.retryCounts.clear();
|
|
340
448
|
await this.flushPending();
|
|
341
449
|
this.flushInterval = setInterval(() => {
|
|
342
|
-
this.flushPending().catch(
|
|
343
|
-
|
|
450
|
+
this.flushPending().catch(
|
|
451
|
+
(err) => console.warn("[bandito] Periodic flush error", err)
|
|
452
|
+
);
|
|
344
453
|
}, 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
|
+
}
|
|
345
461
|
this.connected = true;
|
|
346
462
|
} catch (err) {
|
|
347
463
|
this.store?.close();
|
|
@@ -404,7 +520,8 @@ var BanditoClient = class {
|
|
|
404
520
|
bandit_id: pullResult.banditId,
|
|
405
521
|
arm_id: pullResult.arm.armId,
|
|
406
522
|
model_name: pullResult.arm.modelName,
|
|
407
|
-
model_provider: pullResult.arm.modelProvider
|
|
523
|
+
model_provider: pullResult.arm.modelProvider,
|
|
524
|
+
bandit_name: pullResult.banditName
|
|
408
525
|
};
|
|
409
526
|
if (options.queryText != null) {
|
|
410
527
|
event.query_text = options.queryText;
|
|
@@ -434,8 +551,9 @@ var BanditoClient = class {
|
|
|
434
551
|
event.run_error = true;
|
|
435
552
|
}
|
|
436
553
|
this.store.push(event);
|
|
437
|
-
this.flushPending().catch(
|
|
438
|
-
|
|
554
|
+
this.flushPending().catch(
|
|
555
|
+
(err) => console.warn("[bandito] Flush error", err)
|
|
556
|
+
);
|
|
439
557
|
}
|
|
440
558
|
/**
|
|
441
559
|
* Send a human grade for an existing event. Async (HTTP).
|
|
@@ -468,9 +586,16 @@ var BanditoClient = class {
|
|
|
468
586
|
clearInterval(this.flushInterval);
|
|
469
587
|
this.flushInterval = null;
|
|
470
588
|
}
|
|
589
|
+
if (this.s3FlushInterval) {
|
|
590
|
+
clearInterval(this.s3FlushInterval);
|
|
591
|
+
this.s3FlushInterval = null;
|
|
592
|
+
}
|
|
471
593
|
if (this.store && this.http) {
|
|
472
594
|
await this.flushPending();
|
|
473
595
|
}
|
|
596
|
+
if (this._s3Client !== null && this.store) {
|
|
597
|
+
await this.dumpPendingToS3();
|
|
598
|
+
}
|
|
474
599
|
this.store?.close();
|
|
475
600
|
this.store = null;
|
|
476
601
|
this.http = null;
|
|
@@ -550,6 +675,45 @@ var BanditoClient = class {
|
|
|
550
675
|
this.flushInProgress = false;
|
|
551
676
|
}
|
|
552
677
|
}
|
|
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
|
+
}
|
|
553
717
|
};
|
|
554
718
|
|
|
555
719
|
// src/index.ts
|
package/dist/index.mjs.map
CHANGED
|
@@ -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 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"]}
|
|
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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bandito-ai/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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,12 +21,36 @@
|
|
|
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",
|
|
24
28
|
"@types/better-sqlite3": "^7.6.0",
|
|
25
29
|
"msw": "^2.0.0",
|
|
26
30
|
"tsup": "^8.0.0",
|
|
27
31
|
"typescript": "^5.7.0",
|
|
28
32
|
"vitest": "^3.0.0"
|
|
29
33
|
},
|
|
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
|
+
},
|
|
30
54
|
"engines": {
|
|
31
55
|
"node": ">=18.0.0"
|
|
32
56
|
},
|
package/wasm/bandito_engine.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ 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.
|
|
24
25
|
*/
|
|
25
26
|
pull(query_length?: number | null, exclude_ids?: Int32Array | null): string;
|
|
26
27
|
/**
|
package/wasm/bandito_engine.js
CHANGED
|
@@ -109,6 +109,7 @@ 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.
|
|
112
113
|
* @param {number | null} [query_length]
|
|
113
114
|
* @param {Int32Array | null} [exclude_ids]
|
|
114
115
|
* @returns {string}
|
|
Binary file
|