UNPKG

trellis

Version:

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

629 lines (620 loc) 17.1 kB
import { IrohSyncTransport } from "../chunk-UAQCJ2CF.js"; import { PartyKitRoomTransport, TrellisVcsSyncPeer } from "../chunk-44NB2G5E.js"; import { DEFAULT_SNAPSHOT_MAX_OPS, MAX_SUPPORTED_VERSION, MIN_SUPPORTED_VERSION, PROTOCOL_VERSION, SyncEngine, SyncRoomCore, findForkPoint, init_reconciler, reconcile } from "../chunk-TIRVQJDD.js"; import { PROVENANCE, init_canonical_op } from "../chunk-RUMOVKR4.js"; import "../chunk-GRWQPKYK.js"; import "../chunk-2ESYSVXG.js"; // src/sync/index.ts init_reconciler(); // src/sync/memory-transport.ts var MemoryTransport = class { peerId; peerName; handlers = []; connectedPeers = /* @__PURE__ */ new Map(); constructor(peerId, peerName = peerId) { this.peerId = peerId; this.peerName = peerName; } /** * Connect two transports so they can exchange messages. */ static connect(a, b) { a.connectedPeers.set(b.peerId, b); b.connectedPeers.set(a.peerId, a); } /** * Disconnect two transports. */ static disconnect(a, b) { a.connectedPeers.delete(b.peerId); b.connectedPeers.delete(a.peerId); } async send(peerId, message) { const peer = this.connectedPeers.get(peerId); if (!peer) { throw new Error(`Peer not connected: ${peerId}`); } for (const handler of peer.handlers) { await handler(message); } } onMessage(handler) { this.handlers.push(handler); } peers() { return [...this.connectedPeers.entries()].map(([id, transport]) => ({ id, name: transport.peerName })); } getPeerId() { return this.peerId; } }; // src/sync/memory-room.ts var MemorySyncRoom = class { core; transports = /* @__PURE__ */ new Map(); constructor(roomId = "room", roomName = roomId) { this.core = new SyncRoomCore(roomId, roomName); } connectPeer(peerId, peerName = peerId) { this.core.connectPeer(peerId, peerName); const transport = new MemoryRoomTransport(this, peerId, peerName); this.transports.set(peerId, transport); return transport; } disconnectPeer(peerId) { this.core.disconnectPeer(peerId); this.transports.delete(peerId); } getRoomPeer() { return this.core.getRoomPeer(); } peersFor(peerId) { return this.core.peersFor(peerId); } getOps() { return this.core.getOps(); } getOpCount() { return this.core.getOpCount(); } async receive(fromPeerId, message) { const deliveries = await this.core.receive(fromPeerId, message); for (const delivery of deliveries) { await this.deliver(delivery.peerId, delivery.message); } } async deliver(peerId, message) { const transport = this.transports.get(peerId); if (!transport) { throw new Error(`Peer not connected to room: ${peerId}`); } await transport.deliver(message); } }; var MemoryRoomTransport = class { room; peerId; peerName; handlers = []; constructor(room, peerId, peerName = peerId) { this.room = room; this.peerId = peerId; this.peerName = peerName; } async send(peerId, message) { if (peerId !== this.room.getRoomPeer().id) { throw new Error(`Peer not connected through room: ${peerId}`); } await this.room.receive(this.peerId, message); } onMessage(handler) { this.handlers.push(handler); } peers() { return this.room.peersFor(this.peerId); } getPeerId() { return this.peerId; } getPeerName() { return this.peerName; } async deliver(message) { for (const handler of this.handlers) { await handler(message); } } }; // src/sync/sync-room-server.ts var SyncRoomServer = class { core; connections = /* @__PURE__ */ new Map(); welcomeSnapshot; welcomeSnapshotMaxOps; constructor(opts = {}) { this.core = new SyncRoomCore(opts.roomId, opts.roomName); this.welcomeSnapshot = opts.welcomeSnapshot !== false; this.welcomeSnapshotMaxOps = opts.welcomeSnapshotMaxOps ?? DEFAULT_SNAPSHOT_MAX_OPS; } getRoomId() { return this.core.getRoomPeer().id; } getOpCount() { return this.core.getOpCount(); } getOps() { return this.core.getOps(); } buildSnapshot(maxOps) { return this.core.buildSnapshot(maxOps); } /** Register a connected peer and optionally send a welcome snapshot. */ async connect(conn) { this.core.connectPeer(conn.peerId); this.connections.set(conn.peerId, conn); if (this.welcomeSnapshot) { await this.sendSnapshot(conn.peerId, this.welcomeSnapshotMaxOps); } } disconnect(peerId) { this.core.disconnectPeer(peerId); this.connections.delete(peerId); } /** Route an inbound sync message from a peer. */ async handleMessage(fromPeerId, message) { const deliveries = await this.core.receive(fromPeerId, message); await this.deliverAll(deliveries); } /** Push a tail snapshot to a peer without an inbound message. */ async sendSnapshot(peerId, maxOps = DEFAULT_SNAPSHOT_MAX_OPS) { const deliveries = this.core.snapshotDeliveries(peerId, maxOps); await this.deliverAll(deliveries); } async deliverAll(deliveries) { for (const delivery of deliveries) { const conn = this.connections.get(delivery.peerId); if (!conn) continue; await conn.send(delivery.message); } } }; // src/sync/partykit-room.ts function createPartyKitRoomHandler(opts = {}) { function getState(room) { const bag = room; if (!bag.trellis) { bag.trellis = { server: new SyncRoomServer({ roomId: "room", roomName: room.id, welcomeSnapshot: opts.welcomeSnapshot, welcomeSnapshotMaxOps: opts.welcomeSnapshotMaxOps }) }; } return bag.trellis; } function peerIdFromConnection(connection, requestUrl) { if (requestUrl) { try { const peer = new URL(requestUrl).searchParams.get("peerId"); if (peer) return peer; } catch { } } return connection.id; } return { async onConnect(connection, room, request) { const { server } = getState(room); const peerId = peerIdFromConnection(connection, request?.url); const conn = { peerId, send(message) { connection.send(JSON.stringify(message)); } }; await server.connect(conn); }, async onMessage(message, connection, room, request) { const { server } = getState(room); const peerId = peerIdFromConnection(connection, request?.url); const raw = typeof message === "string" ? message : new TextDecoder().decode(message); let parsed; try { parsed = JSON.parse(raw); } catch { return; } if (!parsed || typeof parsed.type !== "string") return; await server.handleMessage(peerId, parsed); }, onClose(connection, room, request) { const { server } = getState(room); const peerId = peerIdFromConnection(connection, request?.url); server.disconnect(peerId); } }; } // src/sync/http-transport.ts var HttpSyncTransport = class { localPeerId; peerUrls = /* @__PURE__ */ new Map(); messageHandler = null; knownPeers = /* @__PURE__ */ new Map(); constructor(localPeerId) { this.localPeerId = localPeerId; } /** * Add a remote peer by URL (e.g. "http://192.168.1.10:4200"). */ addPeer(peerId, url, name) { this.peerUrls.set(peerId, url); this.knownPeers.set(peerId, { id: peerId, name: name ?? peerId, lastSeen: (/* @__PURE__ */ new Date()).toISOString() }); } /** * Remove a remote peer. */ removePeer(peerId) { this.peerUrls.delete(peerId); this.knownPeers.delete(peerId); } async send(peerId, message) { const url = this.peerUrls.get(peerId); if (!url) throw new Error(`Unknown peer "${peerId}". Add it with addPeer() first.`); const resp = await fetch(`${url}/sync/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message) }); if (!resp.ok) { throw new Error(`Sync message to ${peerId} failed: ${resp.status} ${resp.statusText}`); } const contentType = resp.headers.get("content-type"); if (contentType?.includes("application/json")) { const reply = await resp.json(); if (reply && reply.type && this.messageHandler) { await this.messageHandler(reply); } } } onMessage(handler) { this.messageHandler = handler; } /** * Receive a message (called by the HTTP server handler). */ async receiveMessage(message) { this.knownPeers.set(message.peerId, { id: message.peerId, name: message.peerId, lastSeen: (/* @__PURE__ */ new Date()).toISOString() }); if (this.messageHandler) { await this.messageHandler(message); } } peers() { return [...this.knownPeers.values()]; } getLocalPeerId() { return this.localPeerId; } }; function createSyncHandler(transport) { return (req) => { const url = new URL(req.url); if (url.pathname === "/sync/message" && req.method === "POST") { return new Response( req.json().then(async (body) => { await transport.receiveMessage(body); return JSON.stringify({ ok: true }); }), { headers: { "Content-Type": "application/json" } } ); } if (url.pathname === "/sync/peers" && req.method === "GET") { return new Response( JSON.stringify({ localPeerId: transport.getLocalPeerId(), peers: transport.peers() }), { headers: { "Content-Type": "application/json" } } ); } return null; }; } // src/sync/ws-transport.ts var WebSocketSyncTransport = class { localPeerId; connections = /* @__PURE__ */ new Map(); knownPeers = /* @__PURE__ */ new Map(); messageHandler = null; constructor(localPeerId) { this.localPeerId = localPeerId; } /** * Connect to a remote peer's WebSocket endpoint. */ async connect(peerId, url, name) { return new Promise((resolve, reject) => { const ws = new WebSocket(url); ws.onopen = () => { this.connections.set(peerId, ws); this.knownPeers.set(peerId, { id: peerId, name: name ?? peerId, lastSeen: (/* @__PURE__ */ new Date()).toISOString() }); resolve(); }; ws.onmessage = async (event) => { try { const msg = JSON.parse(String(event.data)); this.knownPeers.set(msg.peerId, { id: msg.peerId, name: msg.peerId, lastSeen: (/* @__PURE__ */ new Date()).toISOString() }); if (this.messageHandler) { await this.messageHandler(msg); } } catch { } }; ws.onclose = () => { this.connections.delete(peerId); }; ws.onerror = (err) => { reject(new Error(`WebSocket connection to ${peerId} failed`)); }; }); } /** * Accept an incoming WebSocket connection (server side). */ acceptConnection(peerId, ws) { this.connections.set(peerId, ws); this.knownPeers.set(peerId, { id: peerId, name: peerId, lastSeen: (/* @__PURE__ */ new Date()).toISOString() }); ws.onmessage = async (event) => { try { const msg = JSON.parse(String(event.data)); this.knownPeers.set(msg.peerId, { id: msg.peerId, name: msg.peerId, lastSeen: (/* @__PURE__ */ new Date()).toISOString() }); if (this.messageHandler) { await this.messageHandler(msg); } } catch { } }; ws.onclose = () => { this.connections.delete(peerId); }; } async send(peerId, message) { const ws = this.connections.get(peerId); if (!ws || ws.readyState !== WebSocket.OPEN) { throw new Error(`No open connection to peer "${peerId}".`); } ws.send(JSON.stringify(message)); } onMessage(handler) { this.messageHandler = handler; } peers() { return [...this.knownPeers.values()]; } /** * Disconnect from a peer. */ disconnect(peerId) { const ws = this.connections.get(peerId); if (ws) { ws.close(); this.connections.delete(peerId); } return Promise.resolve(); } /** * Disconnect from all peers. */ disconnectAll() { for (const [id, ws] of this.connections) { ws.close(); } this.connections.clear(); } /** * Check if connected to a specific peer. */ isConnected(peerId) { const ws = this.connections.get(peerId); return ws !== void 0 && ws.readyState === WebSocket.OPEN; } getLocalPeerId() { return this.localPeerId; } }; // src/sync/multi-repo.ts init_canonical_op(); var SYNC_CTX = { provenance: PROVENANCE.sync }; var MultiRepoManager = class { kernel; constructor(kernel) { this.kernel = kernel; } /** * Link a remote repository. */ async linkRepo(alias, location, description) { const id = `repo:${alias}`; const existing = this.kernel.getEntity(id); if (existing) { throw new Error(`Repo alias "${alias}" is already linked to "${existing.facts.find((f) => f.a === "location")?.v}".`); } await this.kernel.createEntity(id, "LinkedRepo", { alias, location, ...description ? { description } : {}, linkedAt: (/* @__PURE__ */ new Date()).toISOString() }, void 0, SYNC_CTX); } /** * Unlink a remote repository. */ async unlinkRepo(alias) { const id = `repo:${alias}`; await this.kernel.deleteEntity(id, SYNC_CTX); } /** * List all linked repos. */ listLinkedRepos() { const entities = this.kernel.listEntities("LinkedRepo"); return entities.map((e) => { const get = (a) => e.facts.find((f) => f.a === a)?.v; return { alias: String(get("alias") ?? ""), location: String(get("location") ?? ""), description: get("description"), linkedAt: String(get("linkedAt") ?? ""), lastSyncedAt: get("lastSyncedAt") }; }); } /** * Get a linked repo by alias. */ getLinkedRepo(alias) { const entity = this.kernel.getEntity(`repo:${alias}`); if (!entity) return null; const get = (a) => entity.facts.find((f) => f.a === a)?.v; return { alias: String(get("alias") ?? ""), location: String(get("location") ?? ""), description: get("description"), linkedAt: String(get("linkedAt") ?? ""), lastSyncedAt: get("lastSyncedAt") }; } /** * Create a cross-repo link: entity in this repo → entity in remote repo. */ async addCrossRepoLink(sourceEntityId, attribute, targetRepoAlias, targetEntityId) { const repo = this.getLinkedRepo(targetRepoAlias); if (!repo) { throw new Error(`Repo alias "${targetRepoAlias}" is not linked. Use linkRepo() first.`); } const crossRef = `@${targetRepoAlias}:${targetEntityId}`; await this.kernel.addLink(sourceEntityId, attribute, crossRef, SYNC_CTX); } /** * Remove a cross-repo link. */ async removeCrossRepoLink(sourceEntityId, attribute, targetRepoAlias, targetEntityId) { const crossRef = `@${targetRepoAlias}:${targetEntityId}`; await this.kernel.removeLink(sourceEntityId, attribute, crossRef, SYNC_CTX); } /** * Find all cross-repo references from a given entity. */ getCrossRepoLinks(entityId) { const store = this.kernel.getStore(); const links = store.getLinksByEntity(entityId); const results = []; for (const link of links) { if (link.e1 !== entityId) continue; const parsed = parseCrossRepoRef(link.e2); if (parsed) { results.push({ attribute: link.a, ref: parsed }); } } return results; } /** * Find all cross-repo references pointing to a specific remote entity. */ findReferencesTo(repoAlias, entityId) { const crossRef = `@${repoAlias}:${entityId}`; const store = this.kernel.getStore(); return store.getAllLinks().filter((l) => l.e2 === crossRef); } /** * Update the lastSyncedAt timestamp for a linked repo. */ async markSynced(alias) { await this.kernel.updateEntity(`repo:${alias}`, { lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() }, SYNC_CTX); } }; function parseCrossRepoRef(ref) { if (!ref.startsWith("@")) return null; const colonIdx = ref.indexOf(":", 1); if (colonIdx === -1) return null; return { repoAlias: ref.slice(1, colonIdx), entityId: ref.slice(colonIdx + 1) }; } function formatCrossRepoRef(repoAlias, entityId) { return `@${repoAlias}:${entityId}`; } export { DEFAULT_SNAPSHOT_MAX_OPS, HttpSyncTransport, IrohSyncTransport, MAX_SUPPORTED_VERSION, MIN_SUPPORTED_VERSION, MemoryRoomTransport, MemorySyncRoom, MemoryTransport, MultiRepoManager, PROTOCOL_VERSION, PartyKitRoomTransport, SyncEngine, SyncRoomCore, SyncRoomServer, TrellisVcsSyncPeer, WebSocketSyncTransport, createPartyKitRoomHandler, createSyncHandler, findForkPoint, formatCrossRepoRef, parseCrossRepoRef, reconcile };