EXTENSION for treeos-intelligence
intent
The tree acts without being asked. Right now every interaction starts with a human. You type. The AI responds. You navigate. The AI resolves. Even cascade is triggered by you writing a note. Remove the human and the tree sits there. Dormant. Waiting. A real tree doesn't wait for someone to tell it to grow toward sunlight. It just grows. Intent gives the digital tree the same property. A background job runs on a configurable interval. It reads the state of the tree from every source available through enrichContext. Pulse says the failure rate is climbing. Evolution says a branch went dormant 45 days ago. Contradiction detected two conflicting targets three days ago and nobody resolved them. The codebook between two nodes hasn't compressed in 200 interactions. A cascade signal arrived carrying extension metadata this land doesn't have. Intent synthesizes all of that into a queue of actions the tree should take on its own. Each intent becomes a real AI interaction. The job calls runChat at the target node with the intent as the message. The AI activates in whatever mode is configured at that position. It has all the tools available at that node. It does the work. Creates notes. Fires cascade signals. Resolves contradictions. Compresses codebooks. Prunes dormant branches. Alerts the operator through gateway channels. The intent queue lives on a .intent system node under the tree root. Each processed intent writes its result as a note. The user wakes up and checks .intent to see what the tree did overnight. Or they don't check. The tree handled it. What makes this different from a cron job: cron runs the same action on a schedule. Intent generates novel actions from observed state. Two trees with identical schedules would generate completely different intents because their states are different. The intent generation itself goes through the AI. A prompt receives the full state summary and asks: what should this tree do next that nobody has asked for? Safety: intent respects spatial scoping, ownership (metadata.intent.enabled must be true on the tree root), energy budgets (intentMaxTokensPerCycle), and never deletes. It can create, write, compress, alert. Destructive actions require a human. Every processed intent is logged as a contribution with action: intent:executed. Full audit trail. The user can talk back. Navigate to .intent. Chat: stop nudging me about running. The contradiction extension marks that as an intentional gap. The inverse-tree records the correction. Intent stops generating that nudge. The tree learned. Dependencies are all optional. Each one that's installed adds signal to the intent generation. Without any of them, intent has nothing to observe and generates nothing. With all of them, the tree is fully autonomous between human interactions. You planted the seed. The seed grew a tree. The tree learned to think. And now the tree learned to want.
v1.0.1 by TreeOS Site 0 downloads 5 files 926 lines 30.6 KB published 38d ago
treeos ext install intent
View changelog

Manifest

Provides

  • jobs
  • 1 CLI commands
  • 2 energy actions

Requires

  • services: llm, hooks, contributions, session, chat, orchestrator
  • models: Node, User

Optional

  • services: energy
  • extensions: pulse, evolution, contradiction, codebook, gap-detection, inverse-tree, long-memory, treeos-cascade, gateway
SHA256: 2e7319b95ffcaaf89020fc4f0ca8161c665528a8b96c7625497dc05159c5838a

Dependents

1 package depend on this

PackageTypeRelationship
treeos-intelligence v1.0.2bundleincludes

CLI Commands

CommandMethodDescription
intentGETAutonomous intent queue and recent executions. Actions: pause, resume, history, reject.
intent pausePOSTPause autonomous behavior
intent resumePOSTResume autonomous behavior
intent historyGETWhat the tree did on its own
intent rejectPOSTTell the tree not to do that again

Hooks

Listens To

  • afterBoot
  • enrichContext

Source Code

