@poolzin/pool-bot 2026.3.7 → 2026.3.9
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 +16 -0
- package/dist/.buildstamp +1 -1
- package/dist/agents/error-classifier.js +302 -0
- package/dist/agents/skills/security.js +217 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/lazy-commands.example.js +113 -0
- package/dist/cli/lazy-commands.js +329 -0
- package/dist/cli/program/command-registry.js +13 -0
- package/dist/cli/program/register.skills.js +4 -0
- package/dist/config/config.js +1 -0
- package/dist/config/secrets-integration.js +88 -0
- package/dist/context-engine/index.js +33 -0
- package/dist/context-engine/legacy.js +181 -0
- package/dist/context-engine/registry.js +86 -0
- package/dist/context-engine/summarizing.js +293 -0
- package/dist/context-engine/types.js +7 -0
- package/dist/infra/abort-pattern.js +106 -0
- package/dist/infra/retry.js +94 -0
- package/dist/secrets/index.js +28 -0
- package/dist/secrets/resolver.js +185 -0
- package/dist/secrets/runtime.js +142 -0
- package/dist/secrets/types.js +11 -0
- package/dist/security/dangerous-tools.js +80 -0
- package/dist/security/types.js +12 -0
- package/dist/skills/commands.js +351 -0
- package/dist/skills/index.js +167 -0
- package/dist/skills/loader.js +282 -0
- package/dist/skills/parser.js +461 -0
- package/dist/skills/registry.js +397 -0
- package/dist/skills/security.js +318 -0
- package/dist/skills/types.js +21 -0
- package/dist/test-utils/index.js +219 -0
- package/dist/tui/index.js +595 -0
- package/docs/INTEGRATION_PLAN.md +475 -0
- package/docs/INTEGRATION_SUMMARY.md +215 -0
- package/docs/integrations/HEXSTRIKE_PLAN.md +796 -0
- package/docs/integrations/INTEGRATION_PLAN.md +424 -0
- package/docs/integrations/PAGE_AGENT_PLAN.md +370 -0
- package/docs/integrations/XYOPS_PLAN.md +978 -0
- package/docs/skills/IMPLEMENTATION_SUMMARY.md +145 -0
- package/docs/skills/SKILL.md +524 -0
- package/docs/skills.md +405 -0
- package/package.json +1 -1
- package/skills/example-skill/SKILL.md +195 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy Command Loading System
|
|
3
|
+
*
|
|
4
|
+
* Optimizes CLI startup time by loading commands on-demand
|
|
5
|
+
* rather than loading all commands at startup.
|
|
6
|
+
*
|
|
7
|
+
* This significantly improves startup performance when only
|
|
8
|
+
* a subset of commands is used.
|
|
9
|
+
*
|
|
10
|
+
* ## Usage
|
|
11
|
+
*
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { Command } from "commander";
|
|
14
|
+
* import { lazyCommand } from "./lazy-commands.js";
|
|
15
|
+
*
|
|
16
|
+
* const program = new Command();
|
|
17
|
+
*
|
|
18
|
+
* // Register lazy-loaded command
|
|
19
|
+
* lazyCommand(program, "agent", "Manage AI agents", async () => {
|
|
20
|
+
* const { buildAgentCommand } = await import("./commands/agent.js");
|
|
21
|
+
* return buildAgentCommand();
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Global registry for lazy commands
|
|
27
|
+
*/
|
|
28
|
+
class LazyCommandRegistry {
|
|
29
|
+
commands = new Map();
|
|
30
|
+
loadTimes = new Map();
|
|
31
|
+
/**
|
|
32
|
+
* Register a lazy command
|
|
33
|
+
*/
|
|
34
|
+
register(options) {
|
|
35
|
+
if (this.commands.has(options.name)) {
|
|
36
|
+
throw new Error(`Command "${options.name}" is already registered`);
|
|
37
|
+
}
|
|
38
|
+
const entry = {
|
|
39
|
+
name: options.name,
|
|
40
|
+
description: options.description,
|
|
41
|
+
loader: options.loader,
|
|
42
|
+
aliases: options.aliases ?? [],
|
|
43
|
+
loaded: false,
|
|
44
|
+
};
|
|
45
|
+
this.commands.set(options.name, entry);
|
|
46
|
+
// Register aliases pointing to main entry
|
|
47
|
+
for (const alias of options.aliases ?? []) {
|
|
48
|
+
if (this.commands.has(alias)) {
|
|
49
|
+
throw new Error(`Alias "${alias}" is already registered`);
|
|
50
|
+
}
|
|
51
|
+
this.commands.set(alias, entry);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if a command is registered
|
|
56
|
+
*/
|
|
57
|
+
has(name) {
|
|
58
|
+
return this.commands.has(name);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get a registered command entry
|
|
62
|
+
*/
|
|
63
|
+
get(name) {
|
|
64
|
+
return this.commands.get(name);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get command loading statistics
|
|
68
|
+
*/
|
|
69
|
+
getStats() {
|
|
70
|
+
const uniqueCommands = new Set(Array.from(this.commands.values()).map((c) => c.name));
|
|
71
|
+
const total = uniqueCommands.size;
|
|
72
|
+
const loadedEntries = Array.from(this.commands.values()).filter((c) => c.loaded && !c.aliases.includes(c.name) // Count main entries only
|
|
73
|
+
);
|
|
74
|
+
const loaded = loadedEntries.length;
|
|
75
|
+
const loadTimes = {};
|
|
76
|
+
for (const [name, time] of this.loadTimes) {
|
|
77
|
+
loadTimes[name] = time;
|
|
78
|
+
}
|
|
79
|
+
const times = Object.values(loadTimes);
|
|
80
|
+
const averageLoadTime = times.length > 0 ? times.reduce((a, b) => a + b, 0) / times.length : undefined;
|
|
81
|
+
return {
|
|
82
|
+
total,
|
|
83
|
+
loaded,
|
|
84
|
+
lazy: total - loaded,
|
|
85
|
+
averageLoadTime,
|
|
86
|
+
loadTimes,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Mark a command as loaded with its load time
|
|
91
|
+
*/
|
|
92
|
+
markLoaded(name, loadTime) {
|
|
93
|
+
const entry = this.commands.get(name);
|
|
94
|
+
if (entry) {
|
|
95
|
+
entry.loaded = true;
|
|
96
|
+
entry.loadTime = loadTime;
|
|
97
|
+
this.loadTimes.set(name, loadTime);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Mark a command as failed to load
|
|
102
|
+
*/
|
|
103
|
+
markFailed(name, error) {
|
|
104
|
+
const entry = this.commands.get(name);
|
|
105
|
+
if (entry) {
|
|
106
|
+
entry.error = error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get list of registered command names (main commands only, not aliases)
|
|
111
|
+
*/
|
|
112
|
+
getCommandNames() {
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
const names = [];
|
|
115
|
+
for (const entry of this.commands.values()) {
|
|
116
|
+
if (!seen.has(entry.name)) {
|
|
117
|
+
seen.add(entry.name);
|
|
118
|
+
names.push(entry.name);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return names;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get all entries (for debugging)
|
|
125
|
+
*/
|
|
126
|
+
getAllEntries() {
|
|
127
|
+
return Array.from(this.commands.values());
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Clear all registrations
|
|
131
|
+
*/
|
|
132
|
+
clear() {
|
|
133
|
+
this.commands.clear();
|
|
134
|
+
this.loadTimes.clear();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Global registry instance
|
|
139
|
+
*/
|
|
140
|
+
export const globalLazyCommandRegistry = new LazyCommandRegistry();
|
|
141
|
+
/**
|
|
142
|
+
* Register a lazy-loaded command with a Commander program
|
|
143
|
+
*
|
|
144
|
+
* This creates a placeholder command that will load the actual
|
|
145
|
+
* implementation only when the command is invoked.
|
|
146
|
+
*
|
|
147
|
+
* @param program - The Commander program instance
|
|
148
|
+
* @param options - Lazy command configuration
|
|
149
|
+
* @returns The placeholder command (for chaining)
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* lazyCommand(program, {
|
|
154
|
+
* name: "agent",
|
|
155
|
+
* description: "Manage AI agents",
|
|
156
|
+
* loader: async () => {
|
|
157
|
+
* const { buildAgentCommand } = await import("./commands/agent.js");
|
|
158
|
+
* return buildAgentCommand();
|
|
159
|
+
* },
|
|
160
|
+
* aliases: ["a"]
|
|
161
|
+
* });
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export function lazyCommand(program, options) {
|
|
165
|
+
// Register in global registry
|
|
166
|
+
globalLazyCommandRegistry.register(options);
|
|
167
|
+
// Create placeholder command
|
|
168
|
+
const placeholder = program
|
|
169
|
+
.command(options.name)
|
|
170
|
+
.description(options.description);
|
|
171
|
+
// Add arguments if specified
|
|
172
|
+
if (options.arguments) {
|
|
173
|
+
placeholder.arguments(options.arguments);
|
|
174
|
+
}
|
|
175
|
+
// Add aliases
|
|
176
|
+
for (const alias of options.aliases ?? []) {
|
|
177
|
+
placeholder.alias(alias);
|
|
178
|
+
}
|
|
179
|
+
// Set up lazy loading action
|
|
180
|
+
placeholder.action(async (...actionArgs) => {
|
|
181
|
+
const startTime = performance.now();
|
|
182
|
+
try {
|
|
183
|
+
// Load the actual command module
|
|
184
|
+
const realCommand = await options.loader();
|
|
185
|
+
// Calculate load time
|
|
186
|
+
const loadTime = performance.now() - startTime;
|
|
187
|
+
globalLazyCommandRegistry.markLoaded(options.name, loadTime);
|
|
188
|
+
// Execute the loaded command's action with the same arguments
|
|
189
|
+
// The loaded command should have its own action handler
|
|
190
|
+
if (realCommand && typeof realCommand.parseAsync === "function") {
|
|
191
|
+
// If it's a full Command instance, we need to execute its action
|
|
192
|
+
// Get the action handler from the loaded command
|
|
193
|
+
const actionHandler = realCommand._actionHandler;
|
|
194
|
+
if (actionHandler) {
|
|
195
|
+
await actionHandler(...actionArgs);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// Fallback: parse with the same arguments
|
|
199
|
+
// Remove command object from args and re-parse
|
|
200
|
+
const argv = process.argv.slice(2);
|
|
201
|
+
await realCommand.parseAsync(argv);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
throw new Error(`Loader for "${options.name}" did not return a valid Command instance`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
210
|
+
globalLazyCommandRegistry.markFailed(options.name, err);
|
|
211
|
+
throw new Error(`Failed to load command "${options.name}": ${err.message}`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return placeholder;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Simple shorthand for creating a lazy command with minimal config
|
|
218
|
+
*
|
|
219
|
+
* @param program - The Commander program
|
|
220
|
+
* @param name - Command name
|
|
221
|
+
* @param description - Command description
|
|
222
|
+
* @param loader - Async function that loads and returns the command
|
|
223
|
+
* @returns The placeholder command
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```typescript
|
|
227
|
+
* lazy(program, "agent", "Manage agents", async () => {
|
|
228
|
+
* const mod = await import("./commands/agent.js");
|
|
229
|
+
* return mod.buildAgentCommand();
|
|
230
|
+
* });
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
export function lazy(program, name, description, loader) {
|
|
234
|
+
return lazyCommand(program, { name, description, loader });
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Preload commands that are likely to be used
|
|
238
|
+
*
|
|
239
|
+
* This can be called after initial setup to preload commands
|
|
240
|
+
* in the background, improving responsiveness for common commands.
|
|
241
|
+
*
|
|
242
|
+
* @param commandNames - Names of commands to preload
|
|
243
|
+
* @returns Promise that resolves when all preloads are complete
|
|
244
|
+
*/
|
|
245
|
+
export async function preloadCommands(commandNames) {
|
|
246
|
+
await Promise.all(commandNames.map(async (name) => {
|
|
247
|
+
const entry = globalLazyCommandRegistry.get(name);
|
|
248
|
+
if (entry && !entry.loaded) {
|
|
249
|
+
try {
|
|
250
|
+
const startTime = performance.now();
|
|
251
|
+
await entry.loader();
|
|
252
|
+
globalLazyCommandRegistry.markLoaded(name, performance.now() - startTime);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
// Preload errors are non-fatal
|
|
256
|
+
console.warn(`Failed to preload command "${name}": ${error instanceof Error ? error.message : String(error)}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Eagerly load all registered commands
|
|
263
|
+
*
|
|
264
|
+
* Use this for testing or when startup time doesn't matter.
|
|
265
|
+
*
|
|
266
|
+
* @returns Promise that resolves when all commands are loaded
|
|
267
|
+
*/
|
|
268
|
+
export async function loadAllCommands() {
|
|
269
|
+
const names = globalLazyCommandRegistry.getCommandNames();
|
|
270
|
+
await preloadCommands(names);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Get current loading statistics
|
|
274
|
+
*
|
|
275
|
+
* @returns Statistics about lazy-loaded commands
|
|
276
|
+
*/
|
|
277
|
+
export function getLazyCommandStats() {
|
|
278
|
+
return globalLazyCommandRegistry.getStats();
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Performance measurement utilities for command loading
|
|
282
|
+
*/
|
|
283
|
+
export class LoadTimeTracker {
|
|
284
|
+
measurements = new Map();
|
|
285
|
+
/**
|
|
286
|
+
* Measure execution time of a function
|
|
287
|
+
*/
|
|
288
|
+
async measure(name, fn) {
|
|
289
|
+
const start = performance.now();
|
|
290
|
+
try {
|
|
291
|
+
return await fn();
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
const duration = performance.now() - start;
|
|
295
|
+
this.measurements.set(name, duration);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Get all measurements
|
|
300
|
+
*/
|
|
301
|
+
getMeasurements() {
|
|
302
|
+
return Object.fromEntries(this.measurements);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Get total loading time
|
|
306
|
+
*/
|
|
307
|
+
getTotalTime() {
|
|
308
|
+
return Array.from(this.measurements.values()).reduce((a, b) => a + b, 0);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Get slowest operations
|
|
312
|
+
*/
|
|
313
|
+
getSlowest(count = 5) {
|
|
314
|
+
return Array.from(this.measurements.entries())
|
|
315
|
+
.sort((a, b) => b[1] - a[1])
|
|
316
|
+
.slice(0, count)
|
|
317
|
+
.map(([name, time]) => ({ name, time }));
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Clear all measurements
|
|
321
|
+
*/
|
|
322
|
+
clear() {
|
|
323
|
+
this.measurements.clear();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Global load time tracker
|
|
328
|
+
*/
|
|
329
|
+
export const globalLoadTimeTracker = new LoadTimeTracker();
|
|
@@ -174,6 +174,19 @@ const coreEntries = [
|
|
|
174
174
|
mod.registerBrowserCli(program);
|
|
175
175
|
},
|
|
176
176
|
},
|
|
177
|
+
{
|
|
178
|
+
commands: [
|
|
179
|
+
{
|
|
180
|
+
name: "mods",
|
|
181
|
+
description: "Manage capability modules (SKILL.md discovery, enable, configure)",
|
|
182
|
+
hasSubcommands: true,
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
register: async ({ program }) => {
|
|
186
|
+
const mod = await import("./register.skills.js");
|
|
187
|
+
mod.registerSkillsCli(program);
|
|
188
|
+
},
|
|
189
|
+
},
|
|
177
190
|
];
|
|
178
191
|
function collectCoreCliCommandNames(predicate) {
|
|
179
192
|
const seen = new Set();
|
package/dist/config/config.js
CHANGED
|
@@ -2,6 +2,7 @@ export { createConfigIO, loadConfig, parseConfigJson5, readConfigFileSnapshot, r
|
|
|
2
2
|
export { migrateLegacyConfig } from "./legacy-migrate.js";
|
|
3
3
|
export * from "./paths.js";
|
|
4
4
|
export * from "./runtime-overrides.js";
|
|
5
|
+
export * from "./secrets-integration.js";
|
|
5
6
|
export * from "./types.js";
|
|
6
7
|
export { validateConfigObject, validateConfigObjectWithPlugins } from "./validation.js";
|
|
7
8
|
export { PoolBotSchema } from "./zod-schema.js";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Secrets Integration
|
|
3
|
+
*
|
|
4
|
+
* Integrates the secrets management system with config loading.
|
|
5
|
+
* Resolves SecretRefs in config values at runtime.
|
|
6
|
+
*/
|
|
7
|
+
import { SecretsRuntime } from "../secrets/index.js";
|
|
8
|
+
const SECRET_PATHS = [
|
|
9
|
+
"agent.model",
|
|
10
|
+
"agent.apiKey",
|
|
11
|
+
"channels.telegram.botToken",
|
|
12
|
+
"channels.discord.token",
|
|
13
|
+
"channels.slack.botToken",
|
|
14
|
+
"channels.slack.appToken",
|
|
15
|
+
"gateway.auth.token",
|
|
16
|
+
"gateway.auth.password",
|
|
17
|
+
"memory.apiKey",
|
|
18
|
+
"tools.browser.apiKey",
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Deeply traverse config and resolve secret references
|
|
22
|
+
*/
|
|
23
|
+
export function resolveConfigSecrets(config, secretsRuntime) {
|
|
24
|
+
const resolved = { ...config };
|
|
25
|
+
// Resolve specific known secret paths
|
|
26
|
+
for (const path of SECRET_PATHS) {
|
|
27
|
+
resolvePath(resolved, path.split("."), secretsRuntime);
|
|
28
|
+
}
|
|
29
|
+
// Also scan for any string values that look like secret refs
|
|
30
|
+
scanAndResolveSecrets(resolved, "", secretsRuntime);
|
|
31
|
+
return resolved;
|
|
32
|
+
}
|
|
33
|
+
function resolvePath(obj, path, runtime) {
|
|
34
|
+
if (path.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
const [head, ...tail] = path;
|
|
37
|
+
if (!(head in obj))
|
|
38
|
+
return;
|
|
39
|
+
if (tail.length === 0) {
|
|
40
|
+
// Leaf node - resolve if it's a string
|
|
41
|
+
const value = obj[head];
|
|
42
|
+
if (typeof value === "string") {
|
|
43
|
+
const result = runtime.resolve(head, value);
|
|
44
|
+
if (result.value !== undefined) {
|
|
45
|
+
obj[head] = result.value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Traverse deeper
|
|
51
|
+
const child = obj[head];
|
|
52
|
+
if (typeof child === "object" && child !== null && !Array.isArray(child)) {
|
|
53
|
+
resolvePath(child, tail, runtime);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function scanAndResolveSecrets(obj, path, runtime) {
|
|
58
|
+
if (typeof obj === "string") {
|
|
59
|
+
// Check if it looks like a secret ref
|
|
60
|
+
if (obj.startsWith("env:") || obj.startsWith("file:")) {
|
|
61
|
+
runtime.resolve(path || "unknown", obj);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(obj)) {
|
|
66
|
+
obj.forEach((item, index) => {
|
|
67
|
+
scanAndResolveSecrets(item, `${path}[${index}]`, runtime);
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (typeof obj === "object" && obj !== null) {
|
|
72
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
73
|
+
const newPath = path ? `${path}.${key}` : key;
|
|
74
|
+
scanAndResolveSecrets(value, newPath, runtime);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create a secrets runtime from config
|
|
80
|
+
*/
|
|
81
|
+
export function createSecretsRuntimeFromConfig(_config, secretsConfig) {
|
|
82
|
+
return new SecretsRuntime({
|
|
83
|
+
allowDirectValues: true,
|
|
84
|
+
warnOnDirectValues: true,
|
|
85
|
+
failOnUnresolved: false,
|
|
86
|
+
...secretsConfig,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Engine - Pluggable conversation context management
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { LegacyContextEngine, globalContextEngineRegistry } from './context-engine/index.js';
|
|
7
|
+
*
|
|
8
|
+
* // Register the legacy engine
|
|
9
|
+
* globalContextEngineRegistry.register('legacy', LegacyContextEngine);
|
|
10
|
+
*
|
|
11
|
+
* // Get engine instance
|
|
12
|
+
* const engine = globalContextEngineRegistry.getEngine('legacy');
|
|
13
|
+
*
|
|
14
|
+
* // Bootstrap for a session
|
|
15
|
+
* await engine.bootstrap({ sessionId: 'session-123' });
|
|
16
|
+
*
|
|
17
|
+
* // Ingest a message
|
|
18
|
+
* await engine.ingest({
|
|
19
|
+
* sessionId: 'session-123',
|
|
20
|
+
* message: { role: 'user', content: 'Hello!' }
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* // Assemble context
|
|
24
|
+
* const result = await engine.assemble({
|
|
25
|
+
* sessionId: 'session-123',
|
|
26
|
+
* messages: []
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
// Registry
|
|
31
|
+
export { ContextEngineRegistry, globalContextEngineRegistry, resolveContextEngine, } from "./registry.js";
|
|
32
|
+
// Legacy Engine
|
|
33
|
+
export { LegacyContextEngine, createLegacyContextEngine, } from "./legacy.js";
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy Context Engine
|
|
3
|
+
*
|
|
4
|
+
* Maintains PoolBot's original context management behavior.
|
|
5
|
+
* This is the default engine that preserves backward compatibility.
|
|
6
|
+
*
|
|
7
|
+
* Strategy:
|
|
8
|
+
* - Simple message accumulation
|
|
9
|
+
* - Truncation-based overflow handling
|
|
10
|
+
* - Basic compaction via summarization
|
|
11
|
+
*/
|
|
12
|
+
export class LegacyContextEngine {
|
|
13
|
+
info = {
|
|
14
|
+
id: "legacy",
|
|
15
|
+
name: "Legacy Context Engine",
|
|
16
|
+
description: "PoolBot's original context management with simple accumulation and truncation",
|
|
17
|
+
supportsVectorSearch: false,
|
|
18
|
+
supportsFullTextSearch: false,
|
|
19
|
+
tokenLimit: undefined, // No explicit limit
|
|
20
|
+
};
|
|
21
|
+
sessions = new Map();
|
|
22
|
+
async bootstrap(params) {
|
|
23
|
+
try {
|
|
24
|
+
// Initialize session storage
|
|
25
|
+
this.sessions.set(params.sessionId, {
|
|
26
|
+
messages: [],
|
|
27
|
+
lastAccessed: new Date(),
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
error: error instanceof Error ? error.message : String(error),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async ingest(params) {
|
|
41
|
+
try {
|
|
42
|
+
const session = this.sessions.get(params.sessionId);
|
|
43
|
+
if (!session) {
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
error: `Session ${params.sessionId} not found`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
session.messages.push(params.message);
|
|
50
|
+
session.lastAccessed = new Date();
|
|
51
|
+
// Estimate tokens (rough approximation)
|
|
52
|
+
const content = typeof params.message.content === "string"
|
|
53
|
+
? params.message.content
|
|
54
|
+
: JSON.stringify(params.message.content);
|
|
55
|
+
const estimatedTokens = Math.ceil(content.length / 4);
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
tokensConsumed: estimatedTokens,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
error: error instanceof Error ? error.message : String(error),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async assemble(params) {
|
|
69
|
+
const session = this.sessions.get(params.sessionId);
|
|
70
|
+
const messages = session?.messages ?? params.messages;
|
|
71
|
+
// Apply token budget if specified
|
|
72
|
+
let resultMessages = messages;
|
|
73
|
+
let wasTruncated = false;
|
|
74
|
+
let excludedMessages = [];
|
|
75
|
+
if (params.tokenBudget && params.tokenBudget > 0) {
|
|
76
|
+
const estimate = this.estimateTokenCount(messages);
|
|
77
|
+
if (estimate > params.tokenBudget) {
|
|
78
|
+
// Simple truncation: keep newest messages that fit
|
|
79
|
+
const reversed = [...messages].reverse();
|
|
80
|
+
const fitting = [];
|
|
81
|
+
let currentTokens = 0;
|
|
82
|
+
for (const msg of reversed) {
|
|
83
|
+
const msgTokens = this.estimateTokenCount([msg]);
|
|
84
|
+
if (currentTokens + msgTokens <= params.tokenBudget) {
|
|
85
|
+
fitting.unshift(msg);
|
|
86
|
+
currentTokens += msgTokens;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
excludedMessages.unshift(msg);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
resultMessages = fitting;
|
|
93
|
+
wasTruncated = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
messages: resultMessages,
|
|
98
|
+
totalTokens: this.estimateTokenCount(resultMessages),
|
|
99
|
+
wasTruncated,
|
|
100
|
+
excludedMessages: wasTruncated ? excludedMessages : undefined,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async compact(params) {
|
|
104
|
+
try {
|
|
105
|
+
const session = this.sessions.get(params.sessionId);
|
|
106
|
+
if (!session || session.messages.length === 0) {
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
tokensSaved: 0,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const originalTokens = this.estimateTokenCount(session.messages);
|
|
113
|
+
// Legacy compaction: summarize older messages
|
|
114
|
+
// Keep recent messages, summarize older ones
|
|
115
|
+
const keepRecent = 4; // Keep last 2 exchanges
|
|
116
|
+
const toSummarize = session.messages.slice(0, -keepRecent);
|
|
117
|
+
const toKeep = session.messages.slice(-keepRecent);
|
|
118
|
+
if (toSummarize.length === 0) {
|
|
119
|
+
return {
|
|
120
|
+
success: true,
|
|
121
|
+
tokensSaved: 0,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// Create summary message
|
|
125
|
+
const summary = {
|
|
126
|
+
role: "system",
|
|
127
|
+
content: `[${toSummarize.length} earlier messages summarized]`,
|
|
128
|
+
metadata: { compacted: true, originalCount: toSummarize.length },
|
|
129
|
+
};
|
|
130
|
+
session.messages = [summary, ...toKeep];
|
|
131
|
+
const newTokens = this.estimateTokenCount(session.messages);
|
|
132
|
+
return {
|
|
133
|
+
success: true,
|
|
134
|
+
summary: summary.content,
|
|
135
|
+
tokensSaved: originalTokens - newTokens,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
error: error instanceof Error ? error.message : String(error),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async cleanup(params) {
|
|
146
|
+
this.sessions.delete(params.sessionId);
|
|
147
|
+
}
|
|
148
|
+
async getStats(params) {
|
|
149
|
+
const session = this.sessions.get(params.sessionId);
|
|
150
|
+
if (!session) {
|
|
151
|
+
return {
|
|
152
|
+
messageCount: 0,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
messageCount: session.messages.length,
|
|
157
|
+
totalTokens: this.estimateTokenCount(session.messages),
|
|
158
|
+
lastAccessed: session.lastAccessed,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Estimate token count for messages
|
|
163
|
+
* Rough approximation: ~4 characters per token
|
|
164
|
+
*/
|
|
165
|
+
estimateTokenCount(messages) {
|
|
166
|
+
let totalChars = 0;
|
|
167
|
+
for (const msg of messages) {
|
|
168
|
+
const content = typeof msg.content === "string"
|
|
169
|
+
? msg.content
|
|
170
|
+
: JSON.stringify(msg.content);
|
|
171
|
+
totalChars += content.length;
|
|
172
|
+
}
|
|
173
|
+
return Math.ceil(totalChars / 4);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Create the default legacy context engine
|
|
178
|
+
*/
|
|
179
|
+
export function createLegacyContextEngine() {
|
|
180
|
+
return new LegacyContextEngine();
|
|
181
|
+
}
|