EXTENSION for treeos-intelligence
competence
The tree knows where its knowledge ends. Tracks which queries found answers and which found silence. Over time builds a map of the tree's competence boundary. The AI injects: I can help with X, Y, Z at this branch. I don't have information about A or B. Honest about limits instead of hallucinating.
v1.0.1 by TreeOS Site 0 downloads 5 files 350 lines 11.2 KB published 38d ago
treeos ext install competence
View changelog

Manifest

Provides

  • routes
  • tools
  • 1 CLI commands

Requires

  • services: hooks, metadata
  • models: Node

Optional

  • extensions: embed, explore, gap-detection
SHA256: 4ac63e0203bc6ae1f6913751e4c0ab24d597d3c16405553464179b8bbebef204

Dependents

1 package depend on this

PackageTypeRelationship
treeos-intelligence v1.0.2bundleincludes

CLI Commands

CommandMethodDescription
competenceGETKnowledge boundaries at this position

Hooks

Listens To

  • afterLLMCall
  • enrichContext

Source Code

1/**
2 * Competence Core
3 *
4 * Tracks which queries found answers and which found silence.
5 * Builds a competence boundary map from accumulated query history.
6 * No LLM calls. Purely observational.
7 */
8
9import log from "../../seed/log.js";
10import Node from "../../seed/models/node.js";
11
12let _metadata = null;
13export function configure({ metadata }) { _metadata = metadata; }
14
15const MAX_QUERIES = 100;
16
17// ─────────────────────────────────────────────────────────────────────────
18// RECORDING
19// ─────────────────────────────────────────────────────────────────────────
20
21/**
22 * Heuristic: did the AI's response actually answer the user's query?
23 * Checks for tool usage, note references, and hedging language.
24 */
25function detectAnswer(response) {
26  if (!response) return { hadAnswer: false, confidence: 0 };
27  const text = (response || "").toLowerCase();
28
29  // Signals of answering: tool calls happened, notes referenced, specific content cited
30  const answerSignals = [
31    /\baccording to\b/,
32    /\bnotes? (show|indicate|say|mention)/,
33    /\bhere('s| is) what/,
34    /\bfound\b.*\b(note|entry|record)/,
35    /\byou (wrote|created|recorded|noted)/,
36    /\bon \d{4}-\d{2}-\d{2}/,  // date references suggest citing real content
37  ];
38
39  // Signals of not answering: hedging, suggesting to look elsewhere
40  const silenceSignals = [
41    /\bi don't (have|see|find) (information|data|notes?|anything)/,
42    /\bno (information|data|notes?|records?) (about|on|for|regarding)/,
43    /\bi('m| am) not (sure|able|finding)/,
44    /\byou (might|could|should) (try|check|look)/,
45    /\bthere('s| is) no (data|information|content)/,
46    /\bi (can't|cannot) find/,
47  ];
48
49  let answerScore = 0;
50  let silenceScore = 0;
51
52  for (const p of answerSignals) {
53    if (p.test(text)) answerScore++;
54  }
55  for (const p of silenceSignals) {
56    if (p.test(text)) silenceScore++;
57  }
58
59  if (answerScore > silenceScore) {
60    return { hadAnswer: true, confidence: Math.min(0.5 + answerScore * 0.15, 1.0) };
61  }
62  if (silenceScore > 0) {
63    return { hadAnswer: false, confidence: Math.min(0.5 + silenceScore * 0.15, 1.0) };
64  }
65
66  // Ambiguous: assume answered if response is substantial
67  return { hadAnswer: text.length > 200, confidence: 0.4 };
68}
69
70/**
71 * Record a query result on a node.
72 */
73export async function recordQuery(nodeId, query, hadAnswer, confidence, userId) {
74  try {
75    const node = await Node.findById(nodeId);
76    if (!node) return;
77
78    const meta = _metadata.getExtMeta(node, "competence") || {};
79    if (!meta.queries) meta.queries = [];
80
81    meta.queries.push({
82      query: (query || "").slice(0, 200),
83      hadAnswer,
84      confidence,
85      timestamp: Date.now(),
86      userId,
87    });
88
89    // Cap rolling array
90    if (meta.queries.length > MAX_QUERIES) {
91      meta.queries = meta.queries.slice(-MAX_QUERIES);
92    }
93
94    // Recompute topics
95    const computed = computeCompetence(meta.queries);
96    meta.strongTopics = computed.strongTopics;
97    meta.weakTopics = computed.weakTopics;
98    meta.answerRate = computed.answerRate;
99    meta.lastUpdated = Date.now();
100
101    await _metadata.setExtMeta(node, "competence", meta);
102  } catch (err) {
103    log.debug("Competence", `recordQuery failed: ${err.message}`);
104  }
105}
106
107/**
108 * Process an afterLLMCall event to detect competence.
109 */
110export function processLLMCall({ nodeId, userId, message, answer }) {
111  if (!nodeId || !message) return;
112
113  // Only process user queries (not system prompts, not tool responses)
114  if (!userId || userId === "SYSTEM") return;
115
116  const { hadAnswer, confidence } = detectAnswer(answer);
117  recordQuery(nodeId, message, hadAnswer, confidence, userId);
118}
119
120// ─────────────────────────────────────────────────────────────────────────
121// COMPUTATION
122// ─────────────────────────────────────────────────────────────────────────
123
124/**
125 * Extract strong and weak topics from query history via word frequency.
126 */
127function computeCompetence(queries) {
128  if (!queries || queries.length === 0) {
129    return { strongTopics: [], weakTopics: [], answerRate: 0 };
130  }
131
132  const answered = queries.filter(q => q.hadAnswer);
133  const unanswered = queries.filter(q => !q.hadAnswer);
134  const answerRate = queries.length > 0 ? answered.length / queries.length : 0;
135
136  // Word frequency in answered vs unanswered queries
137  const stopWords = new Set(["the", "a", "an", "is", "are", "was", "were", "what", "how", "when", "where", "why", "who", "can", "do", "does", "did", "will", "would", "should", "could", "have", "has", "had", "been", "being", "this", "that", "these", "those", "for", "with", "about", "from", "into", "but", "and", "not", "any", "all", "some", "more", "most", "you", "your", "my", "me", "its"]);
138
139  function extractWords(queryList) {
140    const freq = new Map();
141    for (const q of queryList) {
142      const words = (q.query || "").toLowerCase().split(/\s+/).filter(w => w.length > 2 && !stopWords.has(w));
143      for (const w of words) {
144        freq.set(w, (freq.get(w) || 0) + 1);
145      }
146    }
147    return freq;
148  }
149
150  const answeredFreq = extractWords(answered);
151  const unansweredFreq = extractWords(unanswered);
152
153  // Strong topics: words that appear frequently in answered queries but rarely in unanswered
154  const strongTopics = [...answeredFreq.entries()]
155    .filter(([word, count]) => count >= 2 && (!unansweredFreq.has(word) || unansweredFreq.get(word) < count))
156    .sort((a, b) => b[1] - a[1])
157    .slice(0, 10)
158    .map(([word]) => word);
159
160  // Weak topics: words that appear frequently in unanswered queries but rarely in answered
161  const weakTopics = [...unansweredFreq.entries()]
162    .filter(([word, count]) => count >= 2 && (!answeredFreq.has(word) || answeredFreq.get(word) < count))
163    .sort((a, b) => b[1] - a[1])
164    .slice(0, 10)
165    .map(([word]) => word);
166
167  return { strongTopics, weakTopics, answerRate };
168}
169
170// ─────────────────────────────────────────────────────────────────────────
171// READ
172// ─────────────────────────────────────────────────────────────────────────
173
174/**
175 * Get the competence map for a node.
176 */
177export async function getCompetence(nodeId) {
178  const node = await Node.findById(nodeId).select("metadata").lean();
179  if (!node) return null;
180
181  const meta = node.metadata instanceof Map
182    ? node.metadata.get("competence") || {}
183    : node.metadata?.competence || {};
184
185  return {
186    totalQueries: (meta.queries || []).length,
187    answered: (meta.queries || []).filter(q => q.hadAnswer).length,
188    unanswered: (meta.queries || []).filter(q => !q.hadAnswer).length,
189    answerRate: meta.answerRate || 0,
190    strongTopics: meta.strongTopics || [],
191    weakTopics: meta.weakTopics || [],
192    lastUpdated: meta.lastUpdated || null,
193  };
194}
195
1import log from "../../seed/log.js";
2import tools from "./tools.js";
3import { configure, processLLMCall, getCompetence } from "./core.js";
4
5export async function init(core) {
6  configure({ metadata: core.metadata });
7  // afterLLMCall: track which modes produced tool calls (proxy for finding answers)
8  core.hooks.register("afterLLMCall", async ({ userId, rootId, mode, hasToolCalls }) => {
9    if (!rootId || !userId || userId === "SYSTEM") return;
10
11    try {
12      processLLMCall({ rootId, userId, mode, hasToolCalls });
13    } catch (err) {
14      log.debug("Competence", `afterLLMCall processing failed: ${err.message}`);
15    }
16  }, "competence");
17
18  // enrichContext: tell the AI what it can and can't help with
19  core.hooks.register("enrichContext", async ({ context, node, meta }) => {
20    const comp = meta?.competence;
21    if (!comp?.queries?.length || comp.queries.length < 10) return; // need enough data
22
23    const strong = comp.strongTopics?.slice(0, 5) || [];
24    const weak = comp.weakTopics?.slice(0, 5) || [];
25
26    if (strong.length > 0 || weak.length > 0) {
27      context.competence = {
28        canHelpWith: strong,
29        noDataOn: weak,
30        answerRate: comp.answerRate,
31      };
32    }
33  }, "competence");
34
35  const { default: router } = await import("./routes.js");
36
37  log.verbose("Competence", "Competence loaded");
38
39  return {
40    router,
41    tools,
42    exports: {
43      getCompetence,
44    },
45  };
46}
47
1export default {
2  name: "competence",
3  version: "1.0.1",
4  builtFor: "treeos-intelligence",
5  description:
6    "The tree knows where its knowledge ends. Tracks which queries found answers " +
7    "and which found silence. Over time builds a map of the tree's competence boundary. " +
8    "The AI injects: I can help with X, Y, Z at this branch. I don't have information " +
9    "about A or B. Honest about limits instead of hallucinating.",
10
11  needs: {
12    services: ["hooks", "metadata"],
13    models: ["Node"],
14  },
15
16  optional: {
17    extensions: ["embed", "explore", "gap-detection"],
18  },
19
20  provides: {
21    models: {},
22    routes: "./routes.js",
23    tools: true,
24    jobs: false,
25    orchestrator: false,
26    energyActions: {},
27    sessionTypes: {},
28
29    hooks: {
30      fires: [],
31      listens: ["afterLLMCall", "enrichContext"],
32    },
33
34    cli: [
35      {
36        command: "competence", scope: ["tree"],
37        description: "Knowledge boundaries at this position",
38        method: "GET",
39        endpoint: "/node/:nodeId/competence",
40      },
41    ],
42  },
43};
44
1import express from "express";
2import authenticate from "../../seed/middleware/authenticate.js";
3import { sendOk, sendError, ERR } from "../../seed/protocol.js";
4import { getCompetence } from "./core.js";
5
6const router = express.Router();
7
8// GET /node/:nodeId/competence
9router.get("/node/:nodeId/competence", authenticate, async (req, res) => {
10  try {
11    const comp = await getCompetence(req.params.nodeId);
12    if (!comp || comp.totalQueries === 0) {
13      return sendOk(res, { message: "No competence data yet.", totalQueries: 0 });
14    }
15    sendOk(res, comp);
16  } catch (err) {
17    sendError(res, 500, ERR.INTERNAL, err.message);
18  }
19});
20
21export default router;
22
1import { z } from "zod";
2import { getCompetence } from "./core.js";
3
4export default [
5  {
6    name: "competence-status",
7    description:
8      "Knowledge boundaries at this position. Shows what topics the tree can help " +
9      "with and what it has no data on. Based on accumulated query history.",
10    schema: {
11      nodeId: z.string().describe("The node to check competence for."),
12      userId: z.string().describe("Injected by server. Ignore."),
13      chatId: z.string().nullable().optional().describe("Injected by server. Ignore."),
14      sessionId: z.string().nullable().optional().describe("Injected by server. Ignore."),
15    },
16    annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
17    handler: async ({ nodeId }) => {
18      try {
19        const comp = await getCompetence(nodeId);
20        if (!comp || comp.totalQueries === 0) {
21          return { content: [{ type: "text", text: "No competence data yet. The tree needs more queries to map its knowledge boundaries." }] };
22        }
23        return {
24          content: [{
25            type: "text",
26            text: JSON.stringify({
27              totalQueries: comp.totalQueries,
28              answered: comp.answered,
29              unanswered: comp.unanswered,
30              answerRate: `${(comp.answerRate * 100).toFixed(0)}%`,
31              canHelpWith: comp.strongTopics,
32              noDataOn: comp.weakTopics,
33            }, null, 2),
34          }],
35        };
36      } catch (err) {
37        return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
38      }
39    },
40  },
41];
42

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 competence

Comments

Loading comments...

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