praana 0.5.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.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/bin/praana.js +17 -0
- package/bin/pran.js +17 -0
- package/dist/app-banner.d.ts +11 -0
- package/dist/app-banner.js +161 -0
- package/dist/app-controller.d.ts +44 -0
- package/dist/app-controller.js +143 -0
- package/dist/app-identity.d.ts +18 -0
- package/dist/app-identity.js +52 -0
- package/dist/auto-compact.d.ts +16 -0
- package/dist/auto-compact.js +101 -0
- package/dist/cli-args.d.ts +14 -0
- package/dist/cli-args.js +69 -0
- package/dist/compile-classic.d.ts +21 -0
- package/dist/compile-classic.js +106 -0
- package/dist/compiler.d.ts +75 -0
- package/dist/compiler.js +406 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +433 -0
- package/dist/context-engine/activity-log.d.ts +9 -0
- package/dist/context-engine/activity-log.js +109 -0
- package/dist/context-engine/artifact-store.d.ts +32 -0
- package/dist/context-engine/artifact-store.js +272 -0
- package/dist/context-engine/bm25.d.ts +3 -0
- package/dist/context-engine/bm25.js +32 -0
- package/dist/context-engine/checkpoint.d.ts +34 -0
- package/dist/context-engine/checkpoint.js +430 -0
- package/dist/context-engine/classify.d.ts +3 -0
- package/dist/context-engine/classify.js +60 -0
- package/dist/context-engine/db.d.ts +73 -0
- package/dist/context-engine/db.js +505 -0
- package/dist/context-engine/distiller.d.ts +30 -0
- package/dist/context-engine/distiller.js +67 -0
- package/dist/context-engine/engine-compiler.d.ts +23 -0
- package/dist/context-engine/engine-compiler.js +297 -0
- package/dist/context-engine/error-tracker.d.ts +21 -0
- package/dist/context-engine/error-tracker.js +74 -0
- package/dist/context-engine/event-lineage.d.ts +26 -0
- package/dist/context-engine/event-lineage.js +120 -0
- package/dist/context-engine/extraction.d.ts +26 -0
- package/dist/context-engine/extraction.js +83 -0
- package/dist/context-engine/index.d.ts +82 -0
- package/dist/context-engine/index.js +238 -0
- package/dist/context-engine/scoring.d.ts +13 -0
- package/dist/context-engine/scoring.js +47 -0
- package/dist/context-engine/state-snapshot.d.ts +8 -0
- package/dist/context-engine/state-snapshot.js +50 -0
- package/dist/context-engine/summarize.d.ts +6 -0
- package/dist/context-engine/summarize.js +32 -0
- package/dist/context-engine/telemetry.d.ts +25 -0
- package/dist/context-engine/telemetry.js +64 -0
- package/dist/context-engine/turn-digest.d.ts +50 -0
- package/dist/context-engine/turn-digest.js +250 -0
- package/dist/context-engine/turn-ledger.d.ts +18 -0
- package/dist/context-engine/turn-ledger.js +184 -0
- package/dist/context-engine/turn-recorder.d.ts +24 -0
- package/dist/context-engine/turn-recorder.js +88 -0
- package/dist/context-engine/types.d.ts +201 -0
- package/dist/context-engine/types.js +4 -0
- package/dist/context-pressure.d.ts +19 -0
- package/dist/context-pressure.js +36 -0
- package/dist/distillers/generic.d.ts +14 -0
- package/dist/distillers/generic.js +93 -0
- package/dist/distillers/git-diff.d.ts +8 -0
- package/dist/distillers/git-diff.js +119 -0
- package/dist/distillers/index.d.ts +2 -0
- package/dist/distillers/index.js +16 -0
- package/dist/distillers/npm-test.d.ts +8 -0
- package/dist/distillers/npm-test.js +50 -0
- package/dist/distillers/rg-results.d.ts +8 -0
- package/dist/distillers/rg-results.js +28 -0
- package/dist/distillers/tsc-errors.d.ts +8 -0
- package/dist/distillers/tsc-errors.js +52 -0
- package/dist/event-log.d.ts +56 -0
- package/dist/event-log.js +214 -0
- package/dist/llm.d.ts +29 -0
- package/dist/llm.js +155 -0
- package/dist/logger.d.ts +94 -0
- package/dist/logger.js +287 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +54 -0
- package/dist/memory/confidence.d.ts +7 -0
- package/dist/memory/confidence.js +37 -0
- package/dist/memory/consolidation.d.ts +26 -0
- package/dist/memory/consolidation.js +166 -0
- package/dist/memory/db.d.ts +40 -0
- package/dist/memory/db.js +283 -0
- package/dist/memory/dedup.d.ts +6 -0
- package/dist/memory/dedup.js +50 -0
- package/dist/memory/embedder-factory.d.ts +3 -0
- package/dist/memory/embedder-factory.js +81 -0
- package/dist/memory/embeddings.d.ts +15 -0
- package/dist/memory/embeddings.js +67 -0
- package/dist/memory/index.d.ts +9 -0
- package/dist/memory/index.js +11 -0
- package/dist/memory/ollama-summarizer.d.ts +19 -0
- package/dist/memory/ollama-summarizer.js +72 -0
- package/dist/memory/openai-summarizer.d.ts +21 -0
- package/dist/memory/openai-summarizer.js +51 -0
- package/dist/memory/store.d.ts +61 -0
- package/dist/memory/store.js +502 -0
- package/dist/memory/summarizer-factory.d.ts +3 -0
- package/dist/memory/summarizer-factory.js +69 -0
- package/dist/memory/summarizer.d.ts +4 -0
- package/dist/memory/summarizer.js +112 -0
- package/dist/memory/types.d.ts +87 -0
- package/dist/memory/types.js +17 -0
- package/dist/model-context.d.ts +15 -0
- package/dist/model-context.js +212 -0
- package/dist/project-detector.d.ts +37 -0
- package/dist/project-detector.js +604 -0
- package/dist/render.d.ts +15 -0
- package/dist/render.js +46 -0
- package/dist/session.d.ts +118 -0
- package/dist/session.js +809 -0
- package/dist/skills/index.d.ts +69 -0
- package/dist/skills/index.js +885 -0
- package/dist/skills/types.d.ts +93 -0
- package/dist/skills/types.js +8 -0
- package/dist/slash-commands.d.ts +14 -0
- package/dist/slash-commands.js +301 -0
- package/dist/state-graph.d.ts +38 -0
- package/dist/state-graph.js +255 -0
- package/dist/status-bar.d.ts +54 -0
- package/dist/status-bar.js +184 -0
- package/dist/thinking-display.d.ts +21 -0
- package/dist/thinking-display.js +37 -0
- package/dist/tool-summary.d.ts +4 -0
- package/dist/tool-summary.js +67 -0
- package/dist/tools/index.d.ts +925 -0
- package/dist/tools/index.js +86 -0
- package/dist/tools/knowledge.d.ts +140 -0
- package/dist/tools/knowledge.js +260 -0
- package/dist/tools/memory.d.ts +39 -0
- package/dist/tools/memory.js +300 -0
- package/dist/tools/search-code.d.ts +134 -0
- package/dist/tools/search-code.js +390 -0
- package/dist/tools/system.d.ts +16 -0
- package/dist/tools/system.js +499 -0
- package/dist/tools/tool-def.d.ts +6 -0
- package/dist/tools/tool-def.js +3 -0
- package/dist/turn-control.d.ts +51 -0
- package/dist/turn-control.js +210 -0
- package/dist/turn.d.ts +20 -0
- package/dist/turn.js +624 -0
- package/dist/types.d.ts +233 -0
- package/dist/types.js +4 -0
- package/dist/ui/readline-ui.d.ts +2 -0
- package/dist/ui/readline-ui.js +176 -0
- package/dist/ui/tui/app.d.ts +13 -0
- package/dist/ui/tui/app.js +270 -0
- package/dist/ui/tui/busy-indicator.d.ts +2 -0
- package/dist/ui/tui/busy-indicator.js +13 -0
- package/dist/ui/tui/components/gutter-rule.d.ts +5 -0
- package/dist/ui/tui/components/gutter-rule.js +9 -0
- package/dist/ui/tui/components/inline-tool-row.d.ts +10 -0
- package/dist/ui/tui/components/inline-tool-row.js +8 -0
- package/dist/ui/tui/components/prompt-input.d.ts +20 -0
- package/dist/ui/tui/components/prompt-input.js +120 -0
- package/dist/ui/tui/components/system-line.d.ts +5 -0
- package/dist/ui/tui/components/system-line.js +6 -0
- package/dist/ui/tui/components/thinking-block.d.ts +11 -0
- package/dist/ui/tui/components/thinking-block.js +31 -0
- package/dist/ui/tui/components/toast-line.d.ts +4 -0
- package/dist/ui/tui/components/toast-line.js +8 -0
- package/dist/ui/tui/components/tool-result-line.d.ts +5 -0
- package/dist/ui/tui/components/tool-result-line.js +6 -0
- package/dist/ui/tui/components/turn-footer.d.ts +5 -0
- package/dist/ui/tui/components/turn-footer.js +7 -0
- package/dist/ui/tui/components/user-block.d.ts +6 -0
- package/dist/ui/tui/components/user-block.js +6 -0
- package/dist/ui/tui/logo-banner.d.ts +5 -0
- package/dist/ui/tui/logo-banner.js +8 -0
- package/dist/ui/tui/markdown-render.d.ts +16 -0
- package/dist/ui/tui/markdown-render.js +218 -0
- package/dist/ui/tui/palette.d.ts +12 -0
- package/dist/ui/tui/palette.js +13 -0
- package/dist/ui/tui/reasoning-summary.d.ts +12 -0
- package/dist/ui/tui/reasoning-summary.js +27 -0
- package/dist/ui/tui/reducer.d.ts +92 -0
- package/dist/ui/tui/reducer.js +260 -0
- package/dist/ui/tui/run.d.ts +3 -0
- package/dist/ui/tui/run.js +40 -0
- package/dist/ui/tui/sink.d.ts +4 -0
- package/dist/ui/tui/sink.js +89 -0
- package/dist/ui/tui/status-bar-view.d.ts +5 -0
- package/dist/ui/tui/status-bar-view.js +44 -0
- package/dist/ui/tui/terminal-height.d.ts +12 -0
- package/dist/ui/tui/terminal-height.js +20 -0
- package/dist/ui/tui/terminal-width.d.ts +2 -0
- package/dist/ui/tui/terminal-width.js +5 -0
- package/dist/ui/tui/tool-display.d.ts +23 -0
- package/dist/ui/tui/tool-display.js +217 -0
- package/dist/ui/tui/transcript-line.d.ts +12 -0
- package/dist/ui/tui/transcript-line.js +43 -0
- package/dist/ui/tui/transcript-replay.d.ts +12 -0
- package/dist/ui/tui/transcript-replay.js +117 -0
- package/dist/ui-events.d.ts +39 -0
- package/dist/ui-events.js +33 -0
- package/dist/ui.d.ts +77 -0
- package/dist/ui.js +179 -0
- package/package.json +73 -0
- package/praana.config.example.toml +231 -0
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import yaml from "js-yaml";
|
|
6
|
+
import { getAppLogger } from "../logger.js";
|
|
7
|
+
// ========================================================================
|
|
8
|
+
// Helpers
|
|
9
|
+
// ========================================================================
|
|
10
|
+
function findGitRoot(cwd) {
|
|
11
|
+
try {
|
|
12
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
13
|
+
cwd,
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
16
|
+
}).trim();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return cwd;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function expandHome(p) {
|
|
23
|
+
return p.startsWith("~/") ? p.replace(/^~\//, `${homedir()}/`) : p;
|
|
24
|
+
}
|
|
25
|
+
function isDirectory(path) {
|
|
26
|
+
try {
|
|
27
|
+
return statSync(path).isDirectory();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const SKIP_ALLOWLIST = new Set([".agents", ".aria", ".praana", ".cursor", ".claude"]);
|
|
34
|
+
function shouldSkipDir(dirName) {
|
|
35
|
+
if (dirName === ".git" || dirName === "node_modules")
|
|
36
|
+
return true;
|
|
37
|
+
if (dirName.startsWith(".") && !SKIP_ALLOWLIST.has(dirName))
|
|
38
|
+
return true;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
// ========================================================================
|
|
42
|
+
// SKILL.md Parsing
|
|
43
|
+
// ========================================================================
|
|
44
|
+
export function parseSkillMdContent(content, filePath) {
|
|
45
|
+
const trimmed = content.trim();
|
|
46
|
+
if (!trimmed.startsWith("---"))
|
|
47
|
+
return null;
|
|
48
|
+
const endIdx = trimmed.indexOf("---", 3);
|
|
49
|
+
if (endIdx === -1)
|
|
50
|
+
return null;
|
|
51
|
+
const yamlBlock = trimmed.slice(3, endIdx).trim();
|
|
52
|
+
const body = trimmed.slice(endIdx + 3).trim();
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = yaml.load(yamlBlock);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (!parsed || typeof parsed !== "object")
|
|
61
|
+
return null;
|
|
62
|
+
const name = String(parsed.name ?? "");
|
|
63
|
+
const description = String(parsed.description ?? "");
|
|
64
|
+
if (!name || !description)
|
|
65
|
+
return null;
|
|
66
|
+
const metadata = {
|
|
67
|
+
name,
|
|
68
|
+
description,
|
|
69
|
+
license: parsed.license ? String(parsed.license) : undefined,
|
|
70
|
+
compatibility: parsed.compatibility ? String(parsed.compatibility) : undefined,
|
|
71
|
+
metadata: parsed.metadata,
|
|
72
|
+
allowedTools: parsed["allowed-tools"] ? String(parsed["allowed-tools"]) : undefined,
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
name,
|
|
76
|
+
description,
|
|
77
|
+
location: filePath,
|
|
78
|
+
directory: dirname(filePath),
|
|
79
|
+
body,
|
|
80
|
+
metadata,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function parseSkillMdFile(filePath) {
|
|
84
|
+
try {
|
|
85
|
+
const content = readFileSync(filePath, "utf-8");
|
|
86
|
+
return parseSkillMdContent(content, filePath);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// ========================================================================
|
|
93
|
+
// skills-meta.json Loading
|
|
94
|
+
// ========================================================================
|
|
95
|
+
function loadSkillsMeta(path) {
|
|
96
|
+
try {
|
|
97
|
+
if (!existsSync(path))
|
|
98
|
+
return {};
|
|
99
|
+
const raw = readFileSync(path, "utf-8");
|
|
100
|
+
return JSON.parse(raw);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function getSkillsMetaPaths(cwd) {
|
|
107
|
+
const gitRoot = findGitRoot(cwd);
|
|
108
|
+
const home = homedir();
|
|
109
|
+
return [
|
|
110
|
+
join(gitRoot, ".praana", "skills-meta.json"),
|
|
111
|
+
join(gitRoot, ".aria", "skills-meta.json"),
|
|
112
|
+
expandHome("~/.praana/skills-meta.json"),
|
|
113
|
+
expandHome("~/.aria/skills-meta.json"),
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
export function loadMergedSkillsMeta(cwd) {
|
|
117
|
+
let merged = {};
|
|
118
|
+
for (const path of getSkillsMetaPaths(cwd)) {
|
|
119
|
+
merged = { ...merged, ...loadSkillsMeta(path) };
|
|
120
|
+
}
|
|
121
|
+
return merged;
|
|
122
|
+
}
|
|
123
|
+
// ========================================================================
|
|
124
|
+
// Discovery
|
|
125
|
+
// ========================================================================
|
|
126
|
+
function getSkillSearchPaths(cwd) {
|
|
127
|
+
const gitRoot = findGitRoot(cwd);
|
|
128
|
+
const home = homedir();
|
|
129
|
+
const projectPaths = [
|
|
130
|
+
join(gitRoot, ".agents", "skills"),
|
|
131
|
+
join(gitRoot, ".praana", "skills"),
|
|
132
|
+
join(gitRoot, ".aria", "skills"),
|
|
133
|
+
join(gitRoot, ".cursor", "skills"),
|
|
134
|
+
join(gitRoot, "skills"),
|
|
135
|
+
];
|
|
136
|
+
const userPaths = [
|
|
137
|
+
join(home, ".agents", "skills"),
|
|
138
|
+
join(home, ".praana", "skills"),
|
|
139
|
+
join(home, ".aria", "skills"),
|
|
140
|
+
join(home, ".claude", "skills"),
|
|
141
|
+
];
|
|
142
|
+
return [...projectPaths, ...userPaths];
|
|
143
|
+
}
|
|
144
|
+
function scanSkillsDir(skillsDir, maxDepth) {
|
|
145
|
+
if (!existsSync(skillsDir))
|
|
146
|
+
return [];
|
|
147
|
+
const results = [];
|
|
148
|
+
function scan(dir, depth) {
|
|
149
|
+
if (depth > maxDepth)
|
|
150
|
+
return;
|
|
151
|
+
let entries;
|
|
152
|
+
try {
|
|
153
|
+
entries = readdirSync(dir);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
if (shouldSkipDir(entry))
|
|
160
|
+
continue;
|
|
161
|
+
const fullPath = join(dir, entry);
|
|
162
|
+
const skillFile = join(fullPath, "SKILL.md");
|
|
163
|
+
if (isDirectory(fullPath) && existsSync(skillFile)) {
|
|
164
|
+
const skill = parseSkillMdFile(skillFile);
|
|
165
|
+
if (skill) {
|
|
166
|
+
if (skill.name !== entry) {
|
|
167
|
+
getAppLogger().child("skills").warn(`Name mismatch: "${skill.name}" in ${skillFile}, directory is "${entry}"`);
|
|
168
|
+
}
|
|
169
|
+
results.push(skill);
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (entry.endsWith(".md") && !isDirectory(fullPath)) {
|
|
174
|
+
const skill = parseSkillMdFile(fullPath);
|
|
175
|
+
if (skill)
|
|
176
|
+
results.push(skill);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (isDirectory(fullPath)) {
|
|
180
|
+
scan(fullPath, depth + 1);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
scan(skillsDir, 0);
|
|
185
|
+
return results;
|
|
186
|
+
}
|
|
187
|
+
export function discoverSkills(cwd, maxDepth = 6, _paths) {
|
|
188
|
+
if (_paths) {
|
|
189
|
+
const merged = new Map();
|
|
190
|
+
for (const dir of _paths) {
|
|
191
|
+
for (const skill of scanSkillsDir(dir, maxDepth)) {
|
|
192
|
+
merged.set(skill.name, skill);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
196
|
+
}
|
|
197
|
+
const paths = getSkillSearchPaths(cwd);
|
|
198
|
+
const projectPaths = paths.slice(0, 4);
|
|
199
|
+
const userPaths = paths.slice(4);
|
|
200
|
+
const projectSkills = new Map();
|
|
201
|
+
const userSkills = new Map();
|
|
202
|
+
for (const dir of projectPaths) {
|
|
203
|
+
for (const skill of scanSkillsDir(dir, maxDepth)) {
|
|
204
|
+
if (!projectSkills.has(skill.name))
|
|
205
|
+
projectSkills.set(skill.name, skill);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const dir of userPaths) {
|
|
209
|
+
for (const skill of scanSkillsDir(dir, maxDepth)) {
|
|
210
|
+
if (!userSkills.has(skill.name))
|
|
211
|
+
userSkills.set(skill.name, skill);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const merged = new Map(userSkills);
|
|
215
|
+
for (const [name, skill] of projectSkills) {
|
|
216
|
+
if (merged.has(name)) {
|
|
217
|
+
getAppLogger().child("skills").warn(`"${name}" from project overrides user-level skill`);
|
|
218
|
+
}
|
|
219
|
+
merged.set(name, skill);
|
|
220
|
+
}
|
|
221
|
+
return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
222
|
+
}
|
|
223
|
+
/** Metadata-only skill catalog for classic mode (no residency or BM25). */
|
|
224
|
+
export function buildSkillMetadataCatalog(records) {
|
|
225
|
+
if (records.length === 0)
|
|
226
|
+
return "";
|
|
227
|
+
const lines = [
|
|
228
|
+
"## Available Skills",
|
|
229
|
+
"",
|
|
230
|
+
"Read a skill with read_file when it is relevant:",
|
|
231
|
+
"",
|
|
232
|
+
];
|
|
233
|
+
for (const skill of records) {
|
|
234
|
+
lines.push(`- **${skill.name}**: ${skill.description} (\`${skill.location}\`)`);
|
|
235
|
+
}
|
|
236
|
+
return lines.join("\n");
|
|
237
|
+
}
|
|
238
|
+
// ========================================================================
|
|
239
|
+
// Section Boundary Detection
|
|
240
|
+
// ========================================================================
|
|
241
|
+
function detectSectionRanges(body, ariaSections) {
|
|
242
|
+
if (!body)
|
|
243
|
+
return undefined;
|
|
244
|
+
const ranges = {};
|
|
245
|
+
const lines = body.split("\n");
|
|
246
|
+
let hasAny = false;
|
|
247
|
+
// Use aria.skill.json section headings if provided, else auto-detect
|
|
248
|
+
const sectionDefs = ariaSections ?? {
|
|
249
|
+
planner: ["## Planner"],
|
|
250
|
+
execution: ["## Execution"],
|
|
251
|
+
recovery: ["## Recovery"],
|
|
252
|
+
examples: ["## Examples"],
|
|
253
|
+
};
|
|
254
|
+
for (const [section, headings] of Object.entries(sectionDefs)) {
|
|
255
|
+
// Find the first heading match for this section
|
|
256
|
+
let startIdx = -1;
|
|
257
|
+
for (let i = 0; i < lines.length; i++) {
|
|
258
|
+
for (const h of headings) {
|
|
259
|
+
if (lines[i].trim().startsWith(h)) {
|
|
260
|
+
startIdx = i;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (startIdx >= 0)
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
if (startIdx < 0)
|
|
268
|
+
continue;
|
|
269
|
+
// Find the next heading or end of body
|
|
270
|
+
let endIdx = lines.length;
|
|
271
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
272
|
+
const line = lines[i].trim();
|
|
273
|
+
if (line.startsWith("## ")) {
|
|
274
|
+
endIdx = i;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
ranges[section] = { start: startIdx, end: endIdx };
|
|
279
|
+
hasAny = true;
|
|
280
|
+
}
|
|
281
|
+
return hasAny ? ranges : undefined;
|
|
282
|
+
}
|
|
283
|
+
function extractSection(body, range) {
|
|
284
|
+
if (!range || !body)
|
|
285
|
+
return "";
|
|
286
|
+
const lines = body.split("\n");
|
|
287
|
+
return lines.slice(range.start, range.end).join("\n").trim();
|
|
288
|
+
}
|
|
289
|
+
// ========================================================================
|
|
290
|
+
// BM25 Matcher
|
|
291
|
+
// ========================================================================
|
|
292
|
+
// Default synonym map for V1
|
|
293
|
+
const DEFAULT_SYNONYMS = {
|
|
294
|
+
deploy: ["launch", "release", "rollout", "publish"],
|
|
295
|
+
database: ["db", "postgres", "mysql", "sql", "rds", "dynamodb"],
|
|
296
|
+
container: ["docker", "ecs", "kubernetes", "k8s", "pod"],
|
|
297
|
+
aws: ["amazon", "ec2", "s3", "lambda", "cloud"],
|
|
298
|
+
test: ["testing", "spec", "assert", "verify", "check"],
|
|
299
|
+
build: ["compile", "bundle", "package", "construct"],
|
|
300
|
+
error: ["error", "failure", "bug", "issue", "crash", "exception"],
|
|
301
|
+
fix: ["fix", "repair", "patch", "resolve", "correct"],
|
|
302
|
+
code: ["code", "source", "implementation", "program"],
|
|
303
|
+
review: ["review", "audit", "inspect", "check"],
|
|
304
|
+
config: ["configuration", "setup", "settings", "options"],
|
|
305
|
+
monitor: ["monitoring", "observe", "watch", "track", "metrics"],
|
|
306
|
+
auth: ["authentication", "login", "oauth", "sso", "identity"],
|
|
307
|
+
api: ["rest", "graphql", "endpoint", "service", "http"],
|
|
308
|
+
};
|
|
309
|
+
function tokenize(text) {
|
|
310
|
+
return text
|
|
311
|
+
.toLowerCase()
|
|
312
|
+
.split(/[^a-z0-9]+/)
|
|
313
|
+
.filter((t) => t.length > 1);
|
|
314
|
+
}
|
|
315
|
+
function expandTokens(tokens, synonymMap) {
|
|
316
|
+
const expanded = new Set(tokens);
|
|
317
|
+
for (const t of tokens) {
|
|
318
|
+
const syns = synonymMap[t];
|
|
319
|
+
if (syns)
|
|
320
|
+
for (const s of syns)
|
|
321
|
+
expanded.add(s);
|
|
322
|
+
}
|
|
323
|
+
return [...expanded];
|
|
324
|
+
}
|
|
325
|
+
export function buildBM25Index(skills, meta) {
|
|
326
|
+
return skills.map((s) => {
|
|
327
|
+
const aria = meta[s.name] ?? {};
|
|
328
|
+
const tags = aria.tags ?? [];
|
|
329
|
+
const trigger = aria.trigger ?? "";
|
|
330
|
+
const synonyms = aria.synonyms ?? [];
|
|
331
|
+
// Build search text from name, description, tags, trigger
|
|
332
|
+
const searchParts = [s.name, s.description, ...tags, trigger, ...synonyms];
|
|
333
|
+
const searchText = searchParts.filter(Boolean).join(" ");
|
|
334
|
+
const budgetConfig = aria.budget ?? {};
|
|
335
|
+
const sectionMapping = aria.sections;
|
|
336
|
+
return {
|
|
337
|
+
id: s.name,
|
|
338
|
+
name: s.name,
|
|
339
|
+
description: s.description,
|
|
340
|
+
tags,
|
|
341
|
+
trigger,
|
|
342
|
+
synonyms,
|
|
343
|
+
neighbors: aria.neighbors ?? [],
|
|
344
|
+
searchText,
|
|
345
|
+
sectionRanges: detectSectionRanges(s.body, sectionMapping),
|
|
346
|
+
budgetPriority: budgetConfig.priority ?? "normal",
|
|
347
|
+
maxTokens: budgetConfig.max_tokens ?? 2000,
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/** Score a single query against a document using BM25 */
|
|
352
|
+
function bm25Score(queryTokens, docTokens, avgDocLen, totalDocs, docFreq) {
|
|
353
|
+
const k1 = 1.5;
|
|
354
|
+
const b = 0.75;
|
|
355
|
+
const docLen = docTokens.length;
|
|
356
|
+
// Count term frequencies in this document
|
|
357
|
+
const tf = new Map();
|
|
358
|
+
for (const t of docTokens)
|
|
359
|
+
tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
360
|
+
let score = 0;
|
|
361
|
+
for (const qt of queryTokens) {
|
|
362
|
+
const freq = tf.get(qt) ?? 0;
|
|
363
|
+
if (freq === 0)
|
|
364
|
+
continue;
|
|
365
|
+
const df = docFreq.get(qt) ?? 1;
|
|
366
|
+
const idf = Math.log(1 + (totalDocs - df + 0.5) / (df + 0.5));
|
|
367
|
+
const numerator = freq * (k1 + 1);
|
|
368
|
+
const denominator = freq + k1 * (1 - b + b * (docLen / avgDocLen));
|
|
369
|
+
score += idf * (numerator / denominator);
|
|
370
|
+
}
|
|
371
|
+
return score;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Rank skills by relevance to user input using BM25 + synonym expansion + neighbor boost.
|
|
375
|
+
*/
|
|
376
|
+
export function rankSkills(index, userInput, hotSkillIds, synonymMap) {
|
|
377
|
+
if (index.length === 0 || !userInput.trim())
|
|
378
|
+
return [];
|
|
379
|
+
const syns = synonymMap ?? DEFAULT_SYNONYMS;
|
|
380
|
+
const queryTokens = expandTokens(tokenize(userInput), syns);
|
|
381
|
+
if (queryTokens.length === 0)
|
|
382
|
+
return [];
|
|
383
|
+
// Build document frequency map across corpus
|
|
384
|
+
const docFreq = new Map();
|
|
385
|
+
const docTokenLists = [];
|
|
386
|
+
for (const entry of index) {
|
|
387
|
+
const tokens = tokenize(entry.searchText);
|
|
388
|
+
docTokenLists.push(tokens);
|
|
389
|
+
const unique = new Set(tokens);
|
|
390
|
+
for (const t of unique)
|
|
391
|
+
docFreq.set(t, (docFreq.get(t) ?? 0) + 1);
|
|
392
|
+
}
|
|
393
|
+
const totalDocs = index.length;
|
|
394
|
+
const avgDocLen = docTokenLists.reduce((sum, t) => sum + t.length, 0) / Math.max(1, totalDocs);
|
|
395
|
+
const results = [];
|
|
396
|
+
for (let i = 0; i < index.length; i++) {
|
|
397
|
+
const entry = index[i];
|
|
398
|
+
const docTokens = docTokenLists[i];
|
|
399
|
+
let score = bm25Score(queryTokens, docTokens, avgDocLen, totalDocs, docFreq);
|
|
400
|
+
// Keyword score bonus (0.5 weight): fraction of unique doc tokens that match query
|
|
401
|
+
const querySet = new Set(queryTokens);
|
|
402
|
+
const docSet = new Set(docTokens);
|
|
403
|
+
const overlap = [...querySet].filter((t) => docSet.has(t)).length;
|
|
404
|
+
const keywordScore = docSet.size > 0 ? overlap / docSet.size : 0;
|
|
405
|
+
score = score * 0.3 + keywordScore * 0.5;
|
|
406
|
+
// Name match bonus: if the skill name appears in the query, add +0.25
|
|
407
|
+
const nameTokens = tokenize(entry.name);
|
|
408
|
+
const nameMatch = nameTokens.some((nt) => querySet.has(nt));
|
|
409
|
+
if (nameMatch)
|
|
410
|
+
score += 0.25;
|
|
411
|
+
// Exact skill invocation should load the skill, even when the corpus is
|
|
412
|
+
// tiny and BM25/keyword scoring would otherwise leave it WARM.
|
|
413
|
+
if (userInput.trim().toLowerCase() === entry.name.toLowerCase()) {
|
|
414
|
+
score = Math.max(score, 0.5);
|
|
415
|
+
}
|
|
416
|
+
// Graph neighbor boost (0.2 weight): boost if this skill is neighbor of a hot skill
|
|
417
|
+
let graphBoost = 0;
|
|
418
|
+
for (const hotId of hotSkillIds) {
|
|
419
|
+
const hotEntry = index.find((e) => e.id === hotId);
|
|
420
|
+
if (hotEntry?.neighbors?.includes(entry.id)) {
|
|
421
|
+
graphBoost = 0.2;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
score += graphBoost * 0.2;
|
|
426
|
+
if (score > 0) {
|
|
427
|
+
results.push({ entry, score });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return results.sort((a, b) => b.score - a.score);
|
|
431
|
+
}
|
|
432
|
+
// ========================================================================
|
|
433
|
+
// Neighbor Discovery
|
|
434
|
+
// ========================================================================
|
|
435
|
+
function getNeighborIds(entry) {
|
|
436
|
+
return entry.neighbors ?? [];
|
|
437
|
+
}
|
|
438
|
+
// ========================================================================
|
|
439
|
+
// SkillRuntime
|
|
440
|
+
// ========================================================================
|
|
441
|
+
export class SkillRuntime {
|
|
442
|
+
config;
|
|
443
|
+
cwd;
|
|
444
|
+
// Core state
|
|
445
|
+
records = [];
|
|
446
|
+
index = [];
|
|
447
|
+
runtimeStates = new Map();
|
|
448
|
+
turnCount = 0;
|
|
449
|
+
// Telemetry
|
|
450
|
+
events = [];
|
|
451
|
+
// Token budget base (set from compiler.token_budget each turn)
|
|
452
|
+
budgetBase = 100_000;
|
|
453
|
+
// Synonym map (extendable)
|
|
454
|
+
synonyms = { ...DEFAULT_SYNONYMS };
|
|
455
|
+
constructor(config, cwd) {
|
|
456
|
+
this.config = config;
|
|
457
|
+
this.cwd = cwd;
|
|
458
|
+
}
|
|
459
|
+
/** Parse a SKILL.md file. Returns null if invalid or missing required fields. */
|
|
460
|
+
static parseFile(filePath) {
|
|
461
|
+
if (!existsSync(filePath))
|
|
462
|
+
return null;
|
|
463
|
+
const content = readFileSync(filePath, "utf-8");
|
|
464
|
+
return parseSkillMdContent(content, filePath);
|
|
465
|
+
}
|
|
466
|
+
// ---- Initialization ----
|
|
467
|
+
async initialize() {
|
|
468
|
+
if (!this.config.enabled)
|
|
469
|
+
return;
|
|
470
|
+
// 1. Discover skills
|
|
471
|
+
this.records = this.config.searchPaths
|
|
472
|
+
? discoverSkills(this.cwd, this.config.max_depth, this.config.searchPaths)
|
|
473
|
+
: discoverSkills(this.cwd, this.config.max_depth);
|
|
474
|
+
// 2. Load ARIA-specific metadata
|
|
475
|
+
const meta = loadMergedSkillsMeta(this.cwd);
|
|
476
|
+
// 3. Merge user-provided synonyms from meta (extensible)
|
|
477
|
+
// (no field for custom synonyms yet — future)
|
|
478
|
+
// 4. Build BM25 index
|
|
479
|
+
this.index = buildBM25Index(this.records, meta);
|
|
480
|
+
// 5. Emit discovery events
|
|
481
|
+
for (const entry of this.index) {
|
|
482
|
+
this.emit({
|
|
483
|
+
type: "skill_discovered",
|
|
484
|
+
skill_id: entry.id,
|
|
485
|
+
timestamp: Date.now(),
|
|
486
|
+
});
|
|
487
|
+
// Initialize all skills as cold
|
|
488
|
+
this.runtimeStates.set(entry.id, {
|
|
489
|
+
entry,
|
|
490
|
+
residency: "cold",
|
|
491
|
+
loadedSections: [],
|
|
492
|
+
lastActiveTurn: 0,
|
|
493
|
+
tokenCost: 0,
|
|
494
|
+
body: this.records.find((r) => r.name === entry.id)?.body ?? "",
|
|
495
|
+
directory: this.records.find((r) => r.name === entry.id)?.directory ?? "",
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// ---- Per-turn processing ----
|
|
500
|
+
processUserInput(userInput) {
|
|
501
|
+
if (!this.config.enabled || this.index.length === 0)
|
|
502
|
+
return;
|
|
503
|
+
// 1. Get currently hot skill IDs
|
|
504
|
+
const hotIds = new Set();
|
|
505
|
+
for (const [id, state] of this.runtimeStates) {
|
|
506
|
+
if (state.residency === "hot")
|
|
507
|
+
hotIds.add(id);
|
|
508
|
+
}
|
|
509
|
+
// 2. Rank skills against user input
|
|
510
|
+
const matches = rankSkills(this.index, userInput, hotIds, this.synonyms);
|
|
511
|
+
// 3. Promote top matches to HOT (up to budget)
|
|
512
|
+
// Determine which skills get promoted
|
|
513
|
+
const promoteToHot = [];
|
|
514
|
+
const promoteToWarm = [];
|
|
515
|
+
for (const match of matches) {
|
|
516
|
+
const state = this.runtimeStates.get(match.entry.id);
|
|
517
|
+
if (!state)
|
|
518
|
+
continue;
|
|
519
|
+
if (match.score >= 0.3 && state.residency === "cold") {
|
|
520
|
+
promoteToWarm.push(match.entry.id);
|
|
521
|
+
}
|
|
522
|
+
if (match.score >= 0.5) {
|
|
523
|
+
promoteToHot.push(match.entry.id);
|
|
524
|
+
}
|
|
525
|
+
this.emit({
|
|
526
|
+
type: "skill_matched",
|
|
527
|
+
skill_id: match.entry.id,
|
|
528
|
+
score: Math.round(match.score * 100) / 100,
|
|
529
|
+
residency: state.residency,
|
|
530
|
+
timestamp: Date.now(),
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
// 4. Apply neighbor boosting
|
|
534
|
+
for (const hotId of promoteToHot) {
|
|
535
|
+
const hotState = this.runtimeStates.get(hotId);
|
|
536
|
+
if (!hotState)
|
|
537
|
+
continue;
|
|
538
|
+
for (const nid of getNeighborIds(hotState.entry)) {
|
|
539
|
+
const nState = this.runtimeStates.get(nid);
|
|
540
|
+
if (nState && nState.residency === "cold") {
|
|
541
|
+
promoteToWarm.push(nid);
|
|
542
|
+
this.emit({
|
|
543
|
+
type: "skill_neighbor_boosted",
|
|
544
|
+
skill_id: nid,
|
|
545
|
+
residency: "warm",
|
|
546
|
+
timestamp: Date.now(),
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// 5. Apply residency changes
|
|
552
|
+
for (const id of promoteToHot) {
|
|
553
|
+
this.setResidency(id, "hot");
|
|
554
|
+
}
|
|
555
|
+
for (const id of promoteToWarm) {
|
|
556
|
+
if (this.runtimeStates.get(id)?.residency === "cold") {
|
|
557
|
+
this.setResidency(id, "warm");
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// 6. Enforce token budget
|
|
561
|
+
this.enforceBudget();
|
|
562
|
+
}
|
|
563
|
+
endTurn() {
|
|
564
|
+
if (!this.config.enabled)
|
|
565
|
+
return;
|
|
566
|
+
// 1. Update turn count
|
|
567
|
+
this.turnCount++;
|
|
568
|
+
// 2. Demote idle hot → warm
|
|
569
|
+
for (const [id, state] of this.runtimeStates) {
|
|
570
|
+
if (state.residency !== "hot")
|
|
571
|
+
continue;
|
|
572
|
+
const idle = Math.max(0, this.turnCount - state.lastActiveTurn - 1);
|
|
573
|
+
if (idle >= this.config.active_skill_idle_turns) {
|
|
574
|
+
this.demote(id, "warm");
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// 3. Evict idle warm → cold
|
|
578
|
+
for (const [id, state] of this.runtimeStates) {
|
|
579
|
+
if (state.residency !== "warm")
|
|
580
|
+
continue;
|
|
581
|
+
const idle = Math.max(0, this.turnCount - state.lastActiveTurn - 1);
|
|
582
|
+
if (idle >= this.config.warm_skill_eviction_turns) {
|
|
583
|
+
this.evict(id);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
markSkillActive(skillId) {
|
|
588
|
+
const state = this.runtimeStates.get(skillId);
|
|
589
|
+
if (state) {
|
|
590
|
+
state.lastActiveTurn = this.turnCount;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// ---- Prompt assembly ----
|
|
594
|
+
getSnapshot(tokenBudget) {
|
|
595
|
+
const hot = [];
|
|
596
|
+
const warm = [];
|
|
597
|
+
let tokenUsage = 0;
|
|
598
|
+
for (const state of this.runtimeStates.values()) {
|
|
599
|
+
if (state.residency === "hot") {
|
|
600
|
+
hot.push(state);
|
|
601
|
+
tokenUsage += state.tokenCost;
|
|
602
|
+
}
|
|
603
|
+
else if (state.residency === "warm") {
|
|
604
|
+
warm.push(state);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
hot: hot.sort((a, b) => a.entry.name.localeCompare(b.entry.name)),
|
|
609
|
+
warm: warm.sort((a, b) => a.entry.name.localeCompare(b.entry.name)),
|
|
610
|
+
tokenUsage,
|
|
611
|
+
tokenBudget,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
/** Build the skills section for the compiled prompt. */
|
|
615
|
+
buildPromptSection(tokenBudget) {
|
|
616
|
+
if (!this.config.enabled)
|
|
617
|
+
return "";
|
|
618
|
+
// Enforce budget before building
|
|
619
|
+
this.setBudgetBase(tokenBudget);
|
|
620
|
+
const snapshot = this.getSnapshot(tokenBudget);
|
|
621
|
+
const lines = ["## Loaded Skills"];
|
|
622
|
+
if (snapshot.hot.length === 0 && snapshot.warm.length === 0) {
|
|
623
|
+
const cold = [...this.runtimeStates.values()]
|
|
624
|
+
.filter((s) => s.residency === "cold")
|
|
625
|
+
.sort((a, b) => a.entry.name.localeCompare(b.entry.name));
|
|
626
|
+
if (cold.length === 0) {
|
|
627
|
+
lines.push("", "(no skills loaded)");
|
|
628
|
+
return lines.join("\n");
|
|
629
|
+
}
|
|
630
|
+
lines.push("", "### Available Skills");
|
|
631
|
+
for (const state of cold) {
|
|
632
|
+
lines.push(`- **${state.entry.name}**: ${state.entry.description}`);
|
|
633
|
+
}
|
|
634
|
+
return lines.join("\n");
|
|
635
|
+
}
|
|
636
|
+
// HOT skills — full bodies of loaded sections
|
|
637
|
+
for (const state of snapshot.hot) {
|
|
638
|
+
lines.push("", `### ${state.entry.name} [HOT]`);
|
|
639
|
+
lines.push(`Tags: ${state.entry.tags.join(", ") || "(none)"}`);
|
|
640
|
+
if (state.entry.trigger)
|
|
641
|
+
lines.push(`Trigger: ${state.entry.trigger}`);
|
|
642
|
+
// Progressive sections
|
|
643
|
+
for (const section of state.loadedSections) {
|
|
644
|
+
const sectionContent = this.getSectionContent(state, section);
|
|
645
|
+
if (sectionContent) {
|
|
646
|
+
lines.push("", sectionContent);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// If no sections loaded, load full body
|
|
650
|
+
if (state.loadedSections.length === 0 && state.body) {
|
|
651
|
+
lines.push("", state.body);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// WARM skills — one-line stubs
|
|
655
|
+
if (snapshot.warm.length > 0) {
|
|
656
|
+
lines.push("", "### Standing By");
|
|
657
|
+
for (const state of snapshot.warm) {
|
|
658
|
+
const tags = state.entry.tags.length > 0 ? ` [${state.entry.tags.slice(0, 3).join(", ")}]` : "";
|
|
659
|
+
lines.push(`- ${state.entry.name}${tags}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return lines.join("\n");
|
|
663
|
+
}
|
|
664
|
+
getSectionContent(state, section) {
|
|
665
|
+
if (!state.entry.sectionRanges)
|
|
666
|
+
return "";
|
|
667
|
+
const range = state.entry.sectionRanges[section];
|
|
668
|
+
if (!range)
|
|
669
|
+
return "";
|
|
670
|
+
return extractSection(state.body, range);
|
|
671
|
+
}
|
|
672
|
+
// ---- Residency management ----
|
|
673
|
+
setResidency(id, target) {
|
|
674
|
+
const state = this.runtimeStates.get(id);
|
|
675
|
+
if (!state || state.residency === target)
|
|
676
|
+
return;
|
|
677
|
+
const prev = state.residency;
|
|
678
|
+
state.residency = target;
|
|
679
|
+
state.lastActiveTurn = this.turnCount;
|
|
680
|
+
if (target === "hot") {
|
|
681
|
+
// Progressive hydration: load planner first, execution on active use
|
|
682
|
+
this.hydrateSkill(id);
|
|
683
|
+
}
|
|
684
|
+
if (target === "hot") {
|
|
685
|
+
this.emit({
|
|
686
|
+
type: "skill_loaded",
|
|
687
|
+
skill_id: id,
|
|
688
|
+
residency: target,
|
|
689
|
+
prev_residency: prev,
|
|
690
|
+
sections: state.loadedSections,
|
|
691
|
+
token_cost: state.tokenCost,
|
|
692
|
+
timestamp: Date.now(),
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
this.emit({
|
|
697
|
+
type: "skill_promoted",
|
|
698
|
+
skill_id: id,
|
|
699
|
+
residency: target,
|
|
700
|
+
prev_residency: prev,
|
|
701
|
+
sections: state.loadedSections,
|
|
702
|
+
token_cost: state.tokenCost,
|
|
703
|
+
timestamp: Date.now(),
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
demote(id, target) {
|
|
708
|
+
const state = this.runtimeStates.get(id);
|
|
709
|
+
if (!state)
|
|
710
|
+
return;
|
|
711
|
+
const prev = state.residency;
|
|
712
|
+
state.residency = target;
|
|
713
|
+
state.loadedSections = [];
|
|
714
|
+
state.tokenCost = 0;
|
|
715
|
+
this.emit({
|
|
716
|
+
type: "skill_demoted",
|
|
717
|
+
skill_id: id,
|
|
718
|
+
residency: target,
|
|
719
|
+
prev_residency: prev,
|
|
720
|
+
timestamp: Date.now(),
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
evict(id) {
|
|
724
|
+
const state = this.runtimeStates.get(id);
|
|
725
|
+
if (!state)
|
|
726
|
+
return;
|
|
727
|
+
state.residency = "cold";
|
|
728
|
+
state.loadedSections = [];
|
|
729
|
+
state.tokenCost = 0;
|
|
730
|
+
this.emit({
|
|
731
|
+
type: "skill_evicted",
|
|
732
|
+
skill_id: id,
|
|
733
|
+
residency: "cold",
|
|
734
|
+
timestamp: Date.now(),
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
// ---- Progressive Hydration ----
|
|
738
|
+
hydrateSkill(id) {
|
|
739
|
+
const state = this.runtimeStates.get(id);
|
|
740
|
+
if (!state)
|
|
741
|
+
return;
|
|
742
|
+
// Load planner when present; otherwise leave sections empty so the prompt
|
|
743
|
+
// builder falls back to the full skill body.
|
|
744
|
+
if (state.entry.sectionRanges?.planner && !state.loadedSections.includes("planner")) {
|
|
745
|
+
state.loadedSections.push("planner");
|
|
746
|
+
this.emit({
|
|
747
|
+
type: "skill_hydrated",
|
|
748
|
+
skill_id: id,
|
|
749
|
+
sections: ["planner"],
|
|
750
|
+
timestamp: Date.now(),
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
// Recompute token cost
|
|
754
|
+
state.tokenCost = this.computeTokenCost(state);
|
|
755
|
+
}
|
|
756
|
+
/** Load execution section (called when tool execution starts) */
|
|
757
|
+
hydrateExecution(id) {
|
|
758
|
+
const state = this.runtimeStates.get(id);
|
|
759
|
+
if (!state || state.residency !== "hot")
|
|
760
|
+
return;
|
|
761
|
+
if (!state.loadedSections.includes("execution")) {
|
|
762
|
+
state.loadedSections.push("execution");
|
|
763
|
+
state.tokenCost = this.computeTokenCost(state);
|
|
764
|
+
this.emit({
|
|
765
|
+
type: "skill_hydrated",
|
|
766
|
+
skill_id: id,
|
|
767
|
+
sections: ["execution"],
|
|
768
|
+
timestamp: Date.now(),
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
/** HOT skill IDs for the current turn. */
|
|
773
|
+
getHotSkillIds() {
|
|
774
|
+
return [...this.runtimeStates.entries()]
|
|
775
|
+
.filter(([, state]) => state.residency === "hot")
|
|
776
|
+
.map(([id]) => id);
|
|
777
|
+
}
|
|
778
|
+
/** Load execution sections for all HOT skills (called when tool execution starts). */
|
|
779
|
+
hydrateExecutionForHotSkills() {
|
|
780
|
+
for (const id of this.getHotSkillIds()) {
|
|
781
|
+
this.hydrateExecution(id);
|
|
782
|
+
this.markSkillActive(id);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
/** Load recovery sections for all HOT skills (called on tool failure). */
|
|
786
|
+
hydrateRecoveryForHotSkills() {
|
|
787
|
+
for (const id of this.getHotSkillIds()) {
|
|
788
|
+
this.hydrateRecovery(id);
|
|
789
|
+
this.markSkillActive(id);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/** Load recovery section (called on failure) */
|
|
793
|
+
hydrateRecovery(id) {
|
|
794
|
+
const state = this.runtimeStates.get(id);
|
|
795
|
+
if (!state || state.residency !== "hot")
|
|
796
|
+
return;
|
|
797
|
+
if (!state.loadedSections.includes("recovery")) {
|
|
798
|
+
state.loadedSections.push("recovery");
|
|
799
|
+
state.tokenCost = this.computeTokenCost(state);
|
|
800
|
+
this.emit({
|
|
801
|
+
type: "skill_hydrated",
|
|
802
|
+
skill_id: id,
|
|
803
|
+
sections: ["recovery"],
|
|
804
|
+
timestamp: Date.now(),
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
// ---- Budget ----
|
|
809
|
+
computeTokenCost(state) {
|
|
810
|
+
let total = 0;
|
|
811
|
+
for (const section of state.loadedSections) {
|
|
812
|
+
const content = this.getSectionContent(state, section);
|
|
813
|
+
total += Math.ceil(content.length / 4);
|
|
814
|
+
}
|
|
815
|
+
if (state.loadedSections.length === 0 && state.body) {
|
|
816
|
+
total = Math.ceil(state.body.length / 4);
|
|
817
|
+
}
|
|
818
|
+
return Math.min(total, state.entry.maxTokens);
|
|
819
|
+
}
|
|
820
|
+
getSkillsTokenBudget() {
|
|
821
|
+
return Math.floor(this.budgetBase * this.config.max_token_budget_ratio);
|
|
822
|
+
}
|
|
823
|
+
enforceBudget() {
|
|
824
|
+
const budget = this.getSkillsTokenBudget();
|
|
825
|
+
const hotStates = [...this.runtimeStates.values()]
|
|
826
|
+
.filter((s) => s.residency === "hot")
|
|
827
|
+
.sort((a, b) => a.lastActiveTurn - b.lastActiveTurn);
|
|
828
|
+
let totalCost = hotStates.reduce((sum, s) => sum + s.tokenCost, 0);
|
|
829
|
+
while (totalCost > budget && hotStates.length > 0) {
|
|
830
|
+
const victim = hotStates.shift();
|
|
831
|
+
this.emit({
|
|
832
|
+
type: "skill_budget_exceeded",
|
|
833
|
+
skill_id: victim.entry.id,
|
|
834
|
+
token_cost: victim.tokenCost,
|
|
835
|
+
timestamp: Date.now(),
|
|
836
|
+
});
|
|
837
|
+
this.demote(victim.entry.id, "warm");
|
|
838
|
+
totalCost -= victim.tokenCost;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
/** Update the budget base (called each turn with compiler.token_budget). */
|
|
842
|
+
setBudgetBase(totalTokenBudget) {
|
|
843
|
+
this.budgetBase = totalTokenBudget;
|
|
844
|
+
this.enforceBudget();
|
|
845
|
+
}
|
|
846
|
+
// ---- Telemetry ----
|
|
847
|
+
emit(event) {
|
|
848
|
+
this.events.push(event);
|
|
849
|
+
}
|
|
850
|
+
drainEvents() {
|
|
851
|
+
const drained = this.events;
|
|
852
|
+
this.events = [];
|
|
853
|
+
return drained;
|
|
854
|
+
}
|
|
855
|
+
getEvents() {
|
|
856
|
+
return [...this.events];
|
|
857
|
+
}
|
|
858
|
+
// ---- Queries ----
|
|
859
|
+
getIndex() {
|
|
860
|
+
return [...this.index];
|
|
861
|
+
}
|
|
862
|
+
getRuntimeStates() {
|
|
863
|
+
return new Map(this.runtimeStates);
|
|
864
|
+
}
|
|
865
|
+
getSkillCount() {
|
|
866
|
+
return this.index.length;
|
|
867
|
+
}
|
|
868
|
+
getResidencyCounts() {
|
|
869
|
+
let hot = 0;
|
|
870
|
+
let warm = 0;
|
|
871
|
+
let cold = 0;
|
|
872
|
+
for (const state of this.runtimeStates.values()) {
|
|
873
|
+
if (state.residency === "hot")
|
|
874
|
+
hot++;
|
|
875
|
+
else if (state.residency === "warm")
|
|
876
|
+
warm++;
|
|
877
|
+
else
|
|
878
|
+
cold++;
|
|
879
|
+
}
|
|
880
|
+
return { hot, warm, cold };
|
|
881
|
+
}
|
|
882
|
+
isEnabled() {
|
|
883
|
+
return this.config.enabled;
|
|
884
|
+
}
|
|
885
|
+
}
|