1import log from "../../seed/log.js";
2import { setServices, startIntentJob, stopIntentJob } from "./intentJob.js";
3export async function init(core) {
4  core.llm.registerRootLlmSlot("intent");
5
6  setServices({
7    models: core.models,
8    contributions: core.contributions,
9    energy: core.energy || null,
10    metadata: core.metadata,
11  });
12
13  const { setMetadata: setCollectorMetadata } = await import("./stateCollector.js");
14  setCollectorMetadata(core.metadata);
15
16  // enrichContext: surface intent data so the AI knows what the tree did autonomously
17  core.hooks.register("enrichContext", async ({ context, node, meta }) => {
18    const intentMeta = meta?.intent;
19    if (!intentMeta) return;
20
21    const injected = {};
22
23    // Show recent executions so the AI knows what the tree did autonomously
24    if (intentMeta.recentExecutions?.length > 0) {
25      injected.recentIntents = intentMeta.recentExecutions.slice(0, 5).map(e => ({
26        action: e.action,
27        reason: e.reason,
28        result: e.result,
29        executedAt: e.executedAt,
30      }));
31    }
32
33    // Show pending queue so the AI knows what's coming
34    if (intentMeta.queue?.length > 0) {
35      injected.pendingIntents = intentMeta.queue.length;
36    }
37
38    // Show rejected intents so the AI doesn't suggest what the user already rejected
39    if (intentMeta.rejected?.length > 0) {
40      injected.rejectedIntents = intentMeta.rejected.map(r => r.action || r.pattern || r.description).filter(Boolean);
41    }
42
43    if (Object.keys(injected).length > 0) {
44      context.intent = injected;
45    }
46  }, "intent");
47
48  const { default: router, setModels, setMetadata: setRouteMetadata } = await import("./routes.js");
49  setModels(core.models);
50  setRouteMetadata(core.metadata);
51
52  log.info("Intent", "Autonomous intent engine loaded");
53
54  return {
55    router,
56    jobs: [
57      {
58        name: "intent-cycle",
59        start: () => startIntentJob(),
60        stop: () => stopIntentJob(),
61      },
62    ],
63  };
64}
65
1// Intent Job
2//
3// The background job that makes the tree autonomous. Runs on a configurable
4// interval. For each opted-in tree, collects state, generates intents
5// through the AI, then executes each intent as a real AI interaction
6// at the target node.
7//
8// Uses OrchestratorRuntime for session lifecycle, lock management,
9// abort support, and step tracking. Lock prevents two intent cycles
10// from racing on the same tree.
11//
12// The intent cycle:
13// 1. Find trees with metadata.intent.enabled = true
14// 2. For each tree, collect state from all installed signal sources
15// 3. Send the state to the AI via rt.runStep with the intent generation prompt
16// 4. Parse the returned intents (JSON array)
17// 5. For each intent, rt.runStep at the target node
18// 6. Log each execution as a contribution (action: "intent:executed")
19// 7. Write results to .intent node as notes
20
21import log from "../../seed/log.js";
22import { collectTreeState, formatStateForPrompt } from "./stateCollector.js";
23import { parseJsonSafe } from "../../seed/orchestrators/helpers.js";
24import { OrchestratorRuntime } from "../../seed/orchestrators/runtime.js";
25import { getLandConfigValue } from "../../seed/landConfig.js";
26
27let LLM_PRIORITY;
28try {
29  ({ LLM_PRIORITY } = await import("../../seed/llm/conversation.js"));
30} catch {
31  LLM_PRIORITY = { BACKGROUND: 4 };
32}
33
34let Node = null;
35let User = null;
36let logContribution = null;
37let useEnergy = async () => ({ energyUsed: 0 });
38let _metadata = null;
39
40export function setServices({ models, contributions, energy, metadata }) {
41  Node = models.Node;
42  User = models.User;
43  logContribution = contributions.logContribution;
44  if (energy?.useEnergy) useEnergy = energy.useEnergy;
45  if (metadata) _metadata = metadata;
46}
47
48let _timer = null;
49let _running = false;
50
51function getIntervalMs() {
52  return Number(getLandConfigValue("intentIntervalMs")) || 30 * 60 * 1000; // 30 min default
53}
54
55function getMaxIntentsPerCycle() {
56  return Number(getLandConfigValue("intentMaxPerCycle")) || 5;
57}
58
59export function startIntentJob() {
60  if (_timer) return;
61  const interval = getIntervalMs();
62  _timer = setInterval(runCycle, interval);
63  if (_timer.unref) _timer.unref();
64  log.info("Intent", `Intent job started (checking every ${Math.round(interval / 60000)}m)`);
65}
66
67export function stopIntentJob() {
68  if (_timer) {
69    clearInterval(_timer);
70    _timer = null;
71  }
72}
73
74// ─────────────────────────────────────────────────────────────────────────
75// INTENT GENERATION PROMPT
76// ─────────────────────────────────────────────────────────────────────────
77
78const GENERATION_PROMPT = `You are the intent engine for this tree. You observe its health, patterns, contradictions, gaps, and user behavior. Generate actions the tree should take on its own.
79
80Rules:
81- Only generate intents justified by the data below
82- Never generate intents matching the rejected patterns listed
83- Priority: health > contradictions > maintenance > nudges
84- Max {maxIntents} intents per cycle
85- Each intent must name a specific nodeId and specific tools to use
86- Never generate delete actions. The tree can create, write, compress, alert. It cannot delete.
87- Do not invent work. If the state looks healthy with no issues, return an empty array.
88
89Return a JSON array of intents. Each intent:
90{
91  "action": "short description of what to do",
92  "reason": "why this is justified by the current state",
93  "targetNodeId": "the node ID to act on",
94  "priority": "high" | "medium" | "low",
95  "tools": ["tool-names", "to-use"],
96  "mode": "tree:respond" (or whichever mode is appropriate)
97}
98
99If nothing needs attention, return: []
100
101Current tree state:
102- {stateText}`;
103
104// ─────────────────────────────────────────────────────────────────────────
105// CYCLE
106// ─────────────────────────────────────────────────────────────────────────
107
108async function runCycle() {
109  if (_running) return; // prevent overlap
110  _running = true;
111
112  try {
113    // Find all trees opted in for autonomous intent
114    const roots = await Node.find({
115      rootOwner: { $nin: [null, "SYSTEM"] },
116      "metadata.intent.enabled": true,
117    }).select("_id name rootOwner metadata").lean();
118
119    if (roots.length === 0) {
120      _running = false;
121      return;
122    }
123
124    log.verbose("Intent", `Intent cycle: ${roots.length} tree(s) opted in`);
125
126    for (const root of roots) {
127      try {
128        // Check if paused
129        const intentMeta = _metadata.getExtMeta(root, "intent");
130        if (intentMeta.paused) continue;
131
132        await processTree(root);
133      } catch (err) {
134        log.warn("Intent", `Intent cycle failed for tree ${root.name} (${root._id}): ${err.message}`);
135      }
136    }
137  } catch (err) {
138    log.error("Intent", `Intent cycle error: ${err.message}`);
139  } finally {
140    _running = false;
141  }
142}
143
144async function processTree(root) {
145  const rootId = root._id.toString();
146  const userId = root.rootOwner?.toString();
147  if (!userId) return;
148
149  const user = await User.findById(userId).select("username").lean();
150  if (!user) return;
151
152  // Create runtime. Intent is a standalone background pipeline.
153  const rt = new OrchestratorRuntime({
154    rootId,
155    userId,
156    username: user.username,
157    visitorId: `intent:${rootId}:${Date.now()}`,
158    sessionType: "INTENT",
159    description: `Intent cycle for tree ${root.name}`,
160    modeKeyForLlm: "tree:respond",
161    slot: "intent",
162    lockNamespace: "intent",
163    lockKey: `intent:${rootId}`,
164    llmPriority: LLM_PRIORITY?.BACKGROUND || 4,
165  });
166
167  const ok = await rt.init("Starting intent cycle");
168  if (!ok) {
169    log.debug("Intent", `Intent cycle already running for ${root.name}. Skipping.`);
170    return;
171  }
172
173  try {
174    // 1. Collect state
175    const state = await collectTreeState(rootId, root, { Node });
176    const stateText = formatStateForPrompt(state);
177
178    if (!stateText) {
179      log.debug("Intent", `No observable state for ${root.name}. Skipping.`);
180      rt.setResult("No observable state", "tree:respond");
181      return;
182    }
183
184    rt.trackStep("tree:respond", {
185      input: { phase: "state-collection", rootId },
186      output: { sectionsCollected: stateText.split("\n").length },
187      startTime: Date.now(),
188      endTime: Date.now(),
189    });
190
191    if (rt.aborted) {
192      rt.setError("Intent cycle cancelled", "tree:respond");
193      return;
194    }
195
196    // 2. Energy check for generation
197    try {
198      await useEnergy({ userId, action: "intentGenerate" });
199    } catch {
200      log.debug("Intent", `Insufficient energy for intent generation on ${root.name}`);
201      rt.setResult("Insufficient energy", "tree:respond");
202      return;
203    }
204
205    // 3. Generate intents via AI (through runStep)
206    const maxIntents = getMaxIntentsPerCycle();
207    const prompt = GENERATION_PROMPT
208      .replace("{maxIntents}", String(maxIntents))
209      .replace("{stateText}", stateText);
210
211    let parsed = null;
212    try {
213      const result = await rt.runStep("tree:respond", {
214        prompt,
215      });
216      parsed = result?.parsed;
217    } catch (err) {
218      log.warn("Intent", `Intent generation LLM call failed for ${root.name}: ${err.message}`);
219      rt.setError(err.message, "tree:respond");
220      return;
221    }
222
223    if (!Array.isArray(parsed)) {
224      log.debug("Intent", `Intent generation returned non-array for ${root.name}`);
225      rt.setResult("No intents generated (non-array response)", "tree:respond");
226      return;
227    }
228
229    const intents = parsed
230      .filter(i => i && typeof i === "object" && i.action && i.targetNodeId)
231      .slice(0, maxIntents);
232
233    if (intents.length === 0) {
234      log.debug("Intent", `No intents generated for ${root.name}. Tree is healthy.`);
235      rt.setResult("No intents needed", "tree:respond");
236      return;
237    }
238
239    log.verbose("Intent", `Generated ${intents.length} intent(s) for ${root.name}`);
240
241    // 4. Ensure .intent node exists
242    const intentNodeId = await ensureIntentNode(rootId);
243
244    // 5. Execute each intent (each is a runStep at the target node)
245    const recentExecutions = [];
246
247    for (const intent of intents) {
248      if (rt.aborted) {
249        log.debug("Intent", "Intent cycle aborted between executions");
250        break;
251      }
252
253      try {
254        const execResult = await executeIntent(rt, intent, rootId, userId, user.username, intentNodeId);
255        recentExecutions.push({
256          action: intent.action,
257          reason: intent.reason,
258          result: execResult?.slice(0, 200) || "completed",
259          executedAt: new Date().toISOString(),
260        });
261      } catch (err) {
262        log.warn("Intent", `Intent execution failed: ${intent.action}: ${err.message}`);
263        await writeIntentResult(intentNodeId, intent, null, err.message);
264      }
265    }
266
267    // 6. Write recent executions to metadata for enrichContext
268    try {
269      const rootNode = await Node.findById(rootId);
270      if (rootNode) {
271        const meta = _metadata.getExtMeta(rootNode, "intent") || {};
272        meta.recentExecutions = [
273          ...recentExecutions,
274          ...(meta.recentExecutions || []),
275        ].slice(0, 20);
276        await _metadata.setExtMeta(rootNode, "intent", meta);
277      }
278    } catch (err) {
279      log.debug("Intent", `Failed to write recent executions to metadata: ${err.message}`);
280    }
281
282    rt.setResult(`Executed ${recentExecutions.length} intent(s)`, "tree:respond");
283  } catch (err) {
284    rt.setError(err.message, "tree:respond");
285    throw err;
286  } finally {
287    await rt.cleanup();
288  }
289}
290
291// ─────────────────────────────────────────────────────────────────────────
292// EXECUTION
293// ─────────────────────────────────────────────────────────────────────────
294
295async function executeIntent(rt, intent, rootId, userId, username, intentNodeId) {
296  // Energy check per execution
297  try {
298    await useEnergy({ userId, action: "intentExecute" });
299  } catch {
300    log.debug("Intent", `Insufficient energy for intent execution: ${intent.action}`);
301    return null;
302  }
303
304  // Verify target node exists and is in this tree
305  const targetNode = await Node.findById(intent.targetNodeId).select("_id name rootOwner").lean();
306  if (!targetNode) {
307    log.debug("Intent", `Intent target node not found: ${intent.targetNodeId}`);
308    return null;
309  }
310
311  // Build the intent message. This becomes the AI's instruction.
312  const message =
313    `[Autonomous intent, priority: ${intent.priority || "medium"}] ` +
314    `${intent.action}. ` +
315    `Reason: ${intent.reason || "observed state change"}. ` +
316    `Use tools: ${(intent.tools || []).join(", ") || "as needed"}.`;
317
318  log.verbose("Intent", `Executing: "${intent.action}" at node ${targetNode.name || intent.targetNodeId}`);
319
320  const mode = intent.mode || "tree:respond";
321
322  // Execute through the runtime's runStep
323  let resultText = null;
324  try {
325    const { raw } = await rt.runStep(mode, {
326      prompt: message,
327      treeContext: { nodeId: intent.targetNodeId },
328    });
329    resultText = raw || "completed";
330  } catch (err) {
331    await writeIntentResult(intentNodeId, intent, null, err.message);
332    throw err;
333  }
334
335  // Log as contribution
336  await logContribution({
337    userId,
338    nodeId: intent.targetNodeId,
339    wasAi: true,
340    action: "intent:executed",
341    extensionData: {
342      intent: {
343        action: intent.action,
344        reason: intent.reason,
345        priority: intent.priority,
346        targetNodeId: intent.targetNodeId,
347        tools: intent.tools,
348        result: resultText?.slice(0, 500) || null,
349      },
350    },
351  });
352
353  // Write result to .intent node
354  await writeIntentResult(intentNodeId, intent, resultText, null);
355  return resultText;
356}
357
358// ─────────────────────────────────────────────────────────────────────────
359// .INTENT NODE
360// ─────────────────────────────────────────────────────────────────────────
361
362async function ensureIntentNode(rootId) {
363  // Find or create the .intent node under the tree root
364  let intentNode = await Node.findOne({
365    parent: rootId,
366    name: ".intent",
367  }).select("_id").lean();
368
369  if (!intentNode) {
370    const { createSystemNode } = await import("../../seed/tree/treeManagement.js");
371    intentNode = await createSystemNode({ name: ".intent", parentId: rootId });
372    log.verbose("Intent", `Created .intent node for tree ${rootId}`);
373  }
374
375  return intentNode._id.toString();
376}
377
378async function writeIntentResult(intentNodeId, intent, result, error) {
379  try {
380    const { createNote } = await import("../../seed/tree/notes.js");
381    const content = error
382      ? `[FAILED] ${intent.action}\nReason: ${intent.reason}\nError: ${error}`
383      : `[${intent.priority || "medium"}] ${intent.action}\nReason: ${intent.reason}\nResult: ${(result || "completed").slice(0, 2000)}`;
384
385    await createNote({
386      nodeId: intentNodeId,
387      content,
388      contentType: "text",
389      userId: "SYSTEM",
390    });
391  } catch (err) {
392    log.debug("Intent", `Failed to write intent result note: ${err.message}`);
393  }
394}
395
1export default {
2  name: "intent",
3  version: "1.0.1",
4  builtFor: "treeos-intelligence",
5  description:
6    "The tree acts without being asked. " +
7    "\n\n" +
8    "Right now every interaction starts with a human. You type. The AI responds. " +
9    "You navigate. The AI resolves. Even cascade is triggered by you writing a note. " +
10    "Remove the human and the tree sits there. Dormant. Waiting. A real tree doesn't " +
11    "wait for someone to tell it to grow toward sunlight. It just grows. " +
12    "\n\n" +
13    "Intent gives the digital tree the same property. " +
14    "\n\n" +
15    "A background job runs on a configurable interval. It reads the state of the tree " +
16    "from every source available through enrichContext. Pulse says the failure rate is " +
17    "climbing. Evolution says a branch went dormant 45 days ago. Contradiction detected " +
18    "two conflicting targets three days ago and nobody resolved them. The codebook between " +
19    "two nodes hasn't compressed in 200 interactions. A cascade signal arrived carrying " +
20    "extension metadata this land doesn't have. " +
21    "\n\n" +
22    "Intent synthesizes all of that into a queue of actions the tree should take on its own. " +
23    "Each intent becomes a real AI interaction. The job calls runChat at the target node with " +
24    "the intent as the message. The AI activates in whatever mode is configured at that position. " +
25    "It has all the tools available at that node. It does the work. Creates notes. Fires cascade " +
26    "signals. Resolves contradictions. Compresses codebooks. Prunes dormant branches. Alerts the " +
27    "operator through gateway channels. " +
28    "\n\n" +
29    "The intent queue lives on a .intent system node under the tree root. Each processed intent " +
30    "writes its result as a note. The user wakes up and checks .intent to see what the tree did " +
31    "overnight. Or they don't check. The tree handled it. " +
32    "\n\n" +
33    "What makes this different from a cron job: cron runs the same action on a schedule. Intent " +
34    "generates novel actions from observed state. Two trees with identical schedules would generate " +
35    "completely different intents because their states are different. The intent generation itself " +
36    "goes through the AI. A prompt receives the full state summary and asks: what should this tree " +
37    "do next that nobody has asked for? " +
38    "\n\n" +
39    "Safety: intent respects spatial scoping, ownership (metadata.intent.enabled must be true on " +
40    "the tree root), energy budgets (intentMaxTokensPerCycle), and never deletes. It can create, " +
41    "write, compress, alert. Destructive actions require a human. Every processed intent is logged " +
42    "as a contribution with action: intent:executed. Full audit trail. " +
43    "\n\n" +
44    "The user can talk back. Navigate to .intent. Chat: stop nudging me about running. The " +
45    "contradiction extension marks that as an intentional gap. The inverse-tree records the " +
46    "correction. Intent stops generating that nudge. The tree learned. " +
47    "\n\n" +
48    "Dependencies are all optional. Each one that's installed adds signal to the intent generation. " +
49    "Without any of them, intent has nothing to observe and generates nothing. With all of them, " +
50    "the tree is fully autonomous between human interactions. " +
51    "\n\n" +
52    "You planted the seed. The seed grew a tree. The tree learned to think. And now the tree " +
53    "learned to want.",
54
55  needs: {
56    services: ["llm", "hooks", "contributions", "session", "chat", "orchestrator"],
57    models: ["Node", "User"],
58  },
59
60  optional: {
61    services: ["energy"],
62    extensions: [
63      "pulse",
64      "evolution",
65      "contradiction",
66      "codebook",
67      "gap-detection",
68      "inverse-tree",
69      "long-memory",
70      "treeos-cascade",
71      "gateway",
72    ],
73  },
74
75  provides: {
76    models: {},
77    routes: false,
78    tools: false,
79    jobs: true,
80    orchestrator: false,
81    energyActions: {
82      intentGenerate: { cost: 3 },
83      intentExecute: { cost: 2 },
84    },
85    sessionTypes: {
86      INTENT_CYCLE: "intent-cycle",
87    },
88
89    hooks: {
90      fires: [],
91      listens: ["afterBoot", "enrichContext"],
92    },
93
94    cli: [
95      {
96        command: "intent [action] [args...]", scope: ["tree"],
97        description: "Autonomous intent queue and recent executions. Actions: pause, resume, history, reject.",
98        method: "GET",
99        endpoint: "/root/:rootId/intent",
100        subcommands: {
101          "pause": { method: "POST", endpoint: "/root/:rootId/intent/pause", description: "Pause autonomous behavior" },
102          "resume": { method: "POST", endpoint: "/root/:rootId/intent/resume", description: "Resume autonomous behavior" },
103          "history": { method: "GET", endpoint: "/root/:rootId/intent/history", description: "What the tree did on its own" },
104          "reject": { method: "POST", endpoint: "/root/:rootId/intent/reject", args: ["id"], description: "Tell the tree not to do that again" },
105        },
106      },
107    ],
108  },
109};
110
1import log from "../../seed/log.js";
2import express from "express";
3import { sendOk, sendError, ERR } from "../../seed/protocol.js";
4import authenticate from "../../seed/middleware/authenticate.js";
5
6let Node = null;
7let Note = null;
8let Contribution = null;
9let _metadata = null;
10export function setModels(models) {
11  Node = models.Node;
12  Note = models.Note;
13  Contribution = models.Contribution;
14}
15export function setMetadata(metadata) { _metadata = metadata; }
16
17function validateRootId(req, res) {
18  const rootId = req.params.rootId;
19  if (!rootId || rootId === "undefined" || rootId === "null") {
20    sendError(res, 400, ERR.INVALID_INPUT, "rootId is required");
21    return null;
22  }
23  return rootId;
24}
25
26const router = express.Router();
27
28// GET /root/:rootId/intent - Show current queue and recent executions
29router.get("/root/:rootId/intent", authenticate, async (req, res) => {
30  try {
31    const rootId = validateRootId(req, res);
32    if (!rootId) return;
33    const root = await Node.findById(rootId).select("metadata name").lean();
34    if (!root) return sendError(res, 404, ERR.NODE_NOT_FOUND, "Tree not found");
35
36    const intentMeta = _metadata.getExtMeta(root, "intent");
37
38    // Find .intent node and its recent notes
39    const intentNode = await Node.findOne({ parent: rootId, name: ".intent" }).select("_id").lean();
40    let recentExecutions = [];
41    if (intentNode) {
42      recentExecutions = await Note.find({ nodeId: intentNode._id })
43        .sort({ dateCreated: -1 })
44        .limit(20)
45        .select("content dateCreated")
46        .lean();
47    }
48
49    sendOk(res, {
50      rootId,
51      enabled: !!intentMeta.enabled,
52      paused: !!intentMeta.paused,
53      rejections: intentMeta.rejections || [],
54      recentExecutions: recentExecutions.map(n => ({
55        content: n.content,
56        executedAt: n.dateCreated,
57      })),
58    });
59  } catch (err) {
60    sendError(res, 500, ERR.INTERNAL, err.message);
61  }
62});
63
64// POST /root/:rootId/intent/pause - Pause autonomous behavior
65router.post("/root/:rootId/intent/pause", authenticate, async (req, res) => {
66  try {
67    const rootId = validateRootId(req, res);
68    if (!rootId) return;
69    const root = await Node.findById(rootId);
70    if (!root) return sendError(res, 404, ERR.NODE_NOT_FOUND, "Tree not found");
71
72    const meta = _metadata.getExtMeta(root, "intent");
73    meta.paused = true;
74    await _metadata.setExtMeta(root, "intent", meta);
75
76    log.verbose("Intent", `Autonomous intent paused for tree ${root.name}`);
77    sendOk(res, { paused: true });
78  } catch (err) {
79    sendError(res, 500, ERR.INTERNAL, err.message);
80  }
81});
82
83// POST /root/:rootId/intent/resume - Resume autonomous behavior
84router.post("/root/:rootId/intent/resume", authenticate, async (req, res) => {
85  try {
86    const rootId = validateRootId(req, res);
87    if (!rootId) return;
88    const root = await Node.findById(rootId);
89    if (!root) return sendError(res, 404, ERR.NODE_NOT_FOUND, "Tree not found");
90
91    const meta = _metadata.getExtMeta(root, "intent");
92    meta.paused = false;
93    await _metadata.setExtMeta(root, "intent", meta);
94
95    log.verbose("Intent", `Autonomous intent resumed for tree ${root.name}`);
96    sendOk(res, { paused: false });
97  } catch (err) {
98    sendError(res, 500, ERR.INTERNAL, err.message);
99  }
100});
101
102// GET /root/:rootId/intent/history - Full history of autonomous actions
103router.get("/root/:rootId/intent/history", authenticate, async (req, res) => {
104  try {
105    const rootId = req.params.rootId;
106
107    // Get contributions logged by intent
108    const contributions = await Contribution.find({
109      action: "intent:executed",
110    })
111      .sort({ date: -1 })
112      .limit(50)
113      .lean();
114
115    // Filter to this tree's nodes (walk from root, not rootOwner which is a userId)
116    const { getDescendantIds } = await import("../../seed/tree/treeFetch.js");
117    const allIds = await getDescendantIds(rootId, { maxResults: 10000 });
118    const nodeIds = new Set(allIds.map(id => String(id)));
119
120    const treeContributions = contributions
121      .filter(c => nodeIds.has(c.nodeId?.toString()))
122      .map(c => ({
123        action: c.extensionData?.intent?.action,
124        reason: c.extensionData?.intent?.reason,
125        priority: c.extensionData?.intent?.priority,
126        targetNodeId: c.extensionData?.intent?.targetNodeId,
127        result: c.extensionData?.intent?.result,
128        executedAt: c.date,
129      }));
130
131    sendOk(res, { history: treeContributions });
132  } catch (err) {
133    sendError(res, 500, ERR.INTERNAL, err.message);
134  }
135});
136
137// POST /root/:rootId/intent/reject - Tell the tree not to do that again
138router.post("/root/:rootId/intent/reject", authenticate, async (req, res) => {
139  try {
140    const { id, description } = req.body;
141    if (!id && !description) {
142      return sendError(res, 400, ERR.INVALID_INPUT, "Provide an intent id or description to reject");
143    }
144
145    const rootId = validateRootId(req, res);
146    if (!rootId) return;
147    const root = await Node.findById(rootId);
148    if (!root) return sendError(res, 404, ERR.NODE_NOT_FOUND, "Tree not found");
149
150    const meta = _metadata.getExtMeta(root, "intent");
151    if (!meta.rejections) meta.rejections = [];
152
153    // Cap rejections list to prevent unbounded growth
154    if (meta.rejections.length >= 100) {
155      meta.rejections = meta.rejections.slice(-50);
156    }
157
158    meta.rejections.push({
159      pattern: description || id,
160      rejectedAt: new Date().toISOString(),
161      rejectedBy: req.userId,
162    });
163
164    await _metadata.setExtMeta(root, "intent", meta);
165
166    log.verbose("Intent", `Intent rejected for tree ${root.name}: ${description || id}`);
167    sendOk(res, { rejected: true, totalRejections: meta.rejections.length });
168  } catch (err) {
169    sendError(res, 500, ERR.INTERNAL, err.message);
170  }
171});
172
173export default router;
174
1// State Collector
2//
3// Reads every available signal source for a tree. Each source is optional.
4// If the extension is installed, its data contributes to the state snapshot.
5// If not, that section is empty. Intent generates from whatever is available.
6
7import log from "../../seed/log.js";
8import { getExtension } from "../loader.js";
9
10let _metadata = null;
11export function setMetadata(metadata) { _metadata = metadata; }
12
13/**
14 * Collect the full observable state of a tree for intent generation.
15 *
16 * @param {string} rootId
17 * @param {object} rootNode - the tree root node document
18 * @param {object} models - { Node, User }
19 * @returns {object} state snapshot with sections for each signal source
20 */
21export async function collectTreeState(rootId, rootNode, models) {
22  const { Node } = models;
23  const state = {
24    rootId,
25    rootName: rootNode.name,
26    collectedAt: new Date().toISOString(),
27    pulse: null,
28    evolution: null,
29    contradictions: null,
30    codebook: null,
31    gaps: null,
32    userProfile: null,
33    cascade: null,
34    circuit: null,
35    rejections: [],
36  };
37
38  // Pulse: health snapshots
39  const pulseExt = getExtension("pulse");
40  if (pulseExt?.exports?.getHealthSnapshot) {
41    try {
42      state.pulse = await pulseExt.exports.getHealthSnapshot(rootId);
43    } catch (err) {
44      log.debug("Intent", `Pulse data unavailable for ${rootId}: ${err.message}`);
45    }
46  }
47
48  // Evolution: dormant branches, fitness metrics
49  const evolutionExt = getExtension("evolution");
50  if (evolutionExt?.exports?.getEvolutionState) {
51    try {
52      state.evolution = await evolutionExt.exports.getEvolutionState(rootId);
53    } catch (err) {
54      log.debug("Intent", `Evolution data unavailable for ${rootId}: ${err.message}`);
55    }
56  }
57
58  // Contradiction: unresolved conflicts
59  const contradictionExt = getExtension("contradiction");
60  if (contradictionExt?.exports?.getUnresolved) {
61    try {
62      state.contradictions = await contradictionExt.exports.getUnresolved(rootId);
63    } catch (err) {
64      log.debug("Intent", `Contradiction data unavailable for ${rootId}: ${err.message}`);
65    }
66  }
67
68  // Codebook: compression status
69  const codebookExt = getExtension("codebook");
70  if (codebookExt?.exports?.getCompressionStatus) {
71    try {
72      state.codebook = await codebookExt.exports.getCompressionStatus(rootId);
73    } catch (err) {
74      log.debug("Intent", `Codebook data unavailable for ${rootId}: ${err.message}`);
75    }
76  }
77
78  // Gap detection: missing extensions
79  const gapExt = getExtension("gap-detection");
80  if (gapExt?.exports?.getGaps) {
81    try {
82      state.gaps = await gapExt.exports.getGaps(rootId);
83    } catch (err) {
84      log.debug("Intent", `Gap data unavailable for ${rootId}: ${err.message}`);
85    }
86  }
87
88  // Inverse-tree: user profile, goals vs actions
89  const inverseExt = getExtension("inverse-tree");
90  if (inverseExt?.exports?.getProfile) {
91    try {
92      state.userProfile = await inverseExt.exports.getProfile(rootNode.rootOwner);
93    } catch (err) {
94      log.debug("Intent", `Inverse-tree data unavailable for ${rootId}: ${err.message}`);
95    }
96  }
97
98  // Cascade flow: stuck nodes, flow rates
99  try {
100    const flowMeta = _metadata.getExtMeta(rootNode, "flow");
101    if (flowMeta && Object.keys(flowMeta).length > 0) {
102      state.cascade = flowMeta;
103    }
104  } catch (err) {
105    log.debug("Intent", "Cascade flow metadata read failed:", err.message);
106  }
107
108  // Circuit breaker: approaching thresholds
109  try {
110    const circuitMeta = _metadata.getExtMeta(rootNode, "circuit");
111    if (circuitMeta) {
112      state.circuit = circuitMeta;
113    }
114  } catch (err) {
115    log.debug("Intent", "Circuit breaker metadata read failed:", err.message);
116  }
117
118  // Load rejections (intents the user said "don't do that again")
119  try {
120    const intentMeta = _metadata.getExtMeta(rootNode, "intent");
121    if (intentMeta?.rejections) {
122      state.rejections = intentMeta.rejections;
123    }
124  } catch (err) {
125    log.debug("Intent", "Intent metadata read failed:", err.message);
126  }
127
128  return state;
129}
130
131/**
132 * Format the collected state into a prompt-friendly string.
133 * Only includes sections that have data.
134 */
135export function formatStateForPrompt(state) {
136  const sections = [];
137
138  if (state.pulse) {
139    sections.push(`Pulse health: ${JSON.stringify(state.pulse)}`);
140  }
141  if (state.evolution) {
142    sections.push(`Evolution: ${JSON.stringify(state.evolution)}`);
143  }
144  if (state.contradictions && state.contradictions.length > 0) {
145    sections.push(`Unresolved contradictions (${state.contradictions.length}): ${JSON.stringify(state.contradictions.slice(0, 10))}`);
146  }
147  if (state.codebook) {
148    sections.push(`Codebook compression status: ${JSON.stringify(state.codebook)}`);
149  }
150  if (state.gaps && state.gaps.length > 0) {
151    sections.push(`Detected gaps (${state.gaps.length}): ${JSON.stringify(state.gaps.slice(0, 10))}`);
152  }
153  if (state.userProfile) {
154    sections.push(`User profile: ${JSON.stringify(state.userProfile)}`);
155  }
156  if (state.cascade) {
157    sections.push(`Cascade flow: ${JSON.stringify(state.cascade)}`);
158  }
159  if (state.circuit) {
160    sections.push(`Circuit breaker: ${JSON.stringify(state.circuit)}`);
161  }
162
163  if (sections.length === 0) {
164    return null; // Nothing to observe. No intents to generate.
165  }
166
167  let prompt = sections.join("\n- ");
168
169  if (state.rejections.length > 0) {
170    const rejectSummary = state.rejections
171      .slice(-20)
172      .map(r => r.pattern || r.description || r.action)
173      .filter(Boolean)
174      .join("; ");
175    if (rejectSummary) {
176      prompt += `\n\nRejected intents (do not regenerate these): ${rejectSummary}`;
177    }
178  }
179
180  return prompt;
181}
182

Versions

Version Published Downloads
1.0.1 38d ago 0
1.0.0 48d ago 0
0 stars
0 flags
React from the CLI: treeos ext star intent

Comments

Loading comments...

Post comments from the CLI: treeos ext comment intent "your comment"
Max 3 comments per extension. One star and one flag per user.