UNPKG

trellis

Version:

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

335 lines (331 loc) 11.6 kB
import { PROVENANCE, init_canonical_op } from "../../chunk-RUMOVKR4.js"; import "../../chunk-2ESYSVXG.js"; // src/plugins/agent-memory/ontology.ts var agentMemoryOntology = { id: "trellis:agent-memory", name: "Agent Memory", version: "1.0.0", description: "Graph-persisted agent conversations and message history", entities: [ { name: "Conversation", description: "A thread of messages between user and agent", attributes: [ { name: "title", type: "string", required: true }, { name: "model", type: "string", description: "LLM model used for this conversation" }, { name: "status", type: "string", enum: ["active", "archived"], default: "active" }, { name: "createdAt", type: "string", required: true, description: "ISO 8601 timestamp" }, { name: "createdBy", type: "string", description: "Agent or user ID that initiated" }, { name: "agentId", type: "ref", description: "Agent entity this conversation belongs to", refTypes: ["Agent"] }, { name: "runId", type: "ref", description: "AgentRun this conversation is associated with", refTypes: ["AgentRun"] } ] }, { name: "Message", description: "A single message within a conversation", attributes: [ { name: "role", type: "string", required: true, enum: ["system", "user", "assistant", "tool"] }, { name: "content", type: "string", description: "Message content (may be null for tool-call-only messages)" }, { name: "timestamp", type: "string", required: true, description: "ISO 8601 timestamp" }, { name: "tokenCount", type: "number", description: "Estimated token count" }, { name: "name", type: "string", description: "Tool name for tool messages" }, { name: "toolCallId", type: "string", description: "Tool call ID for tool response messages" }, { name: "status", type: "string", enum: ["active", "archived"], default: "active", description: "Archived messages are preserved for Idea Garden but excluded from active context" } ] } ], relations: [ { name: "hasMessage", sourceTypes: ["Conversation"], targetTypes: ["Message"], cardinality: "many" } ] }; // src/plugins/agent-memory/plugin.ts function createAgentMemoryPlugin() { const isMemoryEntity = (data) => { if (!data || typeof data !== "object") return false; const d = data; return d.type === "Conversation" || d.type === "Message"; }; return { id: "trellis:agent-memory", name: "Agent Memory", version: "1.0.0", description: "Graph-persisted agent conversations and message history", ontologies: [agentMemoryOntology], eventHandlers: [ { event: "entity:created", handler: (data) => { if (!isMemoryEntity(data)) return; } }, { event: "entity:updated", handler: (data) => { if (!isMemoryEntity(data)) return; } }, { event: "entity:deleted", handler: (data) => { if (!isMemoryEntity(data)) return; } } ], onLoad: async (ctx) => { ctx.log("Agent memory system loaded"); }, onUnload: async (ctx) => { ctx.log("Agent memory system unloaded"); } }; } // src/plugins/agent-memory/graph-context-manager.ts init_canonical_op(); var AGENT_CTX = { provenance: PROVENANCE.agent }; var globalIdCounter = 0; var GraphContextManager = class { kernel; conversationId = null; messageCounter = 0; /** * In-memory cache of active messages for fast getHistory() reads. * Always kept in sync with the graph via addMessage/prune/resume. */ cache = []; /** In-flight graph writes from fire-and-forget addMessage/prune calls. */ pendingWrites = /* @__PURE__ */ new Set(); constructor(kernel) { this.kernel = kernel; } // ------------------------------------------------------------------------- // Conversation lifecycle // ------------------------------------------------------------------------- /** * Create a new conversation entity and set it as the active context. * Returns the conversation entity ID. */ async createConversation(opts) { const id = `conversation:${Date.now()}:${++globalIdCounter}`; const attrs = { title: opts.title, status: "active" }; if (opts.agentId) attrs.agentId = opts.agentId; if (opts.model) attrs.model = opts.model; if (opts.createdBy) attrs.createdBy = opts.createdBy; await this.kernel.createEntity(id, "Conversation", attrs, void 0, AGENT_CTX); this.conversationId = id; this.cache = []; this.messageCounter = 0; return id; } /** * Resume an existing conversation by loading its messages from the graph. */ async resumeConversation(conversationId) { const entity = this.kernel.getEntity(conversationId); if (!entity || entity.type !== "Conversation") { throw new Error(`Conversation "${conversationId}" not found.`); } this.conversationId = conversationId; this.cache = this._loadMessagesFromGraph(conversationId); this.messageCounter = this.cache.reduce( (max, record) => Math.max(max, record.sequence), 0 ); } /** * Get the active conversation ID, or null if none is set. */ getConversationId() { return this.conversationId; } /** * Archive the active conversation and clear local state. */ async archiveConversation() { if (!this.conversationId) return; await this.kernel.updateEntity(this.conversationId, { status: "archived" }, AGENT_CTX); this.conversationId = null; this.cache = []; } // ------------------------------------------------------------------------- // ContextManager implementation // ------------------------------------------------------------------------- addMessage(message) { if (!this.conversationId) { throw new Error("GraphContextManager: No active conversation. Call createConversation() first."); } const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const sequence = ++this.messageCounter; const entityId = `message:${this.conversationId.replace("conversation:", "")}:${++globalIdCounter}`; const attrs = { role: message.role, timestamp, sequence, status: "active" }; if (message.content != null) attrs.content = message.content; if (message.name) attrs.name = message.name; if (message.tool_call_id) attrs.toolCallId = message.tool_call_id; const tokenCount = this.calculateTokenCount(message); attrs.tokenCount = tokenCount; const write = this.kernel.createEntity(entityId, "Message", attrs, [ { attribute: "hasMessage", targetEntityId: entityId } ]).then(() => { return this.kernel.addLink(this.conversationId, "hasMessage", entityId, AGENT_CTX); }).catch((err) => { console.error(`GraphContextManager: Failed to persist message ${entityId}:`, err); }).then(() => void 0); this.trackWrite(write); this.cache.push({ entityId, message, timestamp, sequence, status: "active" }); } getHistory() { return this.cache.filter((r) => r.status === "active").map((r) => r.message); } async prune(targetTokenCount) { let totalTokens = 0; const active = this.cache.filter((r) => r.status === "active"); for (const record of active) { totalTokens += this.calculateTokenCount(record.message); } if (totalTokens <= targetTokenCount) return; for (const record of active) { if (totalTokens <= targetTokenCount) break; if (record.message.role === "system") continue; record.status = "archived"; totalTokens -= this.calculateTokenCount(record.message); const write = this.kernel.updateEntity(record.entityId, { status: "archived" }).catch((err) => { console.error(`GraphContextManager: Failed to archive message ${record.entityId}:`, err); }).then(() => void 0); this.trackWrite(write); } } async summarize() { const archived = this.cache.filter((r) => r.status === "archived"); if (archived.length === 0) return ""; return `[${archived.length} earlier messages archived]`; } async injectRagContext(query, limit) { } calculateTokenCount(message) { return (message.content?.length ?? 0) / 4; } // ------------------------------------------------------------------------- // Graph queries // ------------------------------------------------------------------------- /** * List all conversations, optionally filtered by status. */ listConversations(status) { const entities = this.kernel.listEntities("Conversation", status ? { status } : void 0); return entities.map((e) => { const get = (a) => e.facts.find((f) => f.a === a)?.v; const messageLinks = this.kernel.getStore().getLinksByEntityAndAttribute(e.id, "hasMessage"); return { id: e.id, title: String(get("title") ?? "Untitled"), status: String(get("status") ?? "active"), createdAt: String(get("createdAt") ?? ""), messageCount: messageLinks.length }; }); } /** * Get the total message count for the active conversation. */ getMessageCount() { return this.cache.filter((r) => r.status === "active").length; } /** * Get the total estimated token count for the active conversation. */ getTotalTokenCount() { return this.cache.filter((r) => r.status === "active").reduce((sum, r) => sum + this.calculateTokenCount(r.message), 0); } /** Wait for in-flight graph writes (tests and graceful shutdown). */ async awaitPersistence() { while (this.pendingWrites.size > 0) { await Promise.all([...this.pendingWrites]); } } // ------------------------------------------------------------------------- // Internal // ------------------------------------------------------------------------- trackWrite(write) { this.pendingWrites.add(write); write.finally(() => { this.pendingWrites.delete(write); }); } /** * Load messages from the graph for a given conversation, sorted by timestamp. */ _loadMessagesFromGraph(conversationId) { const store = this.kernel.getStore(); const messageLinks = store.getLinksByEntityAndAttribute(conversationId, "hasMessage"); const records = []; for (const link of messageLinks) { const entity = this.kernel.getEntity(link.e2); if (!entity || entity.type !== "Message") continue; const get = (a) => entity.facts.find((f) => f.a === a)?.v; const message = { role: get("role"), content: get("content") ?? null }; const name = get("name"); if (name) message.name = name; const toolCallId = get("toolCallId"); if (toolCallId) message.tool_call_id = toolCallId; records.push({ entityId: entity.id, message, timestamp: String(get("timestamp") ?? ""), sequence: Number(get("sequence") ?? 0), status: get("status") ?? "active" }); } records.sort((a, b) => { const seqA = a.sequence || 0; const seqB = b.sequence || 0; if (seqA !== seqB) return seqA - seqB; return a.timestamp.localeCompare(b.timestamp); }); return records; } }; export { GraphContextManager, agentMemoryOntology, createAgentMemoryPlugin };