UNPKG

trellis

Version:

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

358 lines (353 loc) 10.7 kB
import { decisionEntityId, init_types } from "./chunk-E2CFJKLU.js"; import { createVcsOp, init_ops } from "./chunk-GRWQPKYK.js"; import { __esm } from "./chunk-2ESYSVXG.js"; // src/decisions/hooks.ts function matchesPattern(pattern, toolName) { if (pattern instanceof RegExp) { return pattern.test(toolName); } if (pattern === "*") return true; if (!pattern.includes("*")) return pattern === toolName; const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*"); return new RegExp(`^${escaped}$`).test(toolName); } var HookRegistry; var init_hooks = __esm({ "src/decisions/hooks.ts"() { "use strict"; HookRegistry = class { preHooks = []; postHooks = []; /** * Register a pre-hook that runs before a tool handler. */ registerPreHook(hook) { this.preHooks.push(hook); } /** * Register a post-hook that runs after a tool handler. */ registerPostHook(hook) { this.postHooks.push(hook); } /** * Remove a pre-hook by name. */ removePreHook(name) { this.preHooks = this.preHooks.filter((h) => h.name !== name); } /** * Remove a post-hook by name. */ removePostHook(name) { this.postHooks = this.postHooks.filter((h) => h.name !== name); } /** * Get all pre-hooks matching a tool name. */ getPreHooks(toolName) { return this.preHooks.filter((h) => matchesPattern(h.toolPattern, toolName)); } /** * Get all post-hooks matching a tool name. */ getPostHooks(toolName) { return this.postHooks.filter( (h) => matchesPattern(h.toolPattern, toolName) ); } /** * Run all matching pre-hooks and merge their contexts. */ async runPreHooks(toolName, input) { const hooks = this.getPreHooks(toolName); const merged = {}; for (const hook of hooks) { try { const ctx = await hook.handler(toolName, input); Object.assign(merged, ctx); if (ctx.custom) { merged.custom = { ...merged.custom, ...ctx.custom }; } } catch { } } return merged; } /** * Run all matching post-hooks and merge their enrichments. */ async runPostHooks(toolName, input, output, preContext) { const hooks = this.getPostHooks(toolName); const merged = {}; for (const hook of hooks) { try { const enrichment = await hook.handler( toolName, input, output, preContext ); if (enrichment.rationale) merged.rationale = enrichment.rationale; if (enrichment.alternatives) merged.alternatives = enrichment.alternatives; if (enrichment.confidence !== void 0) merged.confidence = enrichment.confidence; if (enrichment.relatedEntities) { merged.relatedEntities = [ ...merged.relatedEntities ?? [], ...enrichment.relatedEntities ]; } if (enrichment.custom) { merged.custom = { ...merged.custom, ...enrichment.custom }; } } catch { } } return merged; } /** * Clear all hooks. */ clear() { this.preHooks = []; this.postHooks = []; } }; } }); // src/decisions/auto-capture.ts function wrapToolHandler(toolName, handler, opts) { return async (params) => { if (opts.exclude?.has(toolName)) { return handler(params); } const preContext = await opts.hooks.runPreHooks( toolName, params ); const result = await handler(params); const enrichment = await opts.hooks.runPostHooks( toolName, params, result, preContext ); const decision = { toolName, input: sanitizeInput(params), outputSummary: summarize(result), context: preContext.prompt ?? preContext.conversationId, rationale: enrichment.rationale, alternatives: enrichment.alternatives, confidence: enrichment.confidence, relatedEntities: enrichment.relatedEntities, custom: { ...preContext.custom, ...enrichment.custom, agentModel: preContext.agentModel } }; opts.recorder(decision).catch(() => { }); return result; }; } function sanitizeInput(params) { const sanitized = {}; for (const [key, value] of Object.entries(params)) { if (typeof value === "string" && value.length > 2e3) { sanitized[key] = value.slice(0, 2e3) + "\u2026"; } else { sanitized[key] = value; } } return sanitized; } function summarize(result) { if (result === null || result === void 0) return ""; if (typeof result === "object" && result !== null && "content" in result) { const content = result.content; if (Array.isArray(content)) { const texts = content.filter((c) => c.type === "text").map((c) => c.text).join("\n"); return texts.length > 500 ? texts.slice(0, 500) + "\u2026" : texts; } } const str = typeof result === "string" ? result : JSON.stringify(result, null, 0); return str.length > 500 ? str.slice(0, 500) + "\u2026" : str; } var init_auto_capture = __esm({ "src/decisions/auto-capture.ts"() { "use strict"; } }); // src/decisions/index.ts import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"; import { join, dirname } from "path"; function getDecisionCounterPath(rootPath) { return join(rootPath, ".trellis", "decision-counter.json"); } function nextDecisionId(rootPath) { const counterPath = getDecisionCounterPath(rootPath); let counter = 0; if (existsSync(counterPath)) { try { counter = JSON.parse(readFileSync(counterPath, "utf-8")).counter ?? 0; } catch { } } counter++; const dir = dirname(counterPath); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); writeFileSync(counterPath, JSON.stringify({ counter }, null, 2)); return `DEC-${counter}`; } async function recordDecision(ctx, rootPath, input) { const id = nextDecisionId(rootPath); const op = await createVcsOp("vcs:decisionRecord", { agentId: ctx.agentId, previousHash: ctx.getLastOp()?.hash, vcs: { decisionId: id, decisionToolName: input.toolName, decisionToolInput: input.input ? JSON.stringify(input.input) : void 0, decisionToolOutput: input.outputSummary, decisionContext: input.context, decisionRationale: input.rationale, decisionAlternatives: input.alternatives ? JSON.stringify(input.alternatives) : void 0 } }); await ctx.applyOp(op); if (input.relatedEntities) { const eid = decisionEntityId(id); const links = input.relatedEntities.map((target) => ({ e1: eid, a: "relatedTo", e2: target })); if (links.length > 0) { ctx.store.addLinks(links); } } return op; } function buildDecision(ctx, entityId) { const facts = ctx.store.getFactsByEntity(entityId); const get = (a) => { const matches = facts.filter((f) => f.a === a); return matches.length > 0 ? matches[matches.length - 1].v : void 0; }; const links = ctx.store.getLinksByAttribute("relatedTo"); const related = links.filter((l) => l.e1 === entityId).map((l) => l.e2); const bareId = entityId.replace(/^decision:/, ""); const alternativesRaw = get("alternatives"); let alternatives; if (alternativesRaw) { try { alternatives = JSON.parse(alternativesRaw); } catch { alternatives = [alternativesRaw]; } } const confidenceRaw = get("confidence"); const confidence = confidenceRaw !== void 0 ? parseFloat(confidenceRaw) : void 0; return { id: bareId, toolName: get("toolName") ?? "", outputSummary: get("outputSummary"), context: get("context"), rationale: get("rationale"), alternatives, confidence, createdAt: get("createdAt"), createdBy: get("createdBy"), relatedEntities: related }; } function queryDecisions(ctx, filter) { const decisionFacts = ctx.store.getFactsByAttribute("type").filter((f) => f.v === "Decision"); let decisions = decisionFacts.map((f) => buildDecision(ctx, f.e)); if (filter?.toolPattern) { const pattern = filter.toolPattern; const regex = pattern.includes("*") ? new RegExp( `^${pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$` ) : null; decisions = decisions.filter( (d) => regex ? regex.test(d.toolName) : d.toolName === pattern ); } if (filter?.agentId) { decisions = decisions.filter((d) => d.createdBy === filter.agentId); } if (filter?.since) { const since = new Date(filter.since).getTime(); decisions = decisions.filter( (d) => d.createdAt && new Date(d.createdAt).getTime() >= since ); } if (filter?.until) { const until = new Date(filter.until).getTime(); decisions = decisions.filter( (d) => d.createdAt && new Date(d.createdAt).getTime() <= until ); } if (filter?.entityId) { decisions = decisions.filter( (d) => d.relatedEntities.includes(filter.entityId) ); } decisions.sort((a, b) => { if (!a.createdAt || !b.createdAt) return 0; return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); }); if (filter?.limit && filter.limit > 0) { decisions = decisions.slice(0, filter.limit); } return decisions; } function getDecisionChain(ctx, entityId) { const allLinks = ctx.store.getLinksByAttribute("relatedTo"); const decisionEids = new Set( allLinks.filter((l) => l.e2 === entityId).map((l) => l.e1).filter((e) => e.startsWith("decision:")) ); const decisions = Array.from(decisionEids).map( (eid) => buildDecision(ctx, eid) ); decisions.sort((a, b) => { if (!a.createdAt || !b.createdAt) return 0; return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); }); return decisions; } function getDecision(ctx, id) { const eid = decisionEntityId(id); const typeFact = ctx.store.getFactsByEntity(eid).find((f) => f.a === "type" && f.v === "Decision"); if (!typeFact) return null; return buildDecision(ctx, eid); } var init_decisions = __esm({ "src/decisions/index.ts"() { init_hooks(); init_auto_capture(); init_ops(); init_types(); } }); export { HookRegistry, wrapToolHandler, recordDecision, queryDecisions, getDecisionChain, getDecision, init_decisions };