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