EXTENSION seed
teach
When a tree has been alive long enough, it accumulates wisdom that is not structure and is not content. It is meta-knowledge. Evolution discovered that branches with 3 children complete 4x more than branches with 10. Purpose learned that the thesis drifts when the user adds too many top-level branches. Prune learned which patterns indicate dead weight. Codebook learned what language compresses best between this user and this domain. Teach extracts this meta-knowledge from every installed intelligence extension and packages it as a transferable lesson set. Not the raw data. The conclusions. Each lesson names its source extension, states the insight in natural language, carries a confidence score derived from sample size and consistency, and records the sample size so the receiving tree can weigh it. Three delivery paths. Export writes the lesson set to a JSON file that travels alongside a seed-export. Import reads a lesson file into a tree's metadata where enrichContext surfaces it to the AI. Share sends the lesson set to a peered land through canopy as a cascade signal. The receiving tree absorbs the lessons without ever sharing content. Lesson extraction is LLM-powered. For each installed intelligence extension, teach reads its accumulated state (evolution fitness scores, prune history, purpose coherence trends, codebook compression stats, boundary similarity matrices) and asks the AI to distill the data into actionable insights with confidence ratings. The AI sees the numbers. It produces the sentences. Lessons are not permanent. They can be dismissed if they do not apply to the receiving tree's context. Dismissed lessons are excluded from enrichContext but retained in metadata for audit. Lessons decay: confidence drops over time as the receiving tree accumulates its own experience that may contradict the imported wisdom. seed-export captures form. Teach captures understanding. Together they let a new tree start with both the shape and the wisdom of the tree that came before it.
v1.0.1 by TreeOS Site 0 downloads 5 files 753 lines 25.2 KB published 38d ago
treeos ext install teach
View changelog

Manifest

Provides

  • routes
  • tools
  • 1 CLI commands
  • 1 energy actions

Requires

  • services: hooks, llm, contributions
  • models: Node

Optional

  • services: energy
  • extensions: evolution, prune, purpose, codebook, boundary, tree-compress, inverse-tree, phase, seed-export
SHA256: 5d10a483736bd16829f1f349451384810a7e84c537f7058c11e492905bc5073e

Dependents

1 package depend on this

PackageTypeRelationship
treeos v1.0.1osstandalone

CLI Commands

CommandMethodDescription
teachPOSTTree wisdom transfer. Actions: import, share, lessons, dismiss.
teach importPOSTImport lessons to this tree
teach sharePOSTSend lessons to a peered land's tree
teach lessonsGETShow active lessons at this position
teach dismissPOSTDismiss a lesson that does not apply

Hooks

Listens To

  • enrichContext

Source Code

