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