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
JavaScript
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
};