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