1// Teach Core
2//
3// Extract meta-knowledge from intelligence extensions into transferable lesson sets.
4// Each lesson: { id, from, insight, confidence, sampleSize, extractedAt }
5// LLM distills raw extension state into actionable natural language insights.
6
7import log from "../../seed/log.js";
8import { getExtension } from "../loader.js";
9import { parseJsonSafe } from "../../seed/orchestrators/helpers.js";
10import { v4 as uuidv4 } from "uuid";
11
12let Node = null;
13let logContribution = async () => {};
14let runChat = null;
15let useEnergy = async () => ({ energyUsed: 0 });
16let _metadata = null;
17
18export function setServices({ models, contributions, llm, energy, metadata }) {
19  Node = models.Node;
20  logContribution = contributions.logContribution;
21  runChat = llm.runChat;
22  if (energy?.useEnergy) useEnergy = energy.useEnergy;
23  _metadata = metadata;
24}
25
26const TEACH_VERSION = "1.0.0";
27
28// ─────────────────────────────────────────────────────────────────────────
29// EXTRACTION SOURCES
30// Each collector reads an extension's accumulated state and returns
31// a data summary string for the LLM to distill.
32// ─────────────────────────────────────────────────────────────────────────
33
34const COLLECTORS = [
35  {
36    extName: "evolution",
37    label: "structural evolution patterns",
38    collect: async (rootId) => {
39      const ext = getExtension("evolution");
40      if (!ext?.exports?.getEvolutionReport) return null;
41      try {
42        const report = await ext.exports.getEvolutionReport(rootId);
43        if (!report || !report.fitness) return null;
44        return JSON.stringify({
45          fitness: report.fitness,
46          patterns: report.patterns?.slice(0, 10),
47          survivedStructures: report.survivedStructures?.slice(0, 5),
48          revertedStructures: report.revertedStructures?.slice(0, 5),
49        });
50      } catch { return null; }
51    },
52  },
53  {
54    extName: "prune",
55    label: "pruning history and dormancy patterns",
56    collect: async (rootId) => {
57      const root = await Node.findById(rootId).select("metadata").lean();
58      if (!root) return null;
59      const pruneMeta = _metadata.getExtMeta(root, "prune");
60      if (!pruneMeta?.history?.length) return null;
61      return JSON.stringify({
62        totalPruned: pruneMeta.history.reduce((s, h) => s + (h.pruned || 0), 0),
63        totalAbsorbed: pruneMeta.history.reduce((s, h) => s + (h.absorbed || 0), 0),
64        pruneCount: pruneMeta.history.length,
65        recentHistory: pruneMeta.history.slice(-5),
66        dormancyDays: pruneMeta.dormancyDays,
67      });
68    },
69  },
70  {
71    extName: "purpose",
72    label: "thesis coherence trends",
73    collect: async (rootId) => {
74      const ext = getExtension("purpose");
75      if (!ext?.exports?.getThesis) return null;
76      try {
77        const thesis = await ext.exports.getThesis(rootId);
78        if (!thesis) return null;
79        return JSON.stringify({
80          thesis: thesis.statement,
81          coherence: thesis.coherence,
82          lastChecked: thesis.lastCheckedAt,
83          rederiveCount: thesis.rederiveCount,
84        });
85      } catch { return null; }
86    },
87  },
88  {
89    extName: "codebook",
90    label: "language compression statistics",
91    collect: async (rootId) => {
92      const ext = getExtension("codebook");
93      if (!ext?.exports?.getCodebookStats) return null;
94      try {
95        const stats = await ext.exports.getCodebookStats(rootId);
96        if (!stats) return null;
97        return JSON.stringify(stats);
98      } catch { return null; }
99    },
100  },
101  {
102    extName: "boundary",
103    label: "structural cohesion analysis",
104    collect: async (rootId) => {
105      const ext = getExtension("boundary");
106      if (!ext?.exports?.getBoundaryReport) return null;
107      try {
108        const report = await ext.exports.getBoundaryReport(rootId);
109        if (!report) return null;
110        return JSON.stringify({
111          overallCoherence: report.overallCoherence,
112          branchCount: report.branchCount,
113          findingsCount: report.findings?.length,
114          topFindings: report.findings?.slice(0, 5),
115        });
116      } catch { return null; }
117    },
118  },
119  {
120    extName: "tree-compress",
121    label: "compression patterns",
122    collect: async (rootId) => {
123      const ext = getExtension("tree-compress");
124      if (!ext?.exports?.getCompressStatus) return null;
125      try {
126        const status = await ext.exports.getCompressStatus(rootId);
127        if (!status) return null;
128        return JSON.stringify(status);
129      } catch { return null; }
130    },
131  },
132  {
133    extName: "phase",
134    label: "activity phase patterns",
135    collect: async (rootId) => {
136      const ext = getExtension("phase");
137      if (!ext?.exports?.getPhaseState) return null;
138      try {
139        const state = await ext.exports.getPhaseState(rootId);
140        if (!state) return null;
141        return JSON.stringify(state);
142      } catch { return null; }
143    },
144  },
145];
146
147const EXTRACT_PROMPT = `You are analyzing accumulated data from a tree's intelligence extensions. Your job is to distill actionable lessons from the raw data.
148
149For each data source below, produce 0-3 lessons. Each lesson must be:
150- A concrete, specific insight (not generic advice)
151- Derived from the data (not assumed)
152- Useful to someone starting a similar tree from scratch
153
154Data sources:
155{sources}
156
157Return a JSON array of lessons:
158[
159  {
160    "from": "extension-name",
161    "insight": "Specific actionable insight derived from the data",
162    "confidence": 0.85,
163    "sampleSize": 47
164  }
165]
166
167Rules:
168- confidence is 0-1 based on how much data supports the insight
169- sampleSize is the approximate number of data points behind it
170- If a data source has too little data for a meaningful insight, skip it
171- Maximum 15 lessons total
172- If no meaningful lessons can be extracted, return []`;
173
174// ─────────────────────────────────────────────────────────────────────────
175// EXPORT (extract lessons from a tree)
176// ─────────────────────────────────────────────────────────────────────────
177
178export async function extractLessons(rootId, userId, username) {
179  await useEnergy({ userId, action: "teachExtract" });
180
181  const root = await Node.findById(rootId).select("_id name rootOwner").lean();
182  if (!root) throw new Error("Tree root not found");
183
184  // Collect data from all available intelligence extensions
185  const sources = [];
186  for (const collector of COLLECTORS) {
187    const data = await collector.collect(rootId);
188    if (data) {
189      sources.push({ extName: collector.extName, label: collector.label, data });
190    }
191  }
192
193  if (sources.length === 0) {
194    throw new Error("No intelligence extension data available to extract lessons from");
195  }
196
197  // Format sources for the LLM
198  const sourcesText = sources.map(s =>
199    `[${s.extName}] ${s.label}:\n${s.data}`
200  ).join("\n\n");
201
202  const prompt = EXTRACT_PROMPT.replace("{sources}", sourcesText);
203
204  const result = await runChat({
205    userId,
206    username,
207    message: prompt,
208    mode: "tree:respond",
209    rootId,
210    slot: "teach",
211  });
212
213  if (!result?.answer) throw new Error("Lesson extraction produced no result");
214
215  const parsed = parseJsonSafe(result.answer);
216  if (!Array.isArray(parsed)) throw new Error("Lesson extraction did not return a valid lesson array");
217
218  // Add IDs and metadata to each lesson
219  const lessons = parsed
220    .filter(l => l && l.from && l.insight && typeof l.confidence === "number")
221    .slice(0, 15)
222    .map(l => ({
223      id: uuidv4(),
224      from: l.from,
225      insight: l.insight,
226      confidence: Math.max(0, Math.min(1, l.confidence)),
227      sampleSize: l.sampleSize || 0,
228      extractedAt: new Date().toISOString(),
229    }));
230
231  // Get land info
232  let sourceLand = "unknown";
233  try {
234    const { getLandIdentity } = await import("../../canopy/identity.js");
235    const identity = getLandIdentity();
236    if (identity?.domain) sourceLand = identity.domain;
237  } catch {}
238
239  // Calculate tree age
240  const treeAge = root.dateCreated
241    ? Math.round((Date.now() - new Date(root.dateCreated).getTime()) / (30 * 24 * 60 * 60 * 1000))
242    : null;
243
244  const lessonSet = {
245    teachVersion: TEACH_VERSION,
246    source: `${root.name} tree, ${sourceLand}${treeAge ? `, ${treeAge} months active` : ""}`,
247    sourceTreeId: rootId,
248    sourceTreeName: root.name,
249    sourceLand,
250    exportedAt: new Date().toISOString(),
251    exportedBy: username,
252    extensionsQueried: sources.map(s => s.extName),
253    lessons,
254  };
255
256  // Log contribution
257  await logContribution({
258    userId,
259    nodeId: rootId,
260    wasAi: true,
261    action: "teach:exported",
262    extensionData: {
263      teach: {
264        lessonCount: lessons.length,
265        extensionsQueried: sources.map(s => s.extName),
266      },
267    },
268  });
269
270  log.info("Teach", `Extracted ${lessons.length} lesson(s) from ${root.name} (${sources.length} sources)`);
271
272  return lessonSet;
273}
274
275// ─────────────────────────────────────────────────────────────────────────
276// IMPORT (absorb lessons into a tree)
277// ─────────────────────────────────────────────────────────────────────────
278
279export async function importLessons(rootId, lessonSet, userId) {
280  if (!lessonSet?.lessons?.length) throw new Error("No lessons in the provided set");
281
282  const root = await Node.findById(rootId);
283  if (!root) throw new Error("Tree root not found");
284
285  const meta = _metadata.getExtMeta(root, "teach");
286  if (!meta.lessons) meta.lessons = [];
287  if (!meta.dismissed) meta.dismissed = [];
288
289  // Merge: add new lessons, skip duplicates by insight text
290  const existingInsights = new Set(meta.lessons.map(l => l.insight));
291  const dismissedInsights = new Set(meta.dismissed.map(l => l.insight));
292  let added = 0;
293
294  for (const lesson of lessonSet.lessons) {
295    if (existingInsights.has(lesson.insight)) continue;
296    if (dismissedInsights.has(lesson.insight)) continue;
297
298    meta.lessons.push({
299      ...lesson,
300      id: lesson.id || uuidv4(),
301      importedAt: new Date().toISOString(),
302      importedFrom: lessonSet.source || "unknown",
303    });
304    added++;
305  }
306
307  await _metadata.setExtMeta(root, "teach", meta);
308
309  await logContribution({
310    userId,
311    nodeId: rootId,
312    wasAi: false,
313    action: "teach:imported",
314    extensionData: {
315      teach: {
316        added,
317        total: meta.lessons.length,
318        source: lessonSet.source,
319      },
320    },
321  });
322
323  log.verbose("Teach", `Imported ${added} lesson(s) to ${root.name} (${meta.lessons.length} total)`);
324
325  return { added, total: meta.lessons.length, source: lessonSet.source };
326}
327
328// ─────────────────────────────────────────────────────────────────────────
329// SHARE (send lessons to a peered land via cascade)
330// ─────────────────────────────────────────────────────────────────────────
331
332export async function shareLessons(rootId, peerDomain, userId, username) {
333  const lessonSet = await extractLessons(rootId, userId, username);
334
335  // Send via deliverCascade with a teach-specific tag
336  const { deliverCascade } = await import("../../seed/tree/cascade.js");
337  const result = await deliverCascade({
338    nodeId: rootId,
339    signalId: uuidv4(),
340    payload: {
341      _teach: true,
342      lessonSet,
343      targetPeer: peerDomain,
344    },
345    source: rootId,
346    depth: 0,
347  });
348
349  log.verbose("Teach", `Shared ${lessonSet.lessons.length} lesson(s) from ${rootId} to ${peerDomain}`);
350
351  return {
352    shared: true,
353    lessonCount: lessonSet.lessons.length,
354    targetPeer: peerDomain,
355    cascadeStatus: result?.status,
356  };
357}
358
359// ─────────────────────────────────────────────────────────────────────────
360// DISMISS (mark a lesson as not applicable)
361// ─────────────────────────────────────────────────────────────────────────
362
363export async function dismissLesson(rootId, lessonId, userId) {
364  const root = await Node.findById(rootId);
365  if (!root) throw new Error("Tree root not found");
366
367  const meta = _metadata.getExtMeta(root, "teach");
368  if (!meta.lessons) return { dismissed: false };
369  if (!meta.dismissed) meta.dismissed = [];
370
371  const idx = meta.lessons.findIndex(l => l.id === lessonId);
372  if (idx === -1) throw new Error("Lesson not found");
373
374  const lesson = meta.lessons.splice(idx, 1)[0];
375  lesson.dismissedAt = new Date().toISOString();
376  lesson.dismissedBy = userId;
377  meta.dismissed.push(lesson);
378
379  await _metadata.setExtMeta(root, "teach", meta);
380
381  log.verbose("Teach", `Dismissed lesson "${lesson.insight.slice(0, 60)}..." at ${rootId}`);
382
383  return { dismissed: true, lessonId, insight: lesson.insight };
384}
385
386// ─────────────────────────────────────────────────────────────────────────
387// READ
388// ─────────────────────────────────────────────────────────────────────────
389
390export async function getLessons(rootId) {
391  const root = await Node.findById(rootId).select("metadata").lean();
392  if (!root) throw new Error("Tree root not found");
393
394  const meta = _metadata.getExtMeta(root, "teach");
395  return {
396    lessons: meta.lessons || [],
397    dismissed: meta.dismissed || [],
398    totalActive: (meta.lessons || []).length,
399    totalDismissed: (meta.dismissed || []).length,
400  };
401}
402
1import log from "../../seed/log.js";
2import tools from "./tools.js";
3import {
4  setServices,
5  extractLessons,
6  importLessons,
7  shareLessons,
8  dismissLesson,
9  getLessons,
10} from "./core.js";
11
12export async function init(core) {
13  const BG = core.llm.LLM_PRIORITY.BACKGROUND;
14
15  core.llm.registerRootLlmSlot("teach");
16
17  setServices({
18    models: core.models,
19    contributions: core.contributions,
20    llm: { ...core.llm, runChat: async (opts) => {
21      if (opts.userId && opts.userId !== "SYSTEM" && !await core.llm.userHasLlm(opts.userId)) return { answer: null };
22      return core.llm.runChat({ ...opts, llmPriority: BG });
23    } },
24    energy: core.energy || null,
25    metadata: core.metadata,
26  });
27
28  // ── enrichContext: surface active lessons to the AI ──────────────────
29  //
30  // At the tree root, inject all active lessons so the AI knows the
31  // accumulated wisdom. At child nodes, inject a summary count so the
32  // AI knows lessons exist without flooding context at every position.
33
34  core.hooks.register("enrichContext", async ({ context, node, meta }) => {
35    const teachMeta = meta.teach;
36    if (!teachMeta?.lessons?.length) return;
37
38    const active = teachMeta.lessons.filter(l => !l.dismissedAt);
39    if (active.length === 0) return;
40
41    // At root: full lessons. At children: count + top 3.
42    if (node.rootOwner) {
43      context.treeLessons = active.map(l => ({
44        from: l.from,
45        insight: l.insight,
46        confidence: l.confidence,
47        importedFrom: l.importedFrom || null,
48      }));
49    } else {
50      context.treeLessonCount = active.length;
51      context.topLessons = active
52        .sort((a, b) => b.confidence - a.confidence)
53        .slice(0, 3)
54        .map(l => l.insight);
55    }
56  }, "teach");
57
58  // ── onCascade: receive shared lessons from peered lands ─────────────
59
60  core.hooks.register("onCascade", async (hookData) => {
61    const { nodeId, payload } = hookData;
62    if (!payload?._teach || !payload?.lessonSet) return;
63
64    // Auto-import shared lessons
65    try {
66      await importLessons(nodeId, payload.lessonSet, "system");
67      log.verbose("Teach", `Auto-imported ${payload.lessonSet.lessons?.length || 0} lesson(s) from cascade at ${nodeId}`);
68    } catch (err) {
69      log.debug("Teach", `Cascade lesson import failed: ${err.message}`);
70    }
71  }, "teach");
72
73  const { default: router } = await import("./routes.js");
74
75  log.info("Teach", "Tree wisdom transfer loaded");
76
77  return {
78    router,
79    tools,
80    exports: {
81      extractLessons,
82      importLessons,
83      shareLessons,
84      dismissLesson,
85      getLessons,
86    },
87  };
88}
89
1export default {
2  name: "teach",
3  version: "1.0.1",
4  builtFor: "seed",
5  description:
6    "When a tree has been alive long enough, it accumulates wisdom that is not " +
7    "structure and is not content. It is meta-knowledge. Evolution discovered that " +
8    "branches with 3 children complete 4x more than branches with 10. Purpose " +
9    "learned that the thesis drifts when the user adds too many top-level branches. " +
10    "Prune learned which patterns indicate dead weight. Codebook learned what " +
11    "language compresses best between this user and this domain." +
12    "\n\n" +
13    "Teach extracts this meta-knowledge from every installed intelligence extension " +
14    "and packages it as a transferable lesson set. Not the raw data. The conclusions. " +
15    "Each lesson names its source extension, states the insight in natural language, " +
16    "carries a confidence score derived from sample size and consistency, and records " +
17    "the sample size so the receiving tree can weigh it." +
18    "\n\n" +
19    "Three delivery paths. Export writes the lesson set to a JSON file that travels " +
20    "alongside a seed-export. Import reads a lesson file into a tree's metadata where " +
21    "enrichContext surfaces it to the AI. Share sends the lesson set to a peered land " +
22    "through canopy as a cascade signal. The receiving tree absorbs the lessons without " +
23    "ever sharing content." +
24    "\n\n" +
25    "Lesson extraction is LLM-powered. For each installed intelligence extension, teach " +
26    "reads its accumulated state (evolution fitness scores, prune history, purpose " +
27    "coherence trends, codebook compression stats, boundary similarity matrices) and " +
28    "asks the AI to distill the data into actionable insights with confidence ratings. " +
29    "The AI sees the numbers. It produces the sentences." +
30    "\n\n" +
31    "Lessons are not permanent. They can be dismissed if they do not apply to the " +
32    "receiving tree's context. Dismissed lessons are excluded from enrichContext but " +
33    "retained in metadata for audit. Lessons decay: confidence drops over time as the " +
34    "receiving tree accumulates its own experience that may contradict the imported wisdom." +
35    "\n\n" +
36    "seed-export captures form. Teach captures understanding. Together they let a new " +
37    "tree start with both the shape and the wisdom of the tree that came before it.",
38
39  needs: {
40    services: ["hooks", "llm", "contributions"],
41    models: ["Node"],
42  },
43
44  optional: {
45    services: ["energy"],
46    extensions: [
47      "evolution",
48      "prune",
49      "purpose",
50      "codebook",
51      "boundary",
52      "tree-compress",
53      "inverse-tree",
54      "phase",
55      "seed-export",
56    ],
57  },
58
59  provides: {
60    models: {},
61    routes: "./routes.js",
62    tools: true,
63    jobs: false,
64    orchestrator: false,
65    energyActions: {
66      teachExtract: { cost: 3 },
67    },
68    sessionTypes: {},
69
70    hooks: {
71      fires: [],
72      listens: ["enrichContext"],
73    },
74
75    cli: [
76      {
77        command: "teach [action]", scope: ["tree"],
78        description: "Tree wisdom transfer. Actions: import, share, lessons, dismiss.",
79        method: "POST",
80        endpoint: "/root/:rootId/teach/export",
81        subcommands: {
82          import: {
83            method: "POST",
84            endpoint: "/root/:rootId/teach/import",
85            description: "Import lessons to this tree",
86          },
87          share: {
88            method: "POST",
89            endpoint: "/root/:rootId/teach/share",
90            description: "Send lessons to a peered land's tree",
91            bodyMap: { peer: 0 },
92          },
93          lessons: {
94            method: "GET",
95            endpoint: "/root/:rootId/teach",
96            description: "Show active lessons at this position",
97          },
98          dismiss: {
99            method: "POST",
100            endpoint: "/root/:rootId/teach/dismiss",
101            description: "Dismiss a lesson that does not apply",
102            bodyMap: { id: 0 },
103          },
104        },
105      },
106    ],
107  },
108};
109
1import express from "express";
2import { sendOk, sendError, ERR } from "../../seed/protocol.js";
3import authenticate from "../../seed/middleware/authenticate.js";
4import { extractLessons, importLessons, shareLessons, dismissLesson, getLessons } from "./core.js";
5
6const router = express.Router();
7
8// GET /root/:rootId/teach - Show active lessons
9router.get("/root/:rootId/teach", authenticate, async (req, res) => {
10  try {
11    const result = await getLessons(req.params.rootId);
12    sendOk(res, result);
13  } catch (err) {
14    sendError(res, 400, ERR.INVALID_INPUT, err.message);
15  }
16});
17
18// POST /root/:rootId/teach/export - Extract lessons from this tree
19router.post("/root/:rootId/teach/export", authenticate, async (req, res) => {
20  try {
21    const lessonSet = await extractLessons(req.params.rootId, req.userId, req.username);
22    sendOk(res, lessonSet);
23  } catch (err) {
24    sendError(res, 400, ERR.INVALID_INPUT, err.message);
25  }
26});
27
28// POST /root/:rootId/teach/import - Import lessons into this tree
29router.post("/root/:rootId/teach/import", authenticate, async (req, res) => {
30  try {
31    const lessonSet = req.body;
32    if (!lessonSet?.lessons) {
33      return sendError(res, 400, ERR.INVALID_INPUT, "Request body must contain a lesson set with a lessons array");
34    }
35    const result = await importLessons(req.params.rootId, lessonSet, req.userId);
36    sendOk(res, result);
37  } catch (err) {
38    sendError(res, 400, ERR.INVALID_INPUT, err.message);
39  }
40});
41
42// POST /root/:rootId/teach/share - Send lessons to a peered land
43router.post("/root/:rootId/teach/share", authenticate, async (req, res) => {
44  try {
45    const { peer } = req.body;
46    if (!peer) return sendError(res, 400, ERR.INVALID_INPUT, "peer (domain) is required");
47    const result = await shareLessons(req.params.rootId, peer, req.userId, req.username);
48    sendOk(res, result);
49  } catch (err) {
50    sendError(res, 400, ERR.INVALID_INPUT, err.message);
51  }
52});
53
54// POST /root/:rootId/teach/dismiss - Dismiss a lesson
55router.post("/root/:rootId/teach/dismiss", authenticate, async (req, res) => {
56  try {
57    const { id } = req.body;
58    if (!id) return sendError(res, 400, ERR.INVALID_INPUT, "id (lesson ID) is required");
59    const result = await dismissLesson(req.params.rootId, id, req.userId);
60    sendOk(res, result);
61  } catch (err) {
62    sendError(res, 400, ERR.INVALID_INPUT, err.message);
63  }
64});
65
66export default router;
67
1import { z } from "zod";
2import { extractLessons, getLessons, dismissLesson } from "./core.js";
3
4export default [
5  {
6    name: "teach-export",
7    description:
8      "Extract wisdom from this tree's intelligence extensions into a transferable " +
9      "lesson set. Reads evolution, prune, purpose, codebook, boundary, and other " +
10      "installed intelligence data, then distills actionable insights.",
11    schema: {
12      rootId: z.string().describe("Tree root to extract lessons from."),
13      userId: z.string().describe("Injected by server. Ignore."),
14      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
15      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
16    },
17    annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
18    handler: async ({ rootId, userId }) => {
19      try {
20        const User = (await import("../../seed/models/user.js")).default;
21        const user = await User.findById(userId).select("username").lean();
22        const lessonSet = await extractLessons(rootId, userId, user?.username || "system");
23        return {
24          content: [{
25            type: "text",
26            text: JSON.stringify({
27              source: lessonSet.source,
28              lessonCount: lessonSet.lessons.length,
29              extensionsQueried: lessonSet.extensionsQueried,
30              lessons: lessonSet.lessons.map(l => ({
31                from: l.from,
32                insight: l.insight,
33                confidence: l.confidence,
34              })),
35            }, null, 2),
36          }],
37        };
38      } catch (err) {
39        return { content: [{ type: "text", text: `Extraction failed: ${err.message}` }] };
40      }
41    },
42  },
43  {
44    name: "teach-lessons",
45    description: "Show active lessons at this tree. Read-only, no LLM calls.",
46    schema: {
47      rootId: z.string().describe("Tree root to check."),
48      userId: z.string().describe("Injected by server. Ignore."),
49      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
50      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
51    },
52    annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
53    handler: async ({ rootId }) => {
54      try {
55        const result = await getLessons(rootId);
56        if (result.totalActive === 0) {
57          return { content: [{ type: "text", text: "No active lessons at this tree. Use teach-export to extract lessons, or import them from another tree." }] };
58        }
59        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
60      } catch (err) {
61        return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
62      }
63    },
64  },
65  {
66    name: "teach-dismiss",
67    description: "Dismiss a lesson that does not apply to this tree.",
68    schema: {
69      rootId: z.string().describe("Tree root."),
70      lessonId: z.string().describe("ID of the lesson to dismiss."),
71      userId: z.string().describe("Injected by server. Ignore."),
72      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
73      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
74    },
75    annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
76    handler: async ({ rootId, lessonId, userId }) => {
77      try {
78        const result = await dismissLesson(rootId, lessonId, userId);
79        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
80      } catch (err) {
81        return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
82      }
83    },
84  },
85];
86

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 teach

Comments

Loading comments...

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