@kognai/tool-router 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -2
  3. package/dist/adapters/awesome-list.d.ts +10 -0
  4. package/dist/adapters/awesome-list.js +68 -0
  5. package/dist/adapters/mcp-registry.d.ts +12 -0
  6. package/dist/adapters/mcp-registry.js +51 -0
  7. package/dist/adapters/mcp-tools.d.ts +49 -0
  8. package/dist/adapters/mcp-tools.js +60 -0
  9. package/dist/adapters/openapi.d.ts +25 -0
  10. package/dist/adapters/openapi.js +107 -0
  11. package/dist/adapters/pay-skills.d.ts +13 -0
  12. package/dist/adapters/pay-skills.js +119 -0
  13. package/dist/adapters/raw.d.ts +82 -0
  14. package/dist/adapters/raw.js +5 -0
  15. package/dist/adapters/x402-discovery.d.ts +6 -0
  16. package/dist/adapters/x402-discovery.js +100 -0
  17. package/dist/catalog/file-store.d.ts +27 -0
  18. package/dist/catalog/file-store.js +102 -0
  19. package/dist/catalog/ingest.d.ts +53 -0
  20. package/dist/catalog/ingest.js +276 -0
  21. package/dist/catalog/lifecycle.d.ts +24 -0
  22. package/dist/catalog/lifecycle.js +51 -0
  23. package/dist/catalog/memory-store.d.ts +49 -0
  24. package/dist/catalog/memory-store.js +92 -0
  25. package/dist/catalog/risk.d.ts +22 -0
  26. package/dist/catalog/risk.js +73 -0
  27. package/dist/catalog/sanitise.d.ts +17 -0
  28. package/dist/catalog/sanitise.js +109 -0
  29. package/dist/classify/classifier.d.ts +12 -0
  30. package/dist/classify/classifier.js +52 -0
  31. package/dist/classify/decompose.d.ts +10 -0
  32. package/dist/classify/decompose.js +107 -0
  33. package/dist/classify/rules.d.ts +46 -0
  34. package/dist/classify/rules.js +249 -0
  35. package/dist/classify/tagger.d.ts +5 -0
  36. package/dist/classify/tagger.js +29 -0
  37. package/dist/classify/taxonomy.d.ts +31 -0
  38. package/dist/classify/taxonomy.js +221 -0
  39. package/dist/classify/text.d.ts +9 -0
  40. package/dist/classify/text.js +38 -0
  41. package/dist/eligibility/engine.d.ts +11 -0
  42. package/dist/eligibility/engine.js +19 -0
  43. package/dist/eligibility/rules.d.ts +33 -0
  44. package/dist/eligibility/rules.js +121 -0
  45. package/dist/flags.d.ts +33 -0
  46. package/dist/flags.js +34 -0
  47. package/dist/hash.d.ts +8 -0
  48. package/dist/hash.js +30 -0
  49. package/dist/ids.d.ts +17 -0
  50. package/dist/ids.js +54 -0
  51. package/dist/index.d.ts +39 -0
  52. package/dist/index.js +95 -0
  53. package/dist/money.d.ts +10 -0
  54. package/dist/money.js +41 -0
  55. package/dist/rank/confidence.d.ts +15 -0
  56. package/dist/rank/confidence.js +48 -0
  57. package/dist/rank/ranker.d.ts +34 -0
  58. package/dist/rank/ranker.js +81 -0
  59. package/dist/rank/weights.d.ts +40 -0
  60. package/dist/rank/weights.js +46 -0
  61. package/dist/retrieve/bm25.d.ts +21 -0
  62. package/dist/retrieve/bm25.js +59 -0
  63. package/dist/retrieve/capability.d.ts +1 -0
  64. package/dist/retrieve/capability.js +13 -0
  65. package/dist/retrieve/hash-embed.d.ts +4 -0
  66. package/dist/retrieve/hash-embed.js +41 -0
  67. package/dist/retrieve/hybrid.d.ts +74 -0
  68. package/dist/retrieve/hybrid.js +125 -0
  69. package/dist/retrieve/schema-fit.d.ts +9 -0
  70. package/dist/retrieve/schema-fit.js +38 -0
  71. package/dist/retrieve/tokenise.d.ts +2 -0
  72. package/dist/retrieve/tokenise.js +51 -0
  73. package/dist/retrieve/vector.d.ts +18 -0
  74. package/dist/retrieve/vector.js +47 -0
  75. package/dist/router.d.ts +37 -0
  76. package/dist/router.js +114 -0
  77. package/dist/schema/tool.schema.json +117 -0
  78. package/dist/schema/validate.d.ts +9 -0
  79. package/dist/schema/validate.js +92 -0
  80. package/dist/shortlist/generate.d.ts +21 -0
  81. package/dist/shortlist/generate.js +75 -0
  82. package/dist/types/ports.d.ts +71 -0
  83. package/dist/types/ports.js +2 -0
  84. package/dist/types/route.d.ts +121 -0
  85. package/dist/types/route.js +5 -0
  86. package/dist/types/tool.d.ts +123 -0
  87. package/dist/types/tool.js +17 -0
  88. package/package.json +39 -4
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The deterministic classifier (plan.md S2). From the request text alone: capabilities with confidence, task
3
+ * complexity, risk classes, requirement, language and the arguments the text already supplies.
4
+ *
5
+ * Risk classes are computed from the capabilities found and from cue words in the request itself; quoted material
6
+ * (an email body, a pasted page) is data and is masked before any cue is read, so text inside it can never raise or
7
+ * lower a class (threat T5). A cue preceded by a negation ("do not send") is ignored.
8
+ */
9
+ import type { RiskClass } from '../types/tool';
10
+ import type { CallerContext, CapabilityHit, Lang, Requirement, TaskComplexity } from '../types/route';
11
+ import { type ArgName, type CapabilityDef } from './taxonomy';
12
+ export interface RulesResult {
13
+ capabilities: CapabilityHit[];
14
+ task_complexity: TaskComplexity;
15
+ risk_classes: RiskClass[];
16
+ requirement: Requirement;
17
+ confidence: number;
18
+ language: Lang;
19
+ args_known: ArgName[];
20
+ reasoning_cues: string[];
21
+ data_spans: number;
22
+ }
23
+ /** Replaces quoted material and '>' quoted lines with spaces of the same length (positions stay valid). */
24
+ export declare function maskData(text: string): {
25
+ masked: string;
26
+ spans: number;
27
+ };
28
+ interface Compiled {
29
+ def: CapabilityDef;
30
+ latin: RegExp | null;
31
+ arabic: RegExp | null;
32
+ tool: RegExp | null;
33
+ }
34
+ export interface CueHit {
35
+ id: string;
36
+ at: number;
37
+ text: string;
38
+ negated: boolean;
39
+ kind: 'request' | 'tool';
40
+ }
41
+ export declare function findCues(normText: string, compiled?: Compiled[], withTool?: boolean): CueHit[];
42
+ export declare function argsOf(raw: string, norm: string): ArgName[];
43
+ export declare function classifyRules(text: string, _ctx?: Partial<CallerContext>): RulesResult;
44
+ /** Words that chain steps: "then", "and send it", "with the result". */
45
+ export declare const CHAIN_HINT: RegExp;
46
+ export {};
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CHAIN_HINT = void 0;
4
+ exports.maskData = maskData;
5
+ exports.findCues = findCues;
6
+ exports.argsOf = argsOf;
7
+ exports.classifyRules = classifyRules;
8
+ const text_1 = require("./text");
9
+ const taxonomy_1 = require("./taxonomy");
10
+ /* ── data spans (T5) ───────────────────────────────────────────────────────── */
11
+ const QUOTED = [
12
+ /"[^"\n]{15,}"/g, /'[^'\n]{25,}'/g, /“[^”]{15,}”/g, /«[^»]{15,}»/g, /```[\s\S]*?```/g,
13
+ ];
14
+ /** Replaces quoted material and '>' quoted lines with spaces of the same length (positions stay valid). */
15
+ function maskData(text) {
16
+ let spans = 0;
17
+ let t = text.replace(/^>.*$/gm, (m) => { spans++; return ' '.repeat(m.length); });
18
+ for (const re of QUOTED)
19
+ t = t.replace(re, (m) => { spans++; return ' '.repeat(m.length); });
20
+ // "the email says: ..." to the end of the line is the email's text, not the request
21
+ t = t.replace(/\b(says|said|reads|writes|wrote|dit|ecrit|يقول)\s*:\s*[^\n]{10,}/gi, (m) => { spans++; return ' '.repeat(m.length); });
22
+ return { masked: t, spans };
23
+ }
24
+ const isArabicCue = (c) => text_1.ARABIC.test(c);
25
+ const boundary = (frags) => frags.length ? new RegExp(`(?<![\\p{L}\\p{N}])(?:${frags.join('|')})(?![\\p{L}\\p{N}])`, 'gu') : null;
26
+ const COMPILED = taxonomy_1.CAPABILITIES.map((def) => {
27
+ const all = [...def.cues.en, ...def.cues.fr, ...def.cues.derja];
28
+ const latin = all.filter((c) => !isArabicCue(c)).map((c) => normaliseCue(c));
29
+ const arabic = all.filter(isArabicCue).map((c) => (0, text_1.normalise)(c));
30
+ return {
31
+ def,
32
+ latin: boundary(latin),
33
+ arabic: arabic.length ? new RegExp(`(?:${arabic.join('|')})`, 'gu') : null,
34
+ tool: def.tool_cues && def.tool_cues.length ? boundary(def.tool_cues) : null,
35
+ };
36
+ });
37
+ function normaliseCue(c) { return c.toLowerCase().normalize('NFKD').replace(/[̀-ͯ]/g, ''); }
38
+ const NEGATION = /(?:^|[^\p{L}])(not|don'?t|do not|never|without|no|stop|avoid|ne|n'|sans|jamais|matab3athch|ماتبعثش|ما|لا|بلاش|blech|bla)\s+(?:\S+\s+){0,2}$/u;
39
+ function findCues(normText, compiled = COMPILED, withTool = false) {
40
+ const hits = [];
41
+ for (const c of compiled) {
42
+ for (const [re, kind] of [[c.latin, 'request'], [c.arabic, 'request'], [withTool ? c.tool : null, 'tool']]) {
43
+ if (!re)
44
+ continue;
45
+ re.lastIndex = 0;
46
+ let m;
47
+ while ((m = re.exec(normText))) {
48
+ if (m[0].length === 0) {
49
+ re.lastIndex++;
50
+ continue;
51
+ }
52
+ const before = normText.slice(Math.max(0, m.index - 30), m.index);
53
+ hits.push({ id: c.def.id, at: m.index, text: m[0], negated: NEGATION.test(before), kind });
54
+ }
55
+ }
56
+ }
57
+ return hits.sort((a, b) => a.at - b.at);
58
+ }
59
+ /* ── arguments the text supplies ───────────────────────────────────────────── */
60
+ const KNOWN_PLACES = /\b(paris|tunis|sfax|sousse|bizerte|nabeul|hammamet|monastir|kairouan|gabes|djerba|london|new york|berlin|madrid|rome|dubai|cairo|casablanca|algiers|alger|marseille|lyon|tokyo|montreal|brussels|bruxelles|geneva|geneve)\b|تونس|صفاقس|سوسه|باريس|نابل/;
61
+ function argsOf(raw, norm) {
62
+ const a = new Set(['query']);
63
+ if (KNOWN_PLACES.test(norm) || /\b(in|at|for|near|to)\s+(?:the\s+)?[A-Z][\p{L}'-]{2,}/u.test(raw) || /\b(a|à|au|en|pres de)\s+[A-Z][\p{L}'-]{2,}/u.test(raw) || /\bfi\s+[a-z]{3,}/.test(norm) || /في\s+\S{3,}/.test(norm))
64
+ a.add('location');
65
+ if (/https?:\/\/\S+/i.test(raw))
66
+ a.add('url');
67
+ if (/\b0x[0-9a-fA-F]{40}\b|\b[1-9A-HJ-NP-Za-km-z]{32,44}\b/.test(raw)) {
68
+ a.add('address');
69
+ a.add('token');
70
+ }
71
+ if (/\$[A-Za-z]{2,10}\b|\b(bitcoin|btc|ethereum|eth|solana|sol|bonk|usdc|pepe|doge)\b/i.test(raw))
72
+ a.add('token');
73
+ if (/\b\d+(?:[.,]\d+)?\s?(usdc|usdt|sol|eth|btc|kgc|dollars?|euros?|dinars?|tnd|usd|eur)\b|[$€]\s?\d+/i.test(raw))
74
+ a.add('amount');
75
+ if (/\b(this|the|my|attached|ce|cette|mon|ma|le|la)\s+(file|fichier|pdf|excel|spreadsheet|sheet|document|doc|photo|image|picture|screenshot|receipt|ticket|video|vidéo|audio|recording|vocal|voice note|csv|xlsx|deck)\b/i.test(raw) || /\.(pdf|xlsx?|csv|docx?|pptx|png|jpe?g|mp4|mp3|wav|m4a)\b/i.test(raw))
76
+ a.add('file_ref');
77
+ if (/\b(to|a|à)\s+[A-Z][\p{L}]+\b|@\w+|\b[\w.+-]+@[\w-]+\.[\w.]+\b/u.test(raw) || /\b(my|mon|ma)\s+(mother|mom|father|dad|brother|sister|boss|wife|husband|friend|team|mere|pere|frere|soeur|chef)\b/i.test(raw))
78
+ a.add('recipient');
79
+ if (/\b(today|tomorrow|tonight|this (week|weekend|month)|next (week|monday|tuesday|wednesday|thursday|friday)|aujourd'?hui|demain|cette semaine|lyoum|ghodwa|\d{4}-\d{2}-\d{2})\b|اليوم|غدوه/i.test(raw))
80
+ a.add('date');
81
+ return [...a];
82
+ }
83
+ /* ── conflict rules between capabilities ───────────────────────────────────── */
84
+ const MAKE = /(?<![\p{L}])(make|create|generate|draw|design|produce|render|build|fais|fait|cree|creer|genere|generer|dessine|produis|realise|a3mel|a3melli|a3mli|اعمل|اعمللي|عمللي|اصنع|ارسم)(?![\p{L}])/u;
85
+ function resolveConflicts(ids, norm, args) {
86
+ const make = MAKE.test(norm);
87
+ const has = (x) => ids.has(x);
88
+ if (has('image.generate') && (has('image.understand') || has('image.edit') || has('ocr.extract')) && !make)
89
+ ids.delete('image.generate');
90
+ if (has('image.generate') && args.includes('file_ref') && !make && (has('doc.read') || has('sheet.analyse')))
91
+ ids.delete('image.generate');
92
+ if (has('video.generate') && has('video.edit') && !make && args.includes('file_ref'))
93
+ ids.delete('video.generate');
94
+ if (has('sheet.analyse') && has('sheet.edit'))
95
+ ids.delete('sheet.analyse');
96
+ if (has('mail.search'))
97
+ ids.delete('mail.read');
98
+ if (has('wallet.transfer') && has('payment.pay'))
99
+ ids.delete('payment.pay');
100
+ if (has('audio.transcribe') && has('voice.speak') && !/\b(voice.?over|text.to.speech|tts|narrat\w*|read (it|this) aloud)\b/.test(norm))
101
+ ids.delete('voice.speak');
102
+ if (has('token.trade') && has('token.price') && !/\b(price|prix|soum)\b/.test(norm))
103
+ ids.delete('token.price');
104
+ if (has('token.analyse') && has('token.price') && !/\b(price|prix|soum)\b/.test(norm))
105
+ ids.delete('token.price');
106
+ if (has('market.signals') && has('news.search'))
107
+ ids.delete('news.search');
108
+ if (has('social.post') && has('social.content') && !/\b(write|draft|ecris|redige|ekteb)\b/.test(norm))
109
+ ids.delete('social.content');
110
+ if (has('learning.program') && has('video.generate') && /\b(educational|educative|lesson|cours)\b/.test(norm))
111
+ ids.delete('learning.program');
112
+ if (has('text.write') && (has('mail.send') || has('social.content')))
113
+ ids.delete('text.write');
114
+ if (has('deck.make') && has('sheet.analyse') && !/\b(excel|spreadsheet|csv|xlsx|tableur)\b/.test(norm))
115
+ ids.delete('sheet.analyse');
116
+ if (has('literature.search') && has('news.search') && !/\bnews\b/.test(norm))
117
+ ids.delete('news.search');
118
+ if (has('character.design') && !has('video.generate') && !has('image.generate') && !make)
119
+ ids.delete('character.design');
120
+ if (has('voice.speak') && !has('video.generate') && /\btalking\b/.test(norm) && !/\b(voice|tts|narrat)/.test(norm))
121
+ ids.delete('voice.speak');
122
+ if (has('web.search') && ids.size > 1 && !/\b(search|google|lawej|cherche|recherche)\b/.test(norm))
123
+ ids.delete('web.search');
124
+ if (has('code.repo') && has('infra.deploy') && /\bdeploy\b/.test(norm) && !/\b(github|gitlab|repo|pull request)\b/.test(norm))
125
+ ids.delete('code.repo');
126
+ }
127
+ /** A risky verb with no capability for it becomes its own step, so it is planned and then refused, never ignored. */
128
+ function addRiskySteps(ids, norm, live) {
129
+ const neg = (re) => { const m = re.exec(norm); return !m || NEGATION.test(norm.slice(Math.max(0, m.index - 30), m.index)) ? null : m; };
130
+ const hasRisk = (cls) => [...ids].some((i) => (0, taxonomy_1.capabilityById)(i).default_risk.includes(cls));
131
+ if (!hasRisk('payment')) {
132
+ if (neg(/(?<![\p{L}])(buy|sell|swap|trade|achete|vends|echange)(?![\p{L}])/u))
133
+ ids.add('token.trade');
134
+ else if (neg(/(?<![\p{L}])(pay|paye[rz]?|khalles|خلص)(?![\p{L}]).{0,30}(bill|invoice|facture|subscription|rent|loyer|فاتوره)/u))
135
+ ids.add('payment.pay');
136
+ else if (neg(/(?<![\p{L}])(wire|transfer|transfere|virement|send|envoie|ab3ath)(?![\p{L}]).{0,25}(\d|money|funds|usdc|usdt|sol|eth|btc|euros?|dinars?|dollars?|flous|فلوس)/u))
137
+ ids.add('wallet.transfer');
138
+ }
139
+ if (!hasRisk('destructive') && neg(/(?<![\p{L}])(delete|wipe|erase|destroy|supprime[rz]?|efface[rz]?|fasa5|فسخ|امسح)(?![\p{L}])/u))
140
+ ids.add('file.delete');
141
+ if (!hasRisk('publish_or_message') && !hasRisk('payment')) {
142
+ if (neg(/(?<![\p{L}])(post|publish|publie[rz]?|tweet|ensher|انشر)(?![\p{L}])/u) && !/\b(posts?|publications?) (on|sur|from)\b/.test(norm.replace(/\bmy (last|latest|recent) /, '')))
143
+ ids.add('social.post');
144
+ else if (neg(/(?<![\p{L}])(send|forward|envoie|ab3ath)(?![\p{L}]).{0,20}(to|a|à|l) /u))
145
+ ids.add(ids.has('mail.read') || ids.has('mail.search') || /e.?mail|mail/.test(norm) ? 'mail.send' : 'message.send');
146
+ }
147
+ void live;
148
+ }
149
+ /* ── risk cue words in the request itself ──────────────────────────────────── */
150
+ const RISK_WORDS = [
151
+ ['payment', /(?<![\p{L}])(pay|paye[rz]?|payment|transfer|transfere|virement|wire|send \d|envoie \d|buy|sell|swap|trade|achete|vends|ab3ath flous|اشري|خلص)(?![\p{L}])/u],
152
+ ['destructive', /(?<![\p{L}])(delete|wipe|erase|destroy|supprime[rz]?|efface[rz]?|fasa5|فسخ|امسح)(?![\p{L}])/u],
153
+ ['publish_or_message', /(?<![\p{L}])(send (an? )?(e.?mail|message|sms|text|dm)|send (it|this|that|them|the [\p{L}]+) to|forward (it|this|them|the [\p{L}]+)|post (it|this|on|to)|publish|publie[rz]?|tweet (it|this)|reply to|ensher|انشر)(?![\p{L}])/u],
154
+ ['sensitive_data', /(?<![\p{L}])(my (e.?mails?|inbox|mails?|calendar|agenda|drive|files|messages|wallet|balance|bank|passwords?|contacts|dms)|mes (mails|courriels|fichiers|messages)|mon (agenda|solde)|متاعي)(?![\p{L}])/u],
155
+ ];
156
+ const GREETING = /^(hi|hello|hey|bonjour|bonsoir|salut|merci|thanks?|thank you|ok thanks|aslema|ahla|عسلامه|اهلا|مرحبا|شكرا)\b/;
157
+ const CAPABILITY_Q = /^(can you|could you|do you|tu peux|tu sais|est.ce que tu (peux|sais)|vous pouvez|tnajem|tnajjem|تنجم)\b/;
158
+ const CLARIFY = /^(do it|fix (it|this|that)|help( me)?|again|the same|continue|go|ok(ay)?|yes|no|that one|this|it|fais.le|aide.moi|a3melha|زيد)[\s.!?]*$/;
159
+ function classifyRules(text, _ctx) {
160
+ const raw = String(text ?? '');
161
+ const { masked, spans } = maskData(raw);
162
+ const norm = (0, text_1.normalise)(masked);
163
+ const language = (0, text_1.detectLanguage)(raw);
164
+ const args = argsOf(masked, norm);
165
+ const hits = findCues(norm);
166
+ const live = hits.filter((h) => !h.negated);
167
+ const ids = new Set(live.map((h) => h.id));
168
+ resolveConflicts(ids, norm, args);
169
+ addRiskySteps(ids, norm, live);
170
+ // model work on an attached file needs the file read first
171
+ if (args.includes('file_ref') && ids.size && [...ids].every((i) => !(0, taxonomy_1.capabilityById)(i).needs_tool))
172
+ ids.add('doc.read');
173
+ const firstAt = new Map();
174
+ const count = new Map();
175
+ for (const h of live)
176
+ if (ids.has(h.id)) {
177
+ if (!firstAt.has(h.id))
178
+ firstAt.set(h.id, h.at);
179
+ count.set(h.id, (count.get(h.id) || 0) + 1);
180
+ }
181
+ const capabilities = [...ids]
182
+ .sort((a, b) => ((firstAt.get(a) ?? norm.length) - (firstAt.get(b) ?? norm.length)))
183
+ .map((id) => { const h = live.find((x) => x.id === id); return { id, confidence: h ? Math.min(0.95, 0.75 + 0.1 * ((count.get(id) || 1) - 1)) : 0.6, span: h ? h.text : '(implied by a risky verb or an attached file)' }; });
184
+ const reasoning_cues = taxonomy_1.REASONING_CUES.filter((c) => new RegExp(`(?<![\\p{L}\\p{N}])(?:${normaliseCue(c)})(?![\\p{L}\\p{N}])`, 'u').test(norm) || (text_1.ARABIC.test(c) && norm.includes((0, text_1.normalise)(c))));
185
+ // risk: capability defaults plus cue words, never from quoted data, never removed
186
+ const risk = new Set();
187
+ const defs = capabilities.map((c) => (0, taxonomy_1.capabilityById)(c.id));
188
+ for (const d of defs)
189
+ for (const r of d.default_risk)
190
+ risk.add(r);
191
+ for (const [cls, re] of RISK_WORDS) {
192
+ const m = re.exec(norm);
193
+ if (m && !NEGATION.test(norm.slice(Math.max(0, m.index - 30), m.index)))
194
+ risk.add(cls);
195
+ }
196
+ const toolDefs = defs.filter((d) => d.needs_tool);
197
+ if (toolDefs.some((d) => d.default_risk.includes('network')))
198
+ risk.add('network');
199
+ const strict = ['data_modification', 'publish_or_message', 'payment', 'destructive', 'privileged_infra'];
200
+ if (strict.some((c) => risk.has(c)))
201
+ risk.delete('read_only');
202
+ // requirement
203
+ const words = norm.split(/\s+/).filter(Boolean);
204
+ let requirement;
205
+ let confidence;
206
+ const capabilityQuestion = CAPABILITY_Q.test(norm) && /\?\s*$/.test(raw.trim()) && words.length <= 6;
207
+ if (capabilityQuestion) {
208
+ requirement = 'no_tool';
209
+ confidence = 0.7;
210
+ }
211
+ else if (toolDefs.length) {
212
+ requirement = 'tool';
213
+ confidence = Math.min(...capabilities.filter((c) => (0, taxonomy_1.capabilityById)(c.id).needs_tool).map((c) => c.confidence));
214
+ }
215
+ else if (capabilities.length) {
216
+ requirement = 'no_tool';
217
+ confidence = 0.8;
218
+ }
219
+ else if (GREETING.test(norm)) {
220
+ requirement = 'no_tool';
221
+ confidence = 0.8;
222
+ }
223
+ else if (CLARIFY.test(norm) || words.length < 2) {
224
+ requirement = 'clarify';
225
+ confidence = 0.4;
226
+ }
227
+ else {
228
+ requirement = 'no_tool';
229
+ confidence = /\?|^(what|who|when|where|why|how|is|are|can|c.est|qu|quoi|comment|pourquoi|chnowa|kifech|3lech|شنوه|كيفاش|علاش)/.test(norm) ? 0.6 : 0.5;
230
+ }
231
+ // complexity
232
+ const missing = toolDefs.some((d) => (d.args || []).some((a) => !args.includes(a)));
233
+ let task_complexity;
234
+ if (reasoning_cues.length)
235
+ task_complexity = 'REASONING';
236
+ else if (capabilities.length >= 3 || (toolDefs.length >= 1 && exports.CHAIN_HINT.test(norm) && capabilities.length >= 2))
237
+ task_complexity = 'COMPLEX';
238
+ else if (capabilities.length === 2 || (requirement === 'tool' && missing))
239
+ task_complexity = 'MEDIUM';
240
+ else
241
+ task_complexity = 'SIMPLE';
242
+ const order = ['read_only', 'network', 'data_modification', 'publish_or_message', 'payment', 'destructive', 'sensitive_data', 'privileged_infra'];
243
+ return {
244
+ capabilities, task_complexity, risk_classes: order.filter((c) => risk.has(c)), requirement,
245
+ confidence: Number(confidence.toFixed(2)), language, args_known: args, reasoning_cues, data_spans: spans,
246
+ };
247
+ }
248
+ /** Words that chain steps: "then", "and send it", "with the result". */
249
+ exports.CHAIN_HINT = /(?<![\p{L}])(then|and then|after that|afterwards|with (it|the result|that)|using (it|the result)|and (send|post|share|publish|email) it|puis|ensuite|apres ca|et (l.envoie|le publie)|ba3d|w ba3d|بعد|ومن بعد)(?![\p{L}])/u;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * A capability is tagged when one of its tool cues appears, or when two different request cues do: one generic word
3
+ * ("temperature", "transfer", "server") is not enough evidence on its own.
4
+ */
5
+ export declare function tagCapabilities(toolText: string): string[];
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tagCapabilities = tagCapabilities;
4
+ /**
5
+ * Capability tagging of tool text at ingestion: the request cues plus each capability's tool_cues, matched on the
6
+ * sanitised name, summary and description. Deterministic; tags only add evidence for retrieval (the capability
7
+ * signal), they never change a tool's risk or trust.
8
+ */
9
+ const text_1 = require("./text");
10
+ const rules_1 = require("./rules");
11
+ /**
12
+ * A capability is tagged when one of its tool cues appears, or when two different request cues do: one generic word
13
+ * ("temperature", "transfer", "server") is not enough evidence on its own.
14
+ */
15
+ function tagCapabilities(toolText) {
16
+ const hits = (0, rules_1.findCues)((0, text_1.normalise)(toolText), undefined, true).filter((h) => !h.negated);
17
+ const tool = new Set();
18
+ const req = new Map();
19
+ for (const h of hits) {
20
+ if (h.kind === 'tool')
21
+ tool.add(h.id);
22
+ else
23
+ (req.get(h.id) || req.set(h.id, new Set()).get(h.id)).add(h.text);
24
+ }
25
+ for (const [id, texts] of req)
26
+ if (texts.size >= 2)
27
+ tool.add(id);
28
+ return [...tool].sort();
29
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Capability taxonomy (plan.md S2). Each capability has cue phrases in English, French and Tunisian derja (Latin and
3
+ * Arabic script), a default risk, whether a tool is needed at all (false = model work: summarise given text,
4
+ * synthesise, translate), the Kognai product that covers it (ADR-008), and the arguments a tool needs.
5
+ *
6
+ * Cue strings are regular expression fragments matched on normalised text (classify/text.ts): Latin cues with word
7
+ * boundaries, Arabic cues as substrings (Arabic script has no \b). The same cues tag tool descriptions at ingestion
8
+ * (classify/tagger.ts), with `tool_cues` adding words that appear in tool text but not in requests.
9
+ */
10
+ import type { RiskClass } from '../types/tool';
11
+ export type ArgName = 'location' | 'url' | 'address' | 'amount' | 'file_ref' | 'token' | 'recipient' | 'date' | 'query';
12
+ export interface CapabilityDef {
13
+ id: string;
14
+ family: string;
15
+ cues: {
16
+ en: string[];
17
+ fr: string[];
18
+ derja: string[];
19
+ };
20
+ tool_cues?: string[];
21
+ default_risk: RiskClass[];
22
+ needs_tool: boolean;
23
+ product_id?: string;
24
+ args?: ArgName[];
25
+ }
26
+ export declare const CAPABILITIES: CapabilityDef[];
27
+ export declare const capabilityById: (id: string) => CapabilityDef | undefined;
28
+ export declare const CAPABILITY_IDS: string[];
29
+ export declare const familyOf: (id: string) => string | null;
30
+ /** Words that ask for analysis, comparison or synthesis: REASONING whatever the tool count (plan.md S2). */
31
+ export declare const REASONING_CUES: string[];