UNPKG

trellis

Version:

Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications

259 lines (253 loc) 7.86 kB
import { canonicalOpBodyFromOp, init_canonical_op } from "./chunk-RUMOVKR4.js"; import { __require } from "./chunk-2ESYSVXG.js"; // src/core/persist/sqlite-backend.ts init_canonical_op(); var _DatabaseCtor = null; function loadDatabaseCtor() { if (_DatabaseCtor) return _DatabaseCtor; try { const { createRequire } = __require("module"); const requireCJS = createRequire(import.meta.url); _DatabaseCtor = requireCJS("bun:sqlite").Database; return _DatabaseCtor; } catch { throw new Error( "SqliteKernelBackend requires the Bun runtime (built-in `bun:sqlite`). In Node / WebContainer use `createKernelBackend()` from `trellis/core` \u2014 it auto-selects better-sqlite3 or sql.js." ); } } var SCHEMA_SQL = ` CREATE TABLE IF NOT EXISTS ops ( hash TEXT PRIMARY KEY, kind TEXT NOT NULL, timestamp TEXT NOT NULL, agent_id TEXT NOT NULL, previous_hash TEXT, payload TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, last_op_hash TEXT NOT NULL, data TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS blobs ( hash TEXT PRIMARY KEY, content BLOB NOT NULL ); CREATE INDEX IF NOT EXISTS idx_ops_kind ON ops(kind); CREATE INDEX IF NOT EXISTS idx_ops_timestamp ON ops(timestamp); CREATE INDEX IF NOT EXISTS idx_ops_agent ON ops(agent_id); CREATE INDEX IF NOT EXISTS idx_ops_previous ON ops(previous_hash); CREATE INDEX IF NOT EXISTS idx_snapshots_op ON snapshots(last_op_hash); `; var SqliteKernelBackend = class { constructor(dbPath) { this.dbPath = dbPath; const DatabaseCtor = loadDatabaseCtor(); this.db = new DatabaseCtor(dbPath); } db; _stmts = null; init() { this.db.exec("PRAGMA journal_mode=WAL;"); this.db.exec("PRAGMA foreign_keys=ON;"); this.db.exec("PRAGMA synchronous=NORMAL;"); this.db.exec(SCHEMA_SQL); this._prepareStatements(); } _prepareStatements() { this._stmts = { insert: this.db.prepare(` INSERT OR IGNORE INTO ops (hash, kind, timestamp, agent_id, previous_hash, payload) VALUES ($hash, $kind, $timestamp, $agentId, $previousHash, $payload) `), readAll: this.db.prepare(` SELECT hash, kind, timestamp, agent_id, previous_hash, payload FROM ops ORDER BY rowid ASC `), readUntil: this.db.prepare(` SELECT hash, kind, timestamp, agent_id, previous_hash, payload FROM ops WHERE rowid <= (SELECT rowid FROM ops WHERE hash = $hash) ORDER BY rowid ASC `), readAfter: this.db.prepare(` SELECT hash, kind, timestamp, agent_id, previous_hash, payload FROM ops WHERE rowid > (SELECT rowid FROM ops WHERE hash = $hash) ORDER BY rowid ASC `), getByHash: this.db.prepare(` SELECT hash, kind, timestamp, agent_id, previous_hash, payload FROM ops WHERE hash = $hash `), getLast: this.db.prepare(` SELECT hash, kind, timestamp, agent_id, previous_hash, payload FROM ops ORDER BY rowid DESC LIMIT 1 `), count: this.db.prepare("SELECT COUNT(*) as cnt FROM ops"), saveSnapshot: this.db.prepare(` INSERT INTO snapshots (last_op_hash, data) VALUES ($lastOpHash, $data) `), loadSnapshot: this.db.prepare(` SELECT last_op_hash, data FROM snapshots ORDER BY id DESC LIMIT 1 `), putBlob: this.db.prepare(` INSERT OR IGNORE INTO blobs (hash, content) VALUES ($hash, $content) `), getBlob: this.db.prepare(` SELECT content FROM blobs WHERE hash = $hash `), hasBlob: this.db.prepare(` SELECT 1 FROM blobs WHERE hash = $hash `) }; } // ------------------------------------------------------------------------- // Op operations // ------------------------------------------------------------------------- append(op) { const payload = canonicalOpBodyFromOp(op); this._stmts.insert.run({ $hash: op.hash, $kind: op.kind, $timestamp: op.timestamp, $agentId: op.agentId, $previousHash: op.previousHash ?? null, $payload: payload }); } appendBatch(ops) { if (ops.length === 0) return; this.db.transaction(() => { for (const op of ops) { this.append(op); } })(); } readAll() { const rows = this._stmts.readAll.all(); return rows.map(rowToOp); } readUntil(hash) { const rows = this._stmts.readUntil.all({ $hash: hash }); return rows.map(rowToOp); } readAfter(hash) { const rows = this._stmts.readAfter.all({ $hash: hash }); return rows.map(rowToOp); } readUntilTimestamp(isoTimestamp) { const rows = this.db.prepare( `SELECT hash, kind, timestamp, agent_id, previous_hash, payload FROM ops WHERE timestamp <= $ts ORDER BY rowid ASC` ).all({ $ts: isoTimestamp }); return rows.map(rowToOp); } getLastOp() { const row = this._stmts.getLast.get(); return row ? rowToOp(row) : void 0; } getByHash(hash) { return this.getOpByHash(hash); } getOpCount() { return this.count(); } getOpByHash(hash) { const row = this._stmts.getByHash.get({ $hash: hash }); return row ? rowToOp(row) : void 0; } count() { const row = this._stmts.count.get(); return row?.cnt ?? 0; } /** * Find the common ancestor op of two op hashes by walking * previousHash chains until they converge. */ findCommonAncestor(hashA, hashB) { const ancestorsA = /* @__PURE__ */ new Set(); let cursor = hashA; while (cursor) { ancestorsA.add(cursor); const op = this.getOpByHash(cursor); cursor = op?.previousHash; } cursor = hashB; while (cursor) { if (ancestorsA.has(cursor)) { return this.getOpByHash(cursor); } const op = this.getOpByHash(cursor); cursor = op?.previousHash; } return void 0; } // ------------------------------------------------------------------------- // Snapshot operations // ------------------------------------------------------------------------- saveSnapshot(lastOpHash, data) { this._stmts.saveSnapshot.run({ $lastOpHash: lastOpHash, $data: JSON.stringify(data) }); } loadLatestSnapshot() { const row = this._stmts.loadSnapshot.get(); if (!row) return void 0; return { lastOpHash: row.last_op_hash, data: JSON.parse(row.data) }; } // ------------------------------------------------------------------------- // Blob operations // ------------------------------------------------------------------------- putBlob(hash, content) { this._stmts.putBlob.run({ $hash: hash, $content: Buffer.from(content) }); } getBlob(hash) { const row = this._stmts.getBlob.get({ $hash: hash }); if (!row) return void 0; return new Uint8Array(row.content); } hasBlob(hash) { return !!this._stmts.hasBlob.get({ $hash: hash }); } // ------------------------------------------------------------------------- // Lifecycle // ------------------------------------------------------------------------- close() { this.db.close(); } }; function rowToOp(row) { const payload = JSON.parse(row.payload); const op = { hash: row.hash, kind: row.kind, timestamp: row.timestamp, agentId: row.agent_id }; if (row.previous_hash) op.previousHash = row.previous_hash; if (payload.facts) op.facts = payload.facts; if (payload.links) op.links = payload.links; if (payload.deleteFacts) op.deleteFacts = payload.deleteFacts; if (payload.deleteLinks) op.deleteLinks = payload.deleteLinks; if (payload.v !== void 0) op.v = payload.v; if (payload.provenance) op.provenance = payload.provenance; return op; } export { SqliteKernelBackend };