UNPKG

trellis

Version:

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

172 lines (169 loc) 6.26 kB
import { PROVENANCE, init_canonical_op } from "../../chunk-RUMOVKR4.js"; import "../../chunk-2ESYSVXG.js"; // src/plugins/idea-garden/api.ts init_canonical_op(); var AGENT_CTX = { provenance: PROVENANCE.agent }; var IdeaGarden = class { constructor(kernel) { this.kernel = kernel; } /** * Harvests all recoverable ideas from the graph. * Scans for: * 1. Rejected/Cancelled PendingPlans * 2. Archived Conversations * 3. DecisionTraces with unexplored alternatives */ harvestIdeas() { const ideas = []; const plans = this.kernel.listEntities("PendingPlan"); for (const plan of plans) { const status = plan.facts.find((f) => f.a === "status")?.v; if (status === "rejected" || status === "cancelled") { const title = plan.facts.find((f) => f.a === "title")?.v; let opsPayload = []; try { const rawOps = plan.facts.find((f) => f.a === "operations")?.v; if (rawOps) opsPayload = JSON.parse(rawOps); } catch (e) { } ideas.push({ id: `idea:rejected_plan:${plan.id}`, sourceType: "rejected_plan", sourceEntityId: plan.id, title: title || "Untitled Plan", description: `A plan that was ${status} during approval.`, createdAt: plan.facts.find((f) => f.a === "createdAt")?.v || (/* @__PURE__ */ new Date()).toISOString(), payload: { operations: opsPayload } }); } } const convos = this.kernel.listEntities("Conversation"); for (const convo of convos) { const status = convo.facts.find((f) => f.a === "status")?.v; if (status === "archived") { const title = convo.facts.find((f) => f.a === "title")?.v; ideas.push({ id: `idea:archived_conversation:${convo.id}`, sourceType: "archived_conversation", sourceEntityId: convo.id, title: title || "Untitled Conversation", description: "An archived thread that might contain unexplored ideas.", createdAt: convo.facts.find((f) => f.a === "createdAt")?.v || (/* @__PURE__ */ new Date()).toISOString() }); } } const traces = this.kernel.listEntities("DecisionTrace"); for (const trace of traces) { const altsRaw = trace.facts.find((f) => f.a === "alternatives")?.v; if (!altsRaw) continue; let alts = []; try { alts = JSON.parse(altsRaw); } catch (e) { continue; } if (alts.length > 0) { const toolName = trace.facts.find((f) => f.a === "toolName")?.v || "Unknown Tool"; const rationale = trace.facts.find((f) => f.a === "rationale")?.v || "No rationale provided"; ideas.push({ id: `idea:unexplored_alternative:${trace.id}`, sourceType: "unexplored_alternative", sourceEntityId: trace.id, title: `Alternatives for ${toolName}`, description: `The agent chose one path because: "${rationale}". There were ${alts.length} alternative(s) not pursued.`, createdAt: trace.facts.find((f) => f.a === "timestamp")?.v || (/* @__PURE__ */ new Date()).toISOString(), payload: { alternatives: alts } }); } } return ideas.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); } /** * Resurrects a rejected plan by creating a new active 'pending' replica of it. * Returns the new PendingPlan ID. */ async resurrectPlan(ideaId) { const idea = this.harvestIdeas().find((i) => i.id === ideaId); if (!idea || idea.sourceType !== "rejected_plan") { throw new Error(`Cannot resurrect plan: Invalid Idea ID ${ideaId}`); } const oldPlan = this.kernel.listEntities("PendingPlan").find((p) => p.id === idea.sourceEntityId); if (!oldPlan) throw new Error("Original plan not found"); const newId = `plan:${Date.now()}`; const title = oldPlan.facts.find((f) => f.a === "title")?.v || "Resurrected Plan"; const ops = oldPlan.facts.find((f) => f.a === "operations")?.v; await this.kernel.createEntity(newId, "PendingPlan", { title: `${title} (Resurrected)`, status: "pending", operations: ops || "[]", createdAt: (/* @__PURE__ */ new Date()).toISOString() }, void 0, AGENT_CTX); return newId; } }; // src/plugins/idea-garden/plugin.ts function createIdeaGardenPlugin(kernel, harness) { const api = new IdeaGarden(kernel); return { id: "trellis:idea-garden", name: "Idea Garden", version: "1.0.0", description: "Surfaces abandoned threads, cancelled plans, and unexplored alternatives as recoverable ideas.", api, onLoad: async (ctx) => { ctx.log("Idea Garden loaded"); await harness.registerTool( { id: "harvestIdeas", name: "harvestIdeas", description: "Search for recoverable ideas like rejected plans, archived threads, or alternate agent paths.", schema: JSON.stringify({ type: "object", properties: {} }) }, async () => { const ideas = api.harvestIdeas(); return { success: true, output: ideas }; } ); await harness.registerTool( { id: "resurrectPlan", name: "resurrectPlan", description: "Recover a rejected plan by creating a new active exact copy.", schema: JSON.stringify({ type: "object", properties: { ideaId: { type: "string", description: "The unique ID of the idea from harvestIdeas (e.g. idea:rejected_plan:plan:1)" } }, required: ["ideaId"] }) }, async (input) => { const { ideaId } = input; try { const newPlanId = await api.resurrectPlan(ideaId); return { success: true, output: `Successfully resurrected plan. New PendingPlan ID: ${newPlanId}` }; } catch (err) { return { success: false, error: err.message, output: null }; } } ); }, onUnload: async (ctx) => { ctx.log("Idea Garden unloaded"); } }; } export { IdeaGarden, createIdeaGardenPlugin };