@bandito-ai/sdk 0.1.7

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.
@@ -0,0 +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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACSA,yBAA2B;AAC3B,IAAAA,MAAoB;AACpB,IAAAC,QAAsB;AACtB,IAAAC,MAAoB;AACpB,6BAA4B;;;ACJ5B,IAAI,aAA6D;AAMjE,eAAsB,WAA0B;AAC9C,MAAI,WAAY;AAChB,eAAa,MAAM,OAAO,wBAAwB;AACpD;AAMO,SAAS,aAAa,YAAsC;AACjE,MAAI,CAAC,YAAY;AACf,UAAM,IAAI,MAAM,mDAA8C;AAAA,EAChE;AACA,SAAO,IAAI,WAAW,aAAa,UAAU;AAC/C;AAMO,SAAS,aAAa,QAA0B,YAA0B;AAC/E,SAAO,eAAe,UAAU;AAClC;;;ACJO,SAAS,UAAU,MAMlB;AACN,QAAM,MAAW;AAAA,IACf,OAAO,KAAK;AAAA,IACZ,WAAW,KAAK;AAAA,IAChB,eAAe,KAAK;AAAA,IACpB,cAAc,KAAK;AAAA,IACnB,mBAAmB,KAAK,uBAAuB;AAAA,IAC/C,IAAI,QAAQ;AACV,aAAO,KAAK;AAAA,IACd;AAAA,IACA,IAAI,SAAS;AACX,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACA,SAAO,OAAO,OAAO,GAAG;AAC1B;AAGO,SAAS,iBAAiB,MAOlB;AACb,QAAM,SAAqB;AAAA,IACzB,KAAK,KAAK;AAAA,IACV,SAAS,KAAK;AAAA,IACd,UAAU,KAAK;AAAA,IACf,YAAY,KAAK;AAAA,IACjB,QAAQ,OAAO,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC;AAAA,IACxC,IAAI,QAAQ;AACV,aAAO,KAAK,IAAI;AAAA,IAClB;AAAA,IACA,IAAI,SAAS;AACX,aAAO,KAAK,IAAI;AAAA,IAClB;AAAA,IACA,WAAW,KAAK;AAAA,EAClB;AACA,SAAO,OAAO,OAAO,MAAM;AAC7B;;;ACzEA,SAAoB;AACpB,WAAsB;AACtB,SAAoB;AACpB,uBAAmC;AAE5B,IAAM,mBAAmB;AAChC,IAAM,aAAkB,UAAQ,WAAQ,GAAG,UAAU;AACrD,IAAM,cAAmB,UAAK,YAAY,aAAa;AAWhD,SAAS,aAA4B;AAC1C,QAAM,SAAwB;AAAA,IAC5B,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,aAAa;AAAA,EACf;AAGA,MAAO,cAAW,WAAW,GAAG;AAC9B,QAAI;AACF,YAAM,UAAa,gBAAa,aAAa,OAAO;AACpD,YAAM,WAAO,iBAAAC,OAAU,OAAO;AAC9B,UAAI,KAAK,QAAS,QAAO,SAAS,KAAK;AACvC,UAAI,KAAK,SAAU,QAAO,UAAU,KAAK;AACzC,UAAI,KAAK,aAAc,QAAO,cAAc,KAAK;AAAA,IACnD,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,OAAQ,QAAO,SAAS;AAE5B,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,OAAQ,QAAO,UAAU;AAE7B,QAAM,aAAa,QAAQ,IAAI;AAC/B,MAAI,WAAY,QAAO,cAAc;AAErC,SAAO;AACT;;;ACjDA,IAAM,cAAc;AACpB,IAAM,qBAAqB;AAE3B,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAEA,SAAS,YAAY,QAAyB;AAC5C,SAAO,UAAU;AACnB;AAEO,IAAM,cAAN,MAAkB;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,SAAiB,QAAgB,UAAkB,KAAQ;AACrE,SAAK,UAAU,GAAG,QAAQ,QAAQ,OAAO,EAAE,CAAC;AAC5C,SAAK,SAAS;AACd,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAc,QACZ,QACAC,OACA,MACkC;AAClC,QAAI,YAA0B;AAE9B,aAAS,UAAU,GAAG,UAAU,aAAa,WAAW;AACtD,YAAM,aAAa,IAAI,gBAAgB;AACvC,YAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,KAAK,OAAO;AAE/D,UAAI;AACF,cAAM,OAAO,MAAM,MAAM,GAAG,KAAK,OAAO,GAAGA,KAAI,IAAI;AAAA,UACjD;AAAA,UACA,SAAS;AAAA,YACP,aAAa,KAAK;AAAA,YAClB,gBAAgB;AAAA,UAClB;AAAA,UACA,MAAM,QAAQ,OAAO,KAAK,UAAU,IAAI,IAAI;AAAA,UAC5C,QAAQ,WAAW;AAAA,QACrB,CAAC;AAED,qBAAa,KAAK;AAElB,YAAI,CAAC,KAAK,IAAI;AACZ,gBAAM,OAAO,MAAM,KAAK,KAAK,EAAE,MAAM,MAAM,EAAE;AAC7C,cAAI,CAAC,YAAY,KAAK,MAAM,KAAK,YAAY,cAAc,GAAG;AAC5D,kBAAM,IAAI;AAAA,cACR,QAAQ,KAAK,MAAM,OAAO,MAAM,IAAIA,KAAI,KAAK,IAAI;AAAA,YACnD;AAAA,UACF;AAEA,sBAAY,IAAI;AAAA,YACd,QAAQ,KAAK,MAAM,OAAO,MAAM,IAAIA,KAAI,KAAK,IAAI;AAAA,UACnD;AAAA,QACF,OAAO;AACL,iBAAQ,MAAM,KAAK,KAAK;AAAA,QAC1B;AAAA,MACF,SAAS,KAAK;AACZ,qBAAa,KAAK;AAClB,oBAAY;AAGZ,cAAM,qBACH,IAAc,SAAS,gBACvB,IAAc,SAAS;AAC1B,YAAI,CAAC,sBAAsB,UAAU,cAAc,GAAG;AAEpD,gBAAM;AAAA,QACR;AACA,YAAI,YAAY,cAAc,GAAG;AAC/B,gBAAM;AAAA,QACR;AAAA,MACF;AAGA,YAAM,QAAQ,qBAAqB,KAAK;AACxC,YAAM,MAAM,KAAK;AAAA,IACnB;AAEA,UAAM;AAAA,EACR;AAAA;AAAA,EAGA,MAAM,UAA4C;AAChD,WAAO,KAAK,QAAQ,QAAQ,eAAe;AAAA,EAC7C;AAAA;AAAA,EAGA,MAAM,YAA8C;AAClD,WAAO,KAAK,QAAQ,QAAQ,mBAAmB,CAAC,CAAC;AAAA,EACnD;AAAA;AAAA,EAGA,MAAM,aACJ,QACkC;AAClC,WAAO,KAAK,QAAQ,QAAQ,WAAW,EAAE,OAAO,CAAC;AAAA,EACnD;AAAA;AAAA,EAGA,MAAM,YACJ,WACAC,QACkC;AAClC,WAAO,KAAK,QAAQ,SAAS,WAAW,SAAS,UAAU;AAAA,MACzD,OAAAA;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AAAA,EACH;AACF;;;AC/GA,4BAAqB;AAErB,IAAM,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcf,IAAM,oBAAoB;AAAA,EACxB;AAAA,EACA;AACF;AASO,IAAM,aAAN,MAAiB;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,SAAiB,YAAY;AACvC,SAAK,KAAK,IAAI,sBAAAC,QAAS,MAAM;AAC7B,SAAK,GAAG,OAAO,oBAAoB;AACnC,SAAK,GAAG,OAAO,qBAAqB;AACpC,SAAK,GAAG,OAAO,sBAAsB;AACrC,SAAK,GAAG,KAAK,MAAM;AACnB,SAAK,QAAQ;AAGb,SAAK,WAAW,KAAK,GAAG;AAAA,MACtB;AAAA;AAAA;AAAA,IAGF;AACA,SAAK,cAAc,KAAK,GAAG;AAAA,MACzB;AAAA;AAAA,IAEF;AAAA,EACF;AAAA;AAAA,EAGA,KAAK,OAA2B;AAC9B,SAAK,SAAS;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,KAAK,UAAU,KAAK;AAAA,MACpB,KAAK,IAAI,IAAI;AAAA,IACf;AAAA,EACF;AAAA;AAAA,EAGA,QAAQ,QAAgB,IAAoB;AAC1C,UAAM,OAAO,KAAK,YAAY,IAAI,KAAK;AACvC,WAAO,KAAK,IAAI,CAAC,QAAQ,KAAK,MAAM,IAAI,OAAO,CAAC;AAAA,EAClD;AAAA;AAAA,EAGA,YAAY,OAAuB;AACjC,QAAI,MAAM,WAAW,EAAG;AACxB,UAAM,eAAe,MAAM,IAAI,MAAM,GAAG,EAAE,KAAK,GAAG;AAClD,SAAK,GACF;AAAA,MACC;AAAA,sCAC8B,YAAY;AAAA,IAC5C,EACC,IAAI,GAAG,KAAK;AAAA,EACjB;AAAA;AAAA,EAGA,WAAW,MAAc,QAAsB;AAC7C,SAAK,GACF;AAAA,MACC;AAAA;AAAA,IAEF,EACC,IAAI,QAAQ,KAAK,IAAI,IAAI,KAAM,IAAI;AAAA,EACxC;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,GAAG,MAAM;AAAA,EAChB;AAAA,EAEQ,UAAgB;AACtB,eAAW,QAAQ,mBAAmB;AACpC,UAAI;AACF,aAAK,GAAG,KAAK,IAAI;AAAA,MACnB,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;;;AC9GA,IAAM,cAAc,CAAC,cAAc,UAAU;AAC7C,IAAM,kBAAkB,CAAC,cAAc,gBAAgB;AAQhD,SAAS,oBACd,QACA,aAC2B;AAC3B,SAAO,OAAO,IAAI,CAAC,UAAU;AAC3B,UAAM,OAAO,EAAE,GAAG,MAAM;AACxB,eAAW,SAAS,iBAAiB;AACnC,aAAO,KAAK,KAAK;AAAA,IACnB;AACA,QAAI,CAAC,aAAa;AAChB,iBAAW,SAAS,aAAa;AAC/B,eAAO,KAAK,KAAK;AAAA,MACnB;AAAA,IACF;AACA,WAAO;AAAA,EACT,CAAC;AACH;;;ANAA,IAAM,qBAA0B,WAAQ,YAAQ,GAAG,YAAY,WAAW;AAC1E,IAAM,oBAAoB;AA0BnB,IAAM,gBAAN,MAAoB;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,OAA2B;AAAA,EAC3B,QAA2B;AAAA,EAC3B,UAAqC,oBAAI,IAAI;AAAA,EAC7C,UAAoC,oBAAI,IAAI;AAAA,EAC5C,YAAY;AAAA,EACZ,gBAAuD;AAAA,EACvD,kBAAkB;AAAA,EAClB,YAAyB,oBAAI,IAAI;AAAA,EACjC,cAAmC,oBAAI,IAAI;AAAA,EAEnD,YAAY,UAAyB,CAAC,GAAG;AACvC,SAAK,SAAS,QAAQ;AACtB,SAAK,UAAU,QAAQ;AACvB,SAAK,YAAY,QAAQ;AACzB,SAAK,iBAAiB,QAAQ;AAC9B,SAAK,cAAc,QAAQ,eAAe;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,UAAyB;AAE7B,QAAI,KAAK,WAAW;AAClB,YAAM,KAAK,MAAM;AAAA,IACnB;AAGA,UAAM,SAAS;AAGf,UAAM,SAAS,WAAW;AAC1B,UAAM,SAAS,KAAK,UAAU,OAAO;AACrC,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AAEA,UAAM,UAAU,KAAK,WAAW,OAAO;AACvC,QAAI,CAAC,KAAK,gBAAgB;AACxB,WAAK,cAAc,OAAO;AAAA,IAC5B;AAEA,SAAK,OAAO,IAAI,YAAY,SAAS,MAAM;AAC3C,UAAM,YAAY,KAAK,aAAa;AACpC,QAAI,cAAc,YAAY;AAC5B,YAAM,MAAW,cAAQ,SAAS;AAClC,UAAI,CAAI,eAAW,GAAG,GAAG;AACvB,QAAG,cAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,MACvC;AAAA,IACF;AACA,SAAK,QAAQ,IAAI,WAAW,SAAS;AAGrC,QAAI;AACF,YAAM,OAAO,MAAM,KAAK,KAAK,QAAQ;AACrC,WAAK,UAAU,IAAI;AAGnB,WAAK,UAAU,MAAM;AACrB,WAAK,YAAY,MAAM;AAGvB,YAAM,KAAK,aAAa;AAGxB,WAAK,gBAAgB,YAAY,MAAM;AACrC,aAAK,aAAa,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MACpC,GAAG,GAAM;AAET,WAAK,YAAY;AAAA,IACnB,SAAS,KAAK;AACZ,WAAK,OAAO,MAAM;AAClB,WAAK,QAAQ;AACb,WAAK,OAAO;AACZ,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,KAAK,YAAoB,UAAuB,CAAC,GAAe;AAC9D,SAAK,gBAAgB;AAErB,UAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU;AACzC,QAAI,CAAC,OAAO;AACV,YAAM,YAAY,CAAC,GAAG,KAAK,QAAQ,KAAK,CAAC;AACzC,YAAM,IAAI;AAAA,QACR,mBAAmB,UAAU,kBAAkB,UAAU,KAAK,IAAI,CAAC;AAAA,MACrE;AAAA,IACF;AAEA,QAAI,MAAM,KAAK,WAAW,GAAG;AAC3B,YAAM,IAAI,MAAM,WAAW,UAAU,sBAAsB;AAAA,IAC7D;AAEA,UAAM,SAAS,KAAK,QAAQ,IAAI,UAAU;AAC1C,UAAM,cAAc,QAAQ,OAAO,UAAU;AAC7C,UAAM,aAAa,QAAQ,UAAU,WAAW,KAAK,QAAQ,OAAO,IAAI;AAExE,UAAM,aAAa,OAAO,KAAK,aAAa,UAAU;AACtD,UAAM,MAAwB,KAAK,MAAM,UAAU;AAGnD,UAAM,YAAY,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,UAAU,IAAI,MAAM;AAC/D,QAAI,CAAC,WAAW;AACd,YAAM,IAAI;AAAA,QACR,uBAAuB,IAAI,MAAM,0CAA0C,UAAU;AAAA,MAEvF;AAAA,IACF;AAEA,WAAO,iBAAiB;AAAA,MACtB,KAAK;AAAA,MACL,aAAS,+BAAW;AAAA,MACpB,UAAU,MAAM;AAAA,MAChB;AAAA,MACA,QAAQ,IAAI;AAAA,MACZ,UAAU,mCAAY,IAAI;AAAA,IAC5B,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,YAAwB,UAAyB,CAAC,GAAS;AAChE,SAAK,gBAAgB;AAErB,QAAI,SAAS,QAAQ;AACrB,QAAI,QAAQ,UAAU,UAAU,MAAM;AACpC,eAAS;AAAA,IACX;AAGA,QAAI,UAAU,QAAQ;AACtB,QAAI,WAAW,QAAQ,WAAW,YAAY,GAAG;AAC/C,gBAAU,mCAAY,IAAI,IAAI,WAAW;AAAA,IAC3C;AAGA,UAAM,QAAsB;AAAA,MAC1B,kBAAkB,WAAW;AAAA,MAC7B,WAAW,WAAW;AAAA,MACtB,QAAQ,WAAW,IAAI;AAAA,MACvB,YAAY,WAAW,IAAI;AAAA,MAC3B,gBAAgB,WAAW,IAAI;AAAA,IACjC;AAEA,QAAI,QAAQ,aAAa,MAAM;AAC7B,MAAC,MAAkC,aAAa,QAAQ;AAAA,IAC1D;AACA,QAAI,QAAQ,YAAY,MAAM;AAC5B,MAAC,MAAkC,WACjC,OAAO,QAAQ,aAAa,WACxB,EAAE,UAAU,QAAQ,SAAS,IAC7B,QAAQ;AAAA,IAChB;AACA,QAAI,UAAU,MAAM;AAClB,MAAC,MAAkC,eAAe;AAAA,IACpD;AACA,QAAI,QAAQ,QAAQ,MAAM;AACxB,MAAC,MAAkC,OAAO,QAAQ;AAAA,IACpD;AACA,QAAI,WAAW,MAAM;AACnB,MAAC,MAAkC,UAAU;AAAA,IAC/C;AACA,QAAI,QAAQ,eAAe,MAAM;AAC/B,MAAC,MAAkC,eAAe,QAAQ;AAAA,IAC5D;AACA,QAAI,QAAQ,gBAAgB,MAAM;AAChC,MAAC,MAAkC,gBAAgB,QAAQ;AAAA,IAC7D;AACA,QAAI,QAAQ,WAAW,MAAM;AAC3B,MAAC,MAAkC,UAAU,QAAQ;AAAA,IACvD;AACA,QAAI,QAAQ,QAAQ;AAClB,MAAC,MAAkC,YAAY;AAAA,IACjD;AAGA,SAAK,MAAO,KAAK,KAAK;AAGtB,SAAK,aAAa,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,MAAM,SAAiBC,QAA8B;AACzD,SAAK,gBAAgB;AACrB,UAAM,KAAK,KAAM,YAAY,SAASA,MAAK;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAsB;AAC1B,SAAK,gBAAgB;AACrB,UAAM,OAAO,MAAM,KAAK,KAAM,UAAU;AAExC,UAAM,cAAc,IAAI,IAAI,KAAK,OAAO;AACxC,UAAM,cAAc,IAAI,IAAI,KAAK,OAAO;AACxC,QAAI;AACF,WAAK,UAAU,IAAI;AAAA,IACrB,SAAS,KAAK;AAEZ,WAAK,UAAU;AACf,WAAK,UAAU;AACf,cAAQ,KAAK,0EAAqE,GAAG;AAAA,IACvF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC3B,QAAI,KAAK,eAAe;AACtB,oBAAc,KAAK,aAAa;AAChC,WAAK,gBAAgB;AAAA,IACvB;AAGA,QAAI,KAAK,SAAS,KAAK,MAAM;AAC3B,YAAM,KAAK,aAAa;AAAA,IAC1B;AAEA,SAAK,OAAO,MAAM;AAClB,SAAK,QAAQ;AACb,SAAK,OAAO;AACZ,SAAK,QAAQ,MAAM;AACnB,SAAK,QAAQ,MAAM;AACnB,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA,EAIQ,kBAAwB;AAC9B,QAAI,CAAC,KAAK,WAAW;AACnB,YAAM,IAAI,MAAM,2CAAsC;AAAA,IACxD;AAAA,EACF;AAAA,EAEQ,UAAU,MAAqC;AACrD,UAAM,cAAe,KAAK,WAAW,CAAC;AAEtC,UAAM,aAAa,oBAAI,IAAyB;AAChD,UAAM,aAAa,oBAAI,IAA0B;AAEjD,eAAW,KAAK,aAAa;AAC3B,YAAM,OAAQ,EAAE,QAAQ,CAAC;AACzB,UAAI,KAAK,WAAW,EAAG;AAEvB,YAAM,aAAoB,KACvB,OAAO,CAAC,MAAM,EAAE,SAAS,EACzB,IAAI,CAAC,MAAM,UAAU,CAAC,CAAC;AAE1B,YAAM,OAAO,EAAE;AACf,YAAM,QAAqB;AAAA,QACzB,UAAU,OAAO,EAAE,SAAS;AAAA,QAC5B;AAAA,QACA,MAAM;AAAA,QACN,SAAS;AAAA,QACT,kBAAmB,EAAE,qBAAgC;AAAA,QACrD,iBAAiB,EAAE;AAAA,QACnB,QAAQ,EAAE;AAAA,QACV,WAAW,EAAE;AAAA,MACf;AAEA,iBAAW,IAAI,MAAM,KAAK;AAG1B,YAAM,iBAAiB,KAAK,QAAQ,IAAI,IAAI;AAC5C,YAAM,aAAa,KAAK,UAAU,CAAC;AACnC,UAAI,gBAAgB;AAClB,qBAAa,gBAAgB,UAAU;AACvC,mBAAW,IAAI,MAAM,cAAc;AAAA,MACrC,OAAO;AACL,mBAAW,IAAI,MAAM,aAAa,UAAU,CAAC;AAAA,MAC/C;AAAA,IACF;AAEA,SAAK,UAAU;AACf,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAc,eAA8B;AAC1C,QAAI,KAAK,mBAAmB,CAAC,KAAK,SAAS,CAAC,KAAK,KAAM;AACvD,SAAK,kBAAkB;AAEvB,QAAI;AACF,YAAM,UAAU,KAAK,MAAM,QAAQ;AACnC,UAAI,QAAQ,WAAW,EAAG;AAG1B,YAAM,QAAQ,KAAK,UAAU,OAAO,IAChC,QAAQ,OAAO,CAAC,MAAM,CAAC,KAAK,UAAU,IAAI,EAAE,gBAAgB,CAAC,IAC7D;AACJ,UAAI,MAAM,WAAW,EAAG;AAGxB,YAAM,UAAU;AAAA,QACd;AAAA,QACA,KAAK,gBAAgB;AAAA,MACvB;AAEA,YAAM,SAAS,MAAM,KAAK,KAAK,aAAa,OAAO;AAGnD,YAAM,SAAU,OAAO,UAAU,CAAC;AAIlC,YAAM,eAAe,IAAI;AAAA,QACvB,OAAO,IAAI,CAAC,MAAM,EAAE,gBAAgB,EAAE,OAAO,OAAO;AAAA,MACtD;AAGA,iBAAW,OAAO,cAAc;AAC9B,cAAM,SAAS,KAAK,YAAY,IAAI,GAAG,KAAK,KAAK;AACjD,aAAK,YAAY,IAAI,KAAK,KAAK;AAC/B,YAAI,SAAS,mBAAmB;AAC9B,eAAK,UAAU,IAAI,GAAG;AAAA,QACxB;AAAA,MACF;AAGA,YAAM,eAAe,MAClB,IAAI,CAAC,MAAM,EAAE,gBAAgB,EAC7B,OAAO,CAAC,QAAQ,CAAC,aAAa,IAAI,GAAG,CAAC;AACzC,UAAI,aAAa,SAAS,KAAK,KAAK,OAAO;AACzC,aAAK,MAAM,YAAY,YAAY;AAAA,MACrC;AAAA,IACF,SAAS,KAAK;AAEZ,cAAQ,KAAK,kDAA6C,GAAG;AAAA,IAC/D,UAAE;AACA,WAAK,kBAAkB;AAAA,IACzB;AAAA,EACF;AACF;;;AD9XA,IAAI,UAAgC;AAEpC,SAAS,YAA2B;AAClC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,2CAAsC;AAAA,EACxD;AACA,SAAO;AACT;AAGA,eAAsB,QAAQ,UAAyB,CAAC,GAAkB;AACxE,MAAI,SAAS;AACX,UAAM,QAAQ,MAAM;AAAA,EACtB;AACA,YAAU,IAAI,cAAc,OAAO;AACnC,QAAM,QAAQ,QAAQ;AACxB;AAGO,SAAS,KAAK,YAAoB,SAAmC;AAC1E,SAAO,UAAU,EAAE,KAAK,YAAY,OAAO;AAC7C;AAGO,SAAS,OAAO,YAAwB,SAA+B;AAC5E,YAAU,EAAE,OAAO,YAAY,OAAO;AACxC;AAGA,eAAsB,MAAM,SAAiB,YAAmC;AAC9E,QAAM,UAAU,EAAE,MAAM,SAAS,UAAU;AAC7C;AAGA,eAAsB,OAAsB;AAC1C,QAAM,UAAU,EAAE,KAAK;AACzB;AAGA,eAAsB,QAAuB;AAC3C,MAAI,SAAS;AACX,UAAM,QAAQ,MAAM;AACpB,cAAU;AAAA,EACZ;AACF;","names":["fs","path","os","parseToml","path","grade","Database","grade"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,597 @@
1
+ // src/client.ts
2
+ import { randomUUID } from "crypto";
3
+ import * as fs2 from "fs";
4
+ import * as path2 from "path";
5
+ import * as os2 from "os";
6
+ import { performance } from "perf_hooks";
7
+
8
+ // src/engine.ts
9
+ var wasmModule = null;
10
+ async function initWasm() {
11
+ if (wasmModule) return;
12
+ wasmModule = await import("../wasm/bandito_engine");
13
+ }
14
+ function createEngine(banditJson) {
15
+ if (!wasmModule) {
16
+ throw new Error("WASM not initialized \u2014 call initWasm() first");
17
+ }
18
+ return new wasmModule.BanditEngine(banditJson);
19
+ }
20
+ function updateEngine(engine, banditJson) {
21
+ engine.updateFromSync(banditJson);
22
+ }
23
+
24
+ // src/models.ts
25
+ function createArm(data) {
26
+ const arm = {
27
+ armId: data.arm_id,
28
+ modelName: data.model_name,
29
+ modelProvider: data.model_provider,
30
+ systemPrompt: data.system_prompt,
31
+ isPromptTemplated: data.is_prompt_templated ?? false,
32
+ get model() {
33
+ return this.modelName;
34
+ },
35
+ get prompt() {
36
+ return this.systemPrompt;
37
+ }
38
+ };
39
+ return Object.freeze(arm);
40
+ }
41
+ function createPullResult(data) {
42
+ const result = {
43
+ arm: data.arm,
44
+ eventId: data.eventId,
45
+ banditId: data.banditId,
46
+ banditName: data.banditName,
47
+ scores: Object.freeze({ ...data.scores }),
48
+ get model() {
49
+ return this.arm.modelName;
50
+ },
51
+ get prompt() {
52
+ return this.arm.systemPrompt;
53
+ },
54
+ _pullTime: data.pullTime
55
+ };
56
+ return Object.freeze(result);
57
+ }
58
+
59
+ // src/config.ts
60
+ import * as fs from "fs";
61
+ import * as path from "path";
62
+ import * as os from "os";
63
+ import { parse as parseToml } from "smol-toml";
64
+ var DEFAULT_BASE_URL = "https://bandito-api.onrender.com";
65
+ var CONFIG_DIR = path.join(os.homedir(), ".bandito");
66
+ var CONFIG_FILE = path.join(CONFIG_DIR, "config.toml");
67
+ function loadConfig() {
68
+ const config = {
69
+ apiKey: null,
70
+ baseUrl: DEFAULT_BASE_URL,
71
+ dataStorage: "local"
72
+ };
73
+ if (fs.existsSync(CONFIG_FILE)) {
74
+ try {
75
+ const content = fs.readFileSync(CONFIG_FILE, "utf-8");
76
+ const data = parseToml(content);
77
+ if (data.api_key) config.apiKey = data.api_key;
78
+ if (data.base_url) config.baseUrl = data.base_url;
79
+ if (data.data_storage) config.dataStorage = data.data_storage;
80
+ } catch {
81
+ }
82
+ }
83
+ const envKey = process.env.BANDITO_API_KEY;
84
+ if (envKey) config.apiKey = envKey;
85
+ const envUrl = process.env.BANDITO_BASE_URL;
86
+ if (envUrl) config.baseUrl = envUrl;
87
+ const envStorage = process.env.BANDITO_DATA_STORAGE;
88
+ if (envStorage) config.dataStorage = envStorage;
89
+ return config;
90
+ }
91
+
92
+ // src/http.ts
93
+ var MAX_RETRIES = 3;
94
+ var RETRY_BACKOFF_BASE = 500;
95
+ function sleep(ms) {
96
+ return new Promise((resolve) => setTimeout(resolve, ms));
97
+ }
98
+ function isRetryable(status) {
99
+ return status >= 500;
100
+ }
101
+ var BanditoHTTP = class {
102
+ baseUrl;
103
+ apiKey;
104
+ timeout;
105
+ constructor(baseUrl, apiKey, timeout = 1e4) {
106
+ this.baseUrl = `${baseUrl.replace(/\/$/, "")}/api/v1`;
107
+ this.apiKey = apiKey;
108
+ this.timeout = timeout;
109
+ }
110
+ async request(method, path3, body) {
111
+ let lastError = null;
112
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
113
+ const controller = new AbortController();
114
+ const timer = setTimeout(() => controller.abort(), this.timeout);
115
+ try {
116
+ const resp = await fetch(`${this.baseUrl}${path3}`, {
117
+ method,
118
+ headers: {
119
+ "X-API-Key": this.apiKey,
120
+ "Content-Type": "application/json"
121
+ },
122
+ body: body != null ? JSON.stringify(body) : void 0,
123
+ signal: controller.signal
124
+ });
125
+ clearTimeout(timer);
126
+ if (!resp.ok) {
127
+ const text = await resp.text().catch(() => "");
128
+ if (!isRetryable(resp.status) || attempt === MAX_RETRIES - 1) {
129
+ throw new Error(
130
+ `HTTP ${resp.status} on ${method} ${path3}: ${text}`
131
+ );
132
+ }
133
+ lastError = new Error(
134
+ `HTTP ${resp.status} on ${method} ${path3}: ${text}`
135
+ );
136
+ } else {
137
+ return await resp.json();
138
+ }
139
+ } catch (err) {
140
+ clearTimeout(timer);
141
+ lastError = err;
142
+ const isNetworkOrTimeout = err.name === "AbortError" || err.name === "TypeError";
143
+ if (!isNetworkOrTimeout && attempt < MAX_RETRIES - 1) {
144
+ throw err;
145
+ }
146
+ if (attempt === MAX_RETRIES - 1) {
147
+ throw err;
148
+ }
149
+ }
150
+ const delay = RETRY_BACKOFF_BASE * 2 ** attempt;
151
+ await sleep(delay);
152
+ }
153
+ throw lastError;
154
+ }
155
+ /** POST /sync/connect — SDK bootstrap. */
156
+ async connect() {
157
+ return this.request("POST", "/sync/connect");
158
+ }
159
+ /** POST /sync/heartbeat — periodic state refresh. */
160
+ async heartbeat() {
161
+ return this.request("POST", "/sync/heartbeat", {});
162
+ }
163
+ /** POST /events — batch event ingestion. */
164
+ async ingestEvents(events) {
165
+ return this.request("POST", "/events", { events });
166
+ }
167
+ /** PATCH /events/{uuid}/grade — submit human grade. */
168
+ async submitGrade(eventUuid, grade2) {
169
+ return this.request("PATCH", `/events/${eventUuid}/grade`, {
170
+ grade: grade2,
171
+ is_graded: true
172
+ });
173
+ }
174
+ };
175
+
176
+ // src/store.ts
177
+ import Database from "better-sqlite3";
178
+ var SCHEMA = `
179
+ CREATE TABLE IF NOT EXISTS events (
180
+ local_event_uuid TEXT PRIMARY KEY,
181
+ bandit_id INTEGER NOT NULL,
182
+ arm_id INTEGER NOT NULL,
183
+ payload TEXT NOT NULL,
184
+ status TEXT NOT NULL DEFAULT 'pending',
185
+ created_at REAL NOT NULL,
186
+ human_reward REAL,
187
+ graded_at REAL
188
+ );
189
+ CREATE INDEX IF NOT EXISTS idx_events_status ON events(status);
190
+ `;
191
+ var MIGRATION_GRADING = [
192
+ "ALTER TABLE events ADD COLUMN human_reward REAL",
193
+ "ALTER TABLE events ADD COLUMN graded_at REAL"
194
+ ];
195
+ var EventStore = class {
196
+ db;
197
+ pushStmt;
198
+ pendingStmt;
199
+ constructor(dbPath = ":memory:") {
200
+ this.db = new Database(dbPath);
201
+ this.db.pragma("journal_mode = WAL");
202
+ this.db.pragma("busy_timeout = 5000");
203
+ this.db.pragma("synchronous = NORMAL");
204
+ this.db.exec(SCHEMA);
205
+ this.migrate();
206
+ this.pushStmt = this.db.prepare(
207
+ `INSERT OR IGNORE INTO events
208
+ (local_event_uuid, bandit_id, arm_id, payload, status, created_at)
209
+ VALUES (?, ?, ?, ?, 'pending', ?)`
210
+ );
211
+ this.pendingStmt = this.db.prepare(
212
+ `SELECT payload FROM events WHERE status = 'pending'
213
+ ORDER BY created_at ASC LIMIT ?`
214
+ );
215
+ }
216
+ /** Insert a pending event. */
217
+ push(event) {
218
+ this.pushStmt.run(
219
+ event.local_event_uuid,
220
+ event.bandit_id,
221
+ event.arm_id,
222
+ JSON.stringify(event),
223
+ Date.now() / 1e3
224
+ );
225
+ }
226
+ /** Return up to `limit` pending events (oldest first). */
227
+ pending(limit = 50) {
228
+ const rows = this.pendingStmt.all(limit);
229
+ return rows.map((row) => JSON.parse(row.payload));
230
+ }
231
+ /** Mark events as successfully flushed to cloud. */
232
+ markFlushed(uuids) {
233
+ if (uuids.length === 0) return;
234
+ const placeholders = uuids.map(() => "?").join(",");
235
+ this.db.prepare(
236
+ `UPDATE events SET status = 'flushed'
237
+ WHERE local_event_uuid IN (${placeholders})`
238
+ ).run(...uuids);
239
+ }
240
+ /** Record a human grade locally. */
241
+ markGraded(uuid, reward) {
242
+ this.db.prepare(
243
+ `UPDATE events SET human_reward = ?, graded_at = ?
244
+ WHERE local_event_uuid = ?`
245
+ ).run(reward, Date.now() / 1e3, uuid);
246
+ }
247
+ /** Close the database connection. */
248
+ close() {
249
+ this.db.close();
250
+ }
251
+ migrate() {
252
+ for (const stmt of MIGRATION_GRADING) {
253
+ try {
254
+ this.db.exec(stmt);
255
+ } catch {
256
+ }
257
+ }
258
+ }
259
+ };
260
+
261
+ // src/worker.ts
262
+ var TEXT_FIELDS = ["query_text", "response"];
263
+ var METADATA_FIELDS = ["model_name", "model_provider"];
264
+ function prepareCloudPayload(events, includeText) {
265
+ return events.map((event) => {
266
+ const copy = { ...event };
267
+ for (const field of METADATA_FIELDS) {
268
+ delete copy[field];
269
+ }
270
+ if (!includeText) {
271
+ for (const field of TEXT_FIELDS) {
272
+ delete copy[field];
273
+ }
274
+ }
275
+ return copy;
276
+ });
277
+ }
278
+
279
+ // src/client.ts
280
+ var DEFAULT_STORE_PATH = path2.join(os2.homedir(), ".bandito", "events.db");
281
+ var MAX_EVENT_RETRIES = 5;
282
+ var BanditoClient = class {
283
+ apiKey;
284
+ baseUrl;
285
+ storePath;
286
+ dataStorageArg;
287
+ dataStorage;
288
+ http = null;
289
+ store = null;
290
+ engines = /* @__PURE__ */ new Map();
291
+ bandits = /* @__PURE__ */ new Map();
292
+ connected = false;
293
+ flushInterval = null;
294
+ flushInProgress = false;
295
+ deadUuids = /* @__PURE__ */ new Set();
296
+ retryCounts = /* @__PURE__ */ new Map();
297
+ constructor(options = {}) {
298
+ this.apiKey = options.apiKey;
299
+ this.baseUrl = options.baseUrl;
300
+ this.storePath = options.storePath;
301
+ this.dataStorageArg = options.dataStorage;
302
+ this.dataStorage = options.dataStorage ?? "local";
303
+ }
304
+ /**
305
+ * Bootstrap: authenticate and hydrate in-memory state from cloud.
306
+ *
307
+ * Resolves config from: constructor args → env vars → ~/.bandito/config.toml.
308
+ * Initializes WASM, creates HTTP client, SQLite store, fetches full state.
309
+ */
310
+ async connect() {
311
+ if (this.connected) {
312
+ await this.close();
313
+ }
314
+ await initWasm();
315
+ const config = loadConfig();
316
+ const apiKey = this.apiKey ?? config.apiKey;
317
+ if (!apiKey) {
318
+ throw new Error(
319
+ "apiKey required \u2014 pass it to constructor, set BANDITO_API_KEY, or run `bandito signup`"
320
+ );
321
+ }
322
+ const baseUrl = this.baseUrl ?? config.baseUrl;
323
+ if (!this.dataStorageArg) {
324
+ this.dataStorage = config.dataStorage;
325
+ }
326
+ this.http = new BanditoHTTP(baseUrl, apiKey);
327
+ const storePath = this.storePath ?? DEFAULT_STORE_PATH;
328
+ if (storePath !== ":memory:") {
329
+ const dir = path2.dirname(storePath);
330
+ if (!fs2.existsSync(dir)) {
331
+ fs2.mkdirSync(dir, { recursive: true });
332
+ }
333
+ }
334
+ this.store = new EventStore(storePath);
335
+ try {
336
+ const data = await this.http.connect();
337
+ this.applySync(data);
338
+ this.deadUuids.clear();
339
+ this.retryCounts.clear();
340
+ await this.flushPending();
341
+ this.flushInterval = setInterval(() => {
342
+ this.flushPending().catch(() => {
343
+ });
344
+ }, 3e4);
345
+ this.connected = true;
346
+ } catch (err) {
347
+ this.store?.close();
348
+ this.store = null;
349
+ this.http = null;
350
+ throw err;
351
+ }
352
+ }
353
+ /**
354
+ * Local Thompson Sampling decision. Synchronous, <1ms, no network.
355
+ */
356
+ pull(banditName, options = {}) {
357
+ this.ensureConnected();
358
+ const cache = this.bandits.get(banditName);
359
+ if (!cache) {
360
+ const available = [...this.bandits.keys()];
361
+ throw new Error(
362
+ `Unknown bandit '${banditName}'. Available: [${available.join(", ")}]`
363
+ );
364
+ }
365
+ if (cache.arms.length === 0) {
366
+ throw new Error(`Bandit '${banditName}' has no active arms`);
367
+ }
368
+ const engine = this.engines.get(banditName);
369
+ const queryLength = options.query?.length ?? void 0;
370
+ const excludeIds = options.exclude ? Int32Array.from(options.exclude) : void 0;
371
+ const resultJson = engine.pull(queryLength, excludeIds);
372
+ const raw = JSON.parse(resultJson);
373
+ const winnerArm = cache.arms.find((a) => a.armId === raw.arm_id);
374
+ if (!winnerArm) {
375
+ throw new Error(
376
+ `Engine selected arm ${raw.arm_id} but it's not in active arm cache for "${banditName}". This is likely a bug \u2014 please report it at https://github.com/bandito-ai/bandito/issues`
377
+ );
378
+ }
379
+ return createPullResult({
380
+ arm: winnerArm,
381
+ eventId: randomUUID(),
382
+ banditId: cache.banditId,
383
+ banditName,
384
+ scores: raw.scores,
385
+ pullTime: performance.now()
386
+ });
387
+ }
388
+ /**
389
+ * Record an LLM call outcome. Writes to SQLite first (crash-safe),
390
+ * then fires off a non-blocking flush to cloud.
391
+ */
392
+ update(pullResult, options = {}) {
393
+ this.ensureConnected();
394
+ let reward = options.reward;
395
+ if (options.failed && reward == null) {
396
+ reward = 0;
397
+ }
398
+ let latency = options.latency;
399
+ if (latency == null && pullResult._pullTime > 0) {
400
+ latency = performance.now() - pullResult._pullTime;
401
+ }
402
+ const event = {
403
+ local_event_uuid: pullResult.eventId,
404
+ bandit_id: pullResult.banditId,
405
+ arm_id: pullResult.arm.armId,
406
+ model_name: pullResult.arm.modelName,
407
+ model_provider: pullResult.arm.modelProvider
408
+ };
409
+ if (options.queryText != null) {
410
+ event.query_text = options.queryText;
411
+ }
412
+ if (options.response != null) {
413
+ event.response = typeof options.response === "string" ? { response: options.response } : options.response;
414
+ }
415
+ if (reward != null) {
416
+ event.early_reward = reward;
417
+ }
418
+ if (options.cost != null) {
419
+ event.cost = options.cost;
420
+ }
421
+ if (latency != null) {
422
+ event.latency = latency;
423
+ }
424
+ if (options.inputTokens != null) {
425
+ event.input_tokens = options.inputTokens;
426
+ }
427
+ if (options.outputTokens != null) {
428
+ event.output_tokens = options.outputTokens;
429
+ }
430
+ if (options.segment != null) {
431
+ event.segment = options.segment;
432
+ }
433
+ if (options.failed) {
434
+ event.run_error = true;
435
+ }
436
+ this.store.push(event);
437
+ this.flushPending().catch(() => {
438
+ });
439
+ }
440
+ /**
441
+ * Send a human grade for an existing event. Async (HTTP).
442
+ */
443
+ async grade(eventId, grade2) {
444
+ this.ensureConnected();
445
+ await this.http.submitGrade(eventId, grade2);
446
+ }
447
+ /**
448
+ * Explicit state refresh from cloud.
449
+ */
450
+ async sync() {
451
+ this.ensureConnected();
452
+ const data = await this.http.heartbeat();
453
+ const prevBandits = new Map(this.bandits);
454
+ const prevEngines = new Map(this.engines);
455
+ try {
456
+ this.applySync(data);
457
+ } catch (err) {
458
+ this.bandits = prevBandits;
459
+ this.engines = prevEngines;
460
+ console.warn("[bandito] Sync response malformed \u2014 keeping last-known-good state", err);
461
+ }
462
+ }
463
+ /**
464
+ * Shut down: clear interval, flush remaining events, close connections.
465
+ */
466
+ async close() {
467
+ if (this.flushInterval) {
468
+ clearInterval(this.flushInterval);
469
+ this.flushInterval = null;
470
+ }
471
+ if (this.store && this.http) {
472
+ await this.flushPending();
473
+ }
474
+ this.store?.close();
475
+ this.store = null;
476
+ this.http = null;
477
+ this.engines.clear();
478
+ this.bandits.clear();
479
+ this.connected = false;
480
+ }
481
+ // --- Internal ---
482
+ ensureConnected() {
483
+ if (!this.connected) {
484
+ throw new Error("Not connected \u2014 call connect() first");
485
+ }
486
+ }
487
+ applySync(data) {
488
+ const banditsData = data.bandits ?? [];
489
+ const newBandits = /* @__PURE__ */ new Map();
490
+ const newEngines = /* @__PURE__ */ new Map();
491
+ for (const b of banditsData) {
492
+ const arms = b.arms ?? [];
493
+ if (arms.length === 0) continue;
494
+ const activeArms = arms.filter((a) => a.is_active).map((a) => createArm(a));
495
+ const name = b.name;
496
+ const cache = {
497
+ banditId: Number(b.bandit_id),
498
+ name,
499
+ arms: activeArms,
500
+ armWire: arms,
501
+ optimizationMode: b.optimization_mode ?? "base",
502
+ avgLatencyLastN: b.avg_latency_last_n,
503
+ budget: b.budget,
504
+ totalCost: b.total_cost
505
+ };
506
+ newBandits.set(name, cache);
507
+ const existingEngine = this.engines.get(name);
508
+ const banditJson = JSON.stringify(b);
509
+ if (existingEngine) {
510
+ updateEngine(existingEngine, banditJson);
511
+ newEngines.set(name, existingEngine);
512
+ } else {
513
+ newEngines.set(name, createEngine(banditJson));
514
+ }
515
+ }
516
+ this.bandits = newBandits;
517
+ this.engines = newEngines;
518
+ }
519
+ async flushPending() {
520
+ if (this.flushInProgress || !this.store || !this.http) return;
521
+ this.flushInProgress = true;
522
+ try {
523
+ const pending = this.store.pending();
524
+ if (pending.length === 0) return;
525
+ const alive = this.deadUuids.size > 0 ? pending.filter((e) => !this.deadUuids.has(e.local_event_uuid)) : pending;
526
+ if (alive.length === 0) return;
527
+ const payload = prepareCloudPayload(
528
+ alive,
529
+ this.dataStorage !== "local"
530
+ );
531
+ const result = await this.http.ingestEvents(payload);
532
+ const errors = result.errors ?? [];
533
+ const erroredUuids = new Set(
534
+ errors.map((e) => e.local_event_uuid).filter(Boolean)
535
+ );
536
+ for (const uid of erroredUuids) {
537
+ const count = (this.retryCounts.get(uid) ?? 0) + 1;
538
+ this.retryCounts.set(uid, count);
539
+ if (count >= MAX_EVENT_RETRIES) {
540
+ this.deadUuids.add(uid);
541
+ }
542
+ }
543
+ const flushedUuids = alive.map((e) => e.local_event_uuid).filter((uid) => !erroredUuids.has(uid));
544
+ if (flushedUuids.length > 0 && this.store) {
545
+ this.store.markFlushed(flushedUuids);
546
+ }
547
+ } catch (err) {
548
+ console.warn("[bandito] Event flush failed \u2014 will retry", err);
549
+ } finally {
550
+ this.flushInProgress = false;
551
+ }
552
+ }
553
+ };
554
+
555
+ // src/index.ts
556
+ var _client = null;
557
+ function getClient() {
558
+ if (!_client) {
559
+ throw new Error("Not connected \u2014 call connect() first");
560
+ }
561
+ return _client;
562
+ }
563
+ async function connect(options = {}) {
564
+ if (_client) {
565
+ await _client.close();
566
+ }
567
+ _client = new BanditoClient(options);
568
+ await _client.connect();
569
+ }
570
+ function pull(banditName, options) {
571
+ return getClient().pull(banditName, options);
572
+ }
573
+ function update(pullResult, options) {
574
+ getClient().update(pullResult, options);
575
+ }
576
+ async function grade(eventId, gradeValue) {
577
+ await getClient().grade(eventId, gradeValue);
578
+ }
579
+ async function sync() {
580
+ await getClient().sync();
581
+ }
582
+ async function close() {
583
+ if (_client) {
584
+ await _client.close();
585
+ _client = null;
586
+ }
587
+ }
588
+ export {
589
+ BanditoClient,
590
+ close,
591
+ connect,
592
+ grade,
593
+ pull,
594
+ sync,
595
+ update
596
+ };
597
+ //# sourceMappingURL=index.mjs.map