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
Loading comments...