@sns-myagent/cli 0.2.0 → 0.3.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/CHANGELOG.md +12 -1
- package/README.md +7 -8
- package/package.json +3 -3
- package/scripts/apply-pi-natives-patch.js +82 -56
- package/src/adapters/telegram/bot.ts +41 -1
- package/src/adapters/telegram/bridge.ts +186 -0
- package/src/adapters/telegram/handler.ts +138 -13
- package/src/adapters/telegram/index.ts +12 -2
- package/src/agents/__tests__/config.test.ts +79 -0
- package/src/agents/__tests__/resilience.test.ts +119 -0
- package/src/agents/__tests__/strategies.test.ts +83 -0
- package/src/agents/config.ts +367 -0
- package/src/agents/ensemble.ts +224 -0
- package/src/agents/resilience.ts +332 -0
- package/src/agents/strategies/best-of-n.ts +108 -0
- package/src/agents/strategies/consensus.ts +105 -0
- package/src/agents/strategies/critic.ts +131 -0
- package/src/agents/strategies/types.ts +68 -0
- package/src/async/__tests__/task-runner.test.ts +162 -0
- package/src/async/__tests__/task-store.test.ts +146 -0
- package/src/async/index.ts +28 -1
- package/src/async/notifier.ts +133 -0
- package/src/async/task-runner.ts +175 -0
- package/src/async/task-store.ts +170 -0
- package/src/async/types.ts +70 -0
- package/src/cli/entry.ts +3 -1
- package/src/cli/index.ts +69 -54
- package/src/config/index.ts +1 -1
- package/src/debug/index.ts +1 -1
- package/src/modes/components/welcome.ts +13 -6
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/setup-wizard/scenes/splash.ts +1 -1
- package/src/session/agent-session.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +63 -0
- package/src/slash-commands/helpers/task.ts +181 -0
- package/src/tbm/__tests__/tbm.test.ts +660 -0
- package/src/tbm/comm-modes.ts +165 -0
- package/src/tbm/config.ts +136 -0
- package/src/tbm/context-delta.ts +146 -0
- package/src/tbm/context-pyramid.ts +202 -0
- package/src/tbm/dashboard.ts +131 -0
- package/src/tbm/index.ts +247 -0
- package/src/tbm/lazy-skills.ts +182 -0
- package/src/tbm/response-cache.ts +220 -0
- package/src/tbm/tombstone.ts +189 -0
- package/src/tbm/tool-compress.ts +230 -0
- package/src/tools/ask.ts +1 -1
- package/src/tui/chat-blocks.ts +205 -0
- package/src/tui/chat-ui.ts +270 -0
- package/src/tui/code-cell.ts +90 -1
- package/src/tui/command-palette.ts +189 -0
- package/src/tui/index.ts +4 -0
- package/src/tui/splash.ts +130 -0
- package/src/ui/banner.ts +69 -29
- package/src/ui/colors.ts +24 -0
- package/src/ui/error-display.ts +130 -0
- package/src/ui/gradient.ts +104 -0
- package/src/ui/index.ts +15 -0
- package/src/ui/memory-toast.ts +102 -0
- package/src/ui/status-bar.ts +36 -30
- package/bin/snscoder.js +0 -98
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Dashboard — `/tokens` command implementation.
|
|
3
|
+
*
|
|
4
|
+
* Shows session stats: duration, tokens, cost, cache hit rate,
|
|
5
|
+
* pyramid level, communication mode, compression stats.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TbmManager } from "./index";
|
|
9
|
+
|
|
10
|
+
export interface DashboardData {
|
|
11
|
+
sessionDuration: string;
|
|
12
|
+
totalInputTokens: number;
|
|
13
|
+
totalOutputTokens: number;
|
|
14
|
+
cachedTokens: number;
|
|
15
|
+
estimatedCost: string;
|
|
16
|
+
cacheHitRate: string;
|
|
17
|
+
pyramidLevel: number;
|
|
18
|
+
pyramidLabel: string;
|
|
19
|
+
commMode: string;
|
|
20
|
+
compressionRatio: string;
|
|
21
|
+
tokensSaved: string;
|
|
22
|
+
tombstonesActive: number;
|
|
23
|
+
responseCacheSize: number;
|
|
24
|
+
skillsLoaded: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Format a number with K/M suffix.
|
|
29
|
+
*/
|
|
30
|
+
function formatNumber(n: number): string {
|
|
31
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
32
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
33
|
+
return String(n);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format duration from milliseconds.
|
|
38
|
+
*/
|
|
39
|
+
function formatDuration(ms: number): string {
|
|
40
|
+
const seconds = Math.floor(ms / 1000);
|
|
41
|
+
if (seconds < 60) return `${seconds}s`;
|
|
42
|
+
const minutes = Math.floor(seconds / 60);
|
|
43
|
+
const secs = seconds % 60;
|
|
44
|
+
if (minutes < 60) return `${minutes}m ${secs}s`;
|
|
45
|
+
const hours = Math.floor(minutes / 60);
|
|
46
|
+
const mins = minutes % 60;
|
|
47
|
+
return `${hours}h ${mins}m`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Estimate cost from token counts.
|
|
52
|
+
* Rough estimate: $3/1M input, $15/1M output (Claude Sonnet class).
|
|
53
|
+
*/
|
|
54
|
+
function estimateCost(inputTokens: number, outputTokens: number): string {
|
|
55
|
+
const inputCost = (inputTokens / 1_000_000) * 3;
|
|
56
|
+
const outputCost = (outputTokens / 1_000_000) * 15;
|
|
57
|
+
const total = inputCost + outputCost;
|
|
58
|
+
if (total < 0.01) return "<$0.01";
|
|
59
|
+
return `$${total.toFixed(3)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build dashboard data from TBM manager state.
|
|
64
|
+
*/
|
|
65
|
+
export function buildDashboard(mgr: TbmManager, sessionStartTime: number): DashboardData {
|
|
66
|
+
const deltaStats = mgr.contextDelta.stats;
|
|
67
|
+
const pyramidStats = mgr.pyramid.stats;
|
|
68
|
+
const compressStats = mgr.compressor.stats;
|
|
69
|
+
const cacheStats = mgr.responseCache.stats;
|
|
70
|
+
const tombstoneStats = mgr.tombstoner.stats;
|
|
71
|
+
const skillStats = mgr.lazySkills.stats;
|
|
72
|
+
|
|
73
|
+
const totalSaved = deltaStats.tokens_saved
|
|
74
|
+
+ compressStats.tokensSaved
|
|
75
|
+
+ tombstoneStats.tokensSaved
|
|
76
|
+
+ cacheStats.tokensSaved
|
|
77
|
+
+ skillStats.tokensSaved;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
sessionDuration: formatDuration(Date.now() - sessionStartTime),
|
|
81
|
+
totalInputTokens: deltaStats.total_input_tokens,
|
|
82
|
+
totalOutputTokens: 0, // tracked by provider
|
|
83
|
+
cachedTokens: deltaStats.tokens_saved,
|
|
84
|
+
estimatedCost: estimateCost(deltaStats.total_input_tokens, 0),
|
|
85
|
+
cacheHitRate: `${(deltaStats.cache_hits / Math.max(1, deltaStats.turns) * 100).toFixed(0)}%`,
|
|
86
|
+
pyramidLevel: pyramidStats.currentLevel,
|
|
87
|
+
pyramidLabel: pyramidStats.currentLevel.toString(),
|
|
88
|
+
commMode: mgr.commMode.effective,
|
|
89
|
+
compressionRatio: `${(compressStats.totalOutputs > 0 ? (compressStats.compressed / compressStats.totalOutputs * 100) : 0).toFixed(0)}%`,
|
|
90
|
+
tokensSaved: formatNumber(totalSaved),
|
|
91
|
+
tombstonesActive: tombstoneStats.messagesTombstoned,
|
|
92
|
+
responseCacheSize: cacheStats.cacheSize,
|
|
93
|
+
skillsLoaded: `${skillStats.loadedOnDemand}/${skillStats.totalAvailable}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Render dashboard as formatted text for display.
|
|
99
|
+
*/
|
|
100
|
+
export function renderDashboard(data: DashboardData): string {
|
|
101
|
+
return `╔══════════════════════════════════════╗
|
|
102
|
+
║ 🧠 Token Budget Manager ║
|
|
103
|
+
╠══════════════════════════════════════╣
|
|
104
|
+
║ Session ${data.sessionDuration.padEnd(25)}║
|
|
105
|
+
║ Input ${String(data.totalInputTokens).padEnd(25)}║
|
|
106
|
+
║ Cached ${String(data.cachedTokens).padEnd(25)}║
|
|
107
|
+
║ Est. Cost ${data.estimatedCost.padEnd(25)}║
|
|
108
|
+
╠══════════════════════════════════════╣
|
|
109
|
+
║ 📊 Context Delta ║
|
|
110
|
+
║ Cache Hit ${data.cacheHitRate.padEnd(25)}║
|
|
111
|
+
╠══════════════════════════════════════╣
|
|
112
|
+
║ 🔺 Pyramid ║
|
|
113
|
+
║ Level ${String(data.pyramidLevel).padEnd(25)}║
|
|
114
|
+
╠══════════════════════════════════════╣
|
|
115
|
+
║ 💬 Mode ${data.commMode.padEnd(23)}║
|
|
116
|
+
╠══════════════════════════════════════╣
|
|
117
|
+
║ 🗜️ Compression ${data.compressionRatio.padEnd(22)}║
|
|
118
|
+
║ 💾 Saved ${data.tokensSaved.padEnd(23)}║
|
|
119
|
+
╠══════════════════════════════════════╣
|
|
120
|
+
║ 🗂 Tombstones ${String(data.tombstonesActive).padEnd(23)}║
|
|
121
|
+
║ 📦 Cache Size ${String(data.responseCacheSize).padEnd(23)}║
|
|
122
|
+
║ 🎯 Skills ${data.skillsLoaded.padEnd(23)}║
|
|
123
|
+
╚══════════════════════════════════════╝`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Render dashboard as compact one-liner for status bar.
|
|
128
|
+
*/
|
|
129
|
+
export function renderCompactDashboard(data: DashboardData): string {
|
|
130
|
+
return `🧠 TBM | L${data.pyramidLevel} ${data.commMode} | Δ${data.cacheHitRate} | 🗜${data.compressionRatio} | 💾${data.tokensSaved}`;
|
|
131
|
+
}
|
package/src/tbm/index.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TBM Manager — coordinates all Token Budget Manager subsystems.
|
|
3
|
+
*
|
|
4
|
+
* Single entry point for the session/agent loop to interact with TBM.
|
|
5
|
+
* Each subsystem is independently toggleable via config.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const mgr = new TbmManager(config);
|
|
9
|
+
* const ctx = mgr.processTurn(message, fullContext);
|
|
10
|
+
* // use ctx.content, ctx.directive, etc.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { type TbmConfig, DEFAULT_TBM_CONFIG, resolveTbmConfig } from "./config";
|
|
14
|
+
import { ContextDeltaCache, estimateTokens } from "./context-delta";
|
|
15
|
+
import { ContextPyramid, type PyramidLevel } from "./context-pyramid";
|
|
16
|
+
import { LazySkillLoader, type SkillEntry } from "./lazy-skills";
|
|
17
|
+
import { ToolOutputCompressor } from "./tool-compress";
|
|
18
|
+
import { CommunicationModeManager, type CommMode } from "./comm-modes";
|
|
19
|
+
import { ConversationTombstoner } from "./tombstone";
|
|
20
|
+
import { ResponseCache } from "./response-cache";
|
|
21
|
+
import { buildDashboard, renderDashboard, renderCompactDashboard, type DashboardData } from "./dashboard";
|
|
22
|
+
|
|
23
|
+
export type { TbmConfig } from "./config";
|
|
24
|
+
export type { ContextDeltaStats } from "./context-delta";
|
|
25
|
+
export type { PyramidLevel, PyramidStats } from "./context-pyramid";
|
|
26
|
+
export type { SkillEntry, LazySkillStats } from "./lazy-skills";
|
|
27
|
+
export type { CompressStats } from "./tool-compress";
|
|
28
|
+
export type { CommMode } from "./comm-modes";
|
|
29
|
+
export type { TombstoneStats } from "./tombstone";
|
|
30
|
+
export type { ResponseCacheStats } from "./response-cache";
|
|
31
|
+
export type { DashboardData } from "./dashboard";
|
|
32
|
+
|
|
33
|
+
export interface TurnResult {
|
|
34
|
+
/** Content to send (possibly delta-compressed). */
|
|
35
|
+
content: string;
|
|
36
|
+
/** Communication mode directive to inject into system prompt. */
|
|
37
|
+
directive: string;
|
|
38
|
+
/** Current pyramid level. */
|
|
39
|
+
pyramidLevel: PyramidLevel;
|
|
40
|
+
/** Tokens saved this turn across all subsystems. */
|
|
41
|
+
tokensSavedThisTurn: number;
|
|
42
|
+
/** Whether context delta cache hit. */
|
|
43
|
+
deltaCacheHit: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class TbmManager {
|
|
47
|
+
readonly #config: TbmConfig;
|
|
48
|
+
readonly #contextDelta: ContextDeltaCache;
|
|
49
|
+
readonly #pyramid: ContextPyramid;
|
|
50
|
+
readonly #lazySkills: LazySkillLoader;
|
|
51
|
+
readonly #compressor: ToolOutputCompressor;
|
|
52
|
+
readonly #commMode: CommunicationModeManager;
|
|
53
|
+
readonly #tombstoner: ConversationTombstoner;
|
|
54
|
+
readonly #responseCache: ResponseCache;
|
|
55
|
+
readonly #sessionStartTime: number;
|
|
56
|
+
|
|
57
|
+
constructor(config?: Partial<TbmConfig>) {
|
|
58
|
+
this.#config = resolveTbmConfig(config);
|
|
59
|
+
this.#contextDelta = new ContextDeltaCache();
|
|
60
|
+
this.#pyramid = new ContextPyramid(
|
|
61
|
+
this.#config.pyramid.start_level as PyramidLevel,
|
|
62
|
+
this.#config.pyramid.max_level as PyramidLevel,
|
|
63
|
+
);
|
|
64
|
+
this.#lazySkills = new LazySkillLoader();
|
|
65
|
+
this.#compressor = new ToolOutputCompressor(this.#config.compress.budgets);
|
|
66
|
+
this.#commMode = new CommunicationModeManager(this.#config.comm_mode);
|
|
67
|
+
this.#tombstoner = new ConversationTombstoner(
|
|
68
|
+
this.#config.tombstone.after_turns,
|
|
69
|
+
this.#config.tombstone.keep_recent,
|
|
70
|
+
);
|
|
71
|
+
this.#responseCache = new ResponseCache(
|
|
72
|
+
this.#config.response_cache.ttl_seconds,
|
|
73
|
+
this.#config.response_cache.max_entries,
|
|
74
|
+
this.#config.response_cache.similarity_threshold,
|
|
75
|
+
);
|
|
76
|
+
this.#sessionStartTime = Date.now();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Whether TBM is enabled. */
|
|
80
|
+
get enabled(): boolean {
|
|
81
|
+
return this.#config.enabled;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Access subsystems directly. */
|
|
85
|
+
get contextDelta(): ContextDeltaCache { return this.#contextDelta; }
|
|
86
|
+
get pyramid(): ContextPyramid { return this.#pyramid; }
|
|
87
|
+
get lazySkills(): LazySkillLoader { return this.#lazySkills; }
|
|
88
|
+
get compressor(): ToolOutputCompressor { return this.#compressor; }
|
|
89
|
+
get commMode(): CommunicationModeManager { return this.#commMode; }
|
|
90
|
+
get tombstoner(): ConversationTombstoner { return this.#tombstoner; }
|
|
91
|
+
get responseCache(): ResponseCache { return this.#responseCache; }
|
|
92
|
+
get config(): Readonly<TbmConfig> { return this.#config; }
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Process a new user turn — main entry point for the session loop.
|
|
96
|
+
*
|
|
97
|
+
* 1. Check response cache
|
|
98
|
+
* 2. Resolve communication mode
|
|
99
|
+
* 3. Determine pyramid level
|
|
100
|
+
* 4. Process context delta
|
|
101
|
+
* 5. Load relevant skills (lazy)
|
|
102
|
+
*/
|
|
103
|
+
processTurn(message: string, fullContext: string): TurnResult {
|
|
104
|
+
if (!this.#config.enabled) {
|
|
105
|
+
return {
|
|
106
|
+
content: fullContext,
|
|
107
|
+
directive: "",
|
|
108
|
+
pyramidLevel: 4 as PyramidLevel,
|
|
109
|
+
tokensSavedThisTurn: 0,
|
|
110
|
+
deltaCacheHit: false,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let totalSaved = 0;
|
|
115
|
+
|
|
116
|
+
// 1. Response cache check
|
|
117
|
+
const cached = this.#responseCache.get(message);
|
|
118
|
+
if (cached.hit && cached.response) {
|
|
119
|
+
return {
|
|
120
|
+
content: cached.response,
|
|
121
|
+
directive: `[Cache ${cached.matchType} hit]`,
|
|
122
|
+
pyramidLevel: this.#pyramid.level,
|
|
123
|
+
tokensSavedThisTurn: estimateTokens(cached.response),
|
|
124
|
+
deltaCacheHit: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 2. Communication mode
|
|
129
|
+
const { directive } = this.#commMode.resolveForMessage(message);
|
|
130
|
+
|
|
131
|
+
// 3. Pyramid level
|
|
132
|
+
if (this.#config.pyramid.enabled) {
|
|
133
|
+
const level = this.#pyramid.resolveLevel(message);
|
|
134
|
+
this.#pyramid.setLevel(level);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 4. Context delta
|
|
138
|
+
let content = fullContext;
|
|
139
|
+
let deltaCacheHit = false;
|
|
140
|
+
if (this.#config.context_delta.enabled) {
|
|
141
|
+
const delta = this.#contextDelta.processTurn(fullContext);
|
|
142
|
+
content = delta.content;
|
|
143
|
+
totalSaved += delta.tokensSaved;
|
|
144
|
+
deltaCacheHit = !delta.staticChanged;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 5. Lazy skills
|
|
148
|
+
if (this.#config.lazy_skills.enabled) {
|
|
149
|
+
const { skillsToLoad, indexSection } = this.#lazySkills.processMessage(
|
|
150
|
+
message,
|
|
151
|
+
this.#config.lazy_skills.max_per_turn,
|
|
152
|
+
);
|
|
153
|
+
if (skillsToLoad.length > 0) {
|
|
154
|
+
content += "\n\n## Loaded Skills\n\n" + skillsToLoad.map((s) => s.fullContent).join("\n\n---\n\n");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
content,
|
|
160
|
+
directive,
|
|
161
|
+
pyramidLevel: this.#pyramid.level,
|
|
162
|
+
tokensSavedThisTurn: totalSaved,
|
|
163
|
+
deltaCacheHit,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Compress tool output before adding to context.
|
|
169
|
+
*/
|
|
170
|
+
compressToolOutput(toolName: string, output: string): { output: string; compressed: boolean; tokensSaved: number } {
|
|
171
|
+
if (!this.#config.compress.enabled) {
|
|
172
|
+
return { output, compressed: false, tokensSaved: 0 };
|
|
173
|
+
}
|
|
174
|
+
return this.#compressor.compress(toolName, output);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Cache a response for future exact/semantic match.
|
|
179
|
+
*/
|
|
180
|
+
cacheResponse(query: string, response: string): void {
|
|
181
|
+
if (this.#config.response_cache.enabled) {
|
|
182
|
+
this.#responseCache.set(query, response);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Tombstone old conversation messages.
|
|
188
|
+
*/
|
|
189
|
+
tombstoneMessages(messages: Array<{ role: "user" | "assistant"; content: string }>): {
|
|
190
|
+
compressed: Array<{ role: "user" | "assistant"; content: string }>;
|
|
191
|
+
tombstoned: number;
|
|
192
|
+
tokensSaved: number;
|
|
193
|
+
} {
|
|
194
|
+
if (!this.#config.tombstone.enabled) {
|
|
195
|
+
return { compressed: messages, tombstoned: 0, tokensSaved: 0 };
|
|
196
|
+
}
|
|
197
|
+
return this.#tombstoner.tombstone(messages);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Register skills for lazy loading.
|
|
202
|
+
*/
|
|
203
|
+
registerSkills(skills: SkillEntry[]): void {
|
|
204
|
+
this.#lazySkills.registerSkills(skills);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Set communication mode explicitly.
|
|
209
|
+
*/
|
|
210
|
+
setMode(mode: CommMode | "auto"): void {
|
|
211
|
+
this.#commMode.setMode(mode);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Build and render the token dashboard.
|
|
216
|
+
*/
|
|
217
|
+
getDashboard(): DashboardData {
|
|
218
|
+
return buildDashboard(this, this.#sessionStartTime);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Render the dashboard as formatted text.
|
|
223
|
+
*/
|
|
224
|
+
renderDashboard(): string {
|
|
225
|
+
return renderDashboard(this.getDashboard());
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Render the dashboard as a compact one-liner.
|
|
230
|
+
*/
|
|
231
|
+
renderCompactDashboard(): string {
|
|
232
|
+
return renderCompactDashboard(this.getDashboard());
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Reset all subsystems (e.g. on session reset).
|
|
237
|
+
*/
|
|
238
|
+
reset(): void {
|
|
239
|
+
this.#contextDelta.reset();
|
|
240
|
+
this.#pyramid.reset();
|
|
241
|
+
this.#lazySkills.reset();
|
|
242
|
+
this.#compressor.reset();
|
|
243
|
+
this.#commMode.reset();
|
|
244
|
+
this.#tombstoner.reset();
|
|
245
|
+
this.#responseCache.reset();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy Skill Loading — inject skill names only, load full content on-demand.
|
|
3
|
+
*
|
|
4
|
+
* Instead of loading all skills into context (~50,000+ tokens),
|
|
5
|
+
* inject only skill names (~200 tokens) and load full content
|
|
6
|
+
* when the model explicitly references a skill.
|
|
7
|
+
*
|
|
8
|
+
* Target: ~700 tokens vs 50,000+ if all loaded.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface SkillEntry {
|
|
12
|
+
/** Skill name (kebab-case). */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Short description for name-only injection. */
|
|
15
|
+
description: string;
|
|
16
|
+
/** Full content — only loaded on-demand. */
|
|
17
|
+
fullContent: string;
|
|
18
|
+
/** Approximate token count of full content. */
|
|
19
|
+
tokens: number;
|
|
20
|
+
/** Category for grouping. */
|
|
21
|
+
category?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LazySkillStats {
|
|
25
|
+
/** Total skills available. */
|
|
26
|
+
totalAvailable: number;
|
|
27
|
+
/** Skills injected as names only. */
|
|
28
|
+
namesInjected: number;
|
|
29
|
+
/** Skills fully loaded this session. */
|
|
30
|
+
loadedOnDemand: number;
|
|
31
|
+
/** Tokens saved by not loading all skills. */
|
|
32
|
+
tokensSaved: number;
|
|
33
|
+
/** Names injection token cost. */
|
|
34
|
+
nameTokens: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build a compact skill-name-only prompt section.
|
|
39
|
+
* Format: "- skill-name: short description"
|
|
40
|
+
* Stays within the name_budget token limit.
|
|
41
|
+
*/
|
|
42
|
+
export function buildSkillNameIndex(skills: SkillEntry[], nameBudget: number): string {
|
|
43
|
+
let tokens = 0;
|
|
44
|
+
const lines: string[] = ["## Available Skills (load on-demand with skill name)"];
|
|
45
|
+
|
|
46
|
+
for (const skill of skills) {
|
|
47
|
+
const line = `- **${skill.name}**: ${skill.description}`;
|
|
48
|
+
const lineTokens = Math.ceil(line.length / 4);
|
|
49
|
+
|
|
50
|
+
if (tokens + lineTokens > nameBudget) {
|
|
51
|
+
lines.push(`- ...and ${skills.length - lines.length + 1} more`);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lines.push(line);
|
|
56
|
+
tokens += lineTokens;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Detect which skill names are referenced in a message.
|
|
64
|
+
* Returns names of skills that should be fully loaded.
|
|
65
|
+
*/
|
|
66
|
+
export function detectSkillReferences(message: string, availableSkills: SkillEntry[]): string[] {
|
|
67
|
+
const lower = message.toLowerCase();
|
|
68
|
+
const referenced: string[] = [];
|
|
69
|
+
|
|
70
|
+
for (const skill of availableSkills) {
|
|
71
|
+
// Direct name mention
|
|
72
|
+
if (lower.includes(skill.name)) {
|
|
73
|
+
referenced.push(skill.name);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Description keyword match (at least 2 significant words)
|
|
78
|
+
const descWords = skill.description
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.split(/\s+/)
|
|
81
|
+
.filter((w) => w.length > 4);
|
|
82
|
+
const matchCount = descWords.filter((w) => lower.includes(w)).length;
|
|
83
|
+
if (matchCount >= 2) {
|
|
84
|
+
referenced.push(skill.name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return referenced;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class LazySkillLoader {
|
|
92
|
+
#skills: SkillEntry[] = [];
|
|
93
|
+
#loadedThisSession: Set<string> = new Set();
|
|
94
|
+
#stats: LazySkillStats = {
|
|
95
|
+
totalAvailable: 0,
|
|
96
|
+
namesInjected: 0,
|
|
97
|
+
loadedOnDemand: 0,
|
|
98
|
+
tokensSaved: 0,
|
|
99
|
+
nameTokens: 0,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Register available skills (called once at session start).
|
|
104
|
+
*/
|
|
105
|
+
registerSkills(skills: SkillEntry[]): void {
|
|
106
|
+
this.#skills = skills;
|
|
107
|
+
this.#stats.totalAvailable = skills.length;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get the compact skill name index for prompt injection.
|
|
112
|
+
* Returns the name-only section (~200 tokens).
|
|
113
|
+
*/
|
|
114
|
+
getNameIndex(nameBudget: number = 200): string {
|
|
115
|
+
const index = buildSkillNameIndex(this.#skills, nameBudget);
|
|
116
|
+
this.#stats.namesInjected = Math.min(this.#skills.length, this.#countLines(index));
|
|
117
|
+
this.#stats.nameTokens = Math.ceil(index.length / 4);
|
|
118
|
+
return index;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Process a user message — detect skill references and return
|
|
123
|
+
* full content for skills that should be loaded.
|
|
124
|
+
*/
|
|
125
|
+
processMessage(message: string, maxPerTurn: number = 3): {
|
|
126
|
+
skillsToLoad: SkillEntry[];
|
|
127
|
+
indexSection: string;
|
|
128
|
+
} {
|
|
129
|
+
const refs = detectSkillReferences(message, this.#skills);
|
|
130
|
+
const newRefs = refs.filter((r) => !this.#loadedThisSession.has(r));
|
|
131
|
+
|
|
132
|
+
// Limit per-turn loading
|
|
133
|
+
const toLoad = newRefs.slice(0, maxPerTurn);
|
|
134
|
+
const skillsToLoad: SkillEntry[] = [];
|
|
135
|
+
|
|
136
|
+
for (const name of toLoad) {
|
|
137
|
+
const skill = this.#skills.find((s) => s.name === name);
|
|
138
|
+
if (skill) {
|
|
139
|
+
skillsToLoad.push(skill);
|
|
140
|
+
this.#loadedThisSession.add(name);
|
|
141
|
+
this.#stats.loadedOnDemand++;
|
|
142
|
+
this.#stats.tokensSaved += skill.tokens - Math.ceil(skill.name.length / 4);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
skillsToLoad,
|
|
148
|
+
indexSection: this.getNameIndex(),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Get stats snapshot. */
|
|
153
|
+
get stats(): Readonly<LazySkillStats> {
|
|
154
|
+
return { ...this.#stats };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Get number of skills currently loaded in full. */
|
|
158
|
+
get loadedCount(): number {
|
|
159
|
+
return this.#loadedThisSession.size;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Check if a skill is already loaded. */
|
|
163
|
+
isLoaded(name: string): boolean {
|
|
164
|
+
return this.#loadedThisSession.has(name);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Reset loader state. */
|
|
168
|
+
reset(): void {
|
|
169
|
+
this.#loadedThisSession.clear();
|
|
170
|
+
this.#stats = {
|
|
171
|
+
totalAvailable: this.#skills.length,
|
|
172
|
+
namesInjected: 0,
|
|
173
|
+
loadedOnDemand: 0,
|
|
174
|
+
tokensSaved: 0,
|
|
175
|
+
nameTokens: 0,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#countLines(text: string): number {
|
|
180
|
+
return text.split("\n").filter((l) => l.startsWith("- **")).length;
|
|
181
|
+
}
|
|
182
|
+
}
|