@lleverage-ai/agent-sdk 0.0.1
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 +2321 -0
- package/dist/agent.d.ts +52 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +2122 -0
- package/dist/agent.js.map +1 -0
- package/dist/backend.d.ts +378 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +71 -0
- package/dist/backend.js.map +1 -0
- package/dist/backends/composite.d.ts +258 -0
- package/dist/backends/composite.d.ts.map +1 -0
- package/dist/backends/composite.js +437 -0
- package/dist/backends/composite.js.map +1 -0
- package/dist/backends/filesystem.d.ts +268 -0
- package/dist/backends/filesystem.d.ts.map +1 -0
- package/dist/backends/filesystem.js +623 -0
- package/dist/backends/filesystem.js.map +1 -0
- package/dist/backends/index.d.ts +14 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/index.js +14 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/persistent.d.ts +312 -0
- package/dist/backends/persistent.d.ts.map +1 -0
- package/dist/backends/persistent.js +519 -0
- package/dist/backends/persistent.js.map +1 -0
- package/dist/backends/sandbox.d.ts +315 -0
- package/dist/backends/sandbox.d.ts.map +1 -0
- package/dist/backends/sandbox.js +490 -0
- package/dist/backends/sandbox.js.map +1 -0
- package/dist/backends/state.d.ts +225 -0
- package/dist/backends/state.d.ts.map +1 -0
- package/dist/backends/state.js +396 -0
- package/dist/backends/state.js.map +1 -0
- package/dist/checkpointer/file-saver.d.ts +182 -0
- package/dist/checkpointer/file-saver.d.ts.map +1 -0
- package/dist/checkpointer/file-saver.js +298 -0
- package/dist/checkpointer/file-saver.js.map +1 -0
- package/dist/checkpointer/index.d.ts +40 -0
- package/dist/checkpointer/index.d.ts.map +1 -0
- package/dist/checkpointer/index.js +40 -0
- package/dist/checkpointer/index.js.map +1 -0
- package/dist/checkpointer/kv-saver.d.ts +142 -0
- package/dist/checkpointer/kv-saver.d.ts.map +1 -0
- package/dist/checkpointer/kv-saver.js +176 -0
- package/dist/checkpointer/kv-saver.js.map +1 -0
- package/dist/checkpointer/memory-saver.d.ts +158 -0
- package/dist/checkpointer/memory-saver.d.ts.map +1 -0
- package/dist/checkpointer/memory-saver.js +222 -0
- package/dist/checkpointer/memory-saver.js.map +1 -0
- package/dist/checkpointer/types.d.ts +353 -0
- package/dist/checkpointer/types.d.ts.map +1 -0
- package/dist/checkpointer/types.js +159 -0
- package/dist/checkpointer/types.js.map +1 -0
- package/dist/context-manager.d.ts +627 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +1039 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/context.d.ts +57 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +76 -0
- package/dist/context.js.map +1 -0
- package/dist/errors/index.d.ts +611 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +1023 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/generation-helpers.d.ts +126 -0
- package/dist/generation-helpers.d.ts.map +1 -0
- package/dist/generation-helpers.js +181 -0
- package/dist/generation-helpers.js.map +1 -0
- package/dist/hooks/audit.d.ts +210 -0
- package/dist/hooks/audit.d.ts.map +1 -0
- package/dist/hooks/audit.js +305 -0
- package/dist/hooks/audit.js.map +1 -0
- package/dist/hooks/cache.d.ts +180 -0
- package/dist/hooks/cache.d.ts.map +1 -0
- package/dist/hooks/cache.js +273 -0
- package/dist/hooks/cache.js.map +1 -0
- package/dist/hooks/guardrails.d.ts +145 -0
- package/dist/hooks/guardrails.d.ts.map +1 -0
- package/dist/hooks/guardrails.js +326 -0
- package/dist/hooks/guardrails.js.map +1 -0
- package/dist/hooks/index.d.ts +18 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +32 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/logging.d.ts +193 -0
- package/dist/hooks/logging.d.ts.map +1 -0
- package/dist/hooks/logging.js +345 -0
- package/dist/hooks/logging.js.map +1 -0
- package/dist/hooks/parallel-guardrails.d.ts +268 -0
- package/dist/hooks/parallel-guardrails.d.ts.map +1 -0
- package/dist/hooks/parallel-guardrails.js +416 -0
- package/dist/hooks/parallel-guardrails.js.map +1 -0
- package/dist/hooks/rate-limit.d.ts +305 -0
- package/dist/hooks/rate-limit.d.ts.map +1 -0
- package/dist/hooks/rate-limit.js +372 -0
- package/dist/hooks/rate-limit.js.map +1 -0
- package/dist/hooks/retry.d.ts +144 -0
- package/dist/hooks/retry.d.ts.map +1 -0
- package/dist/hooks/retry.js +210 -0
- package/dist/hooks/retry.js.map +1 -0
- package/dist/hooks/secrets.d.ts +174 -0
- package/dist/hooks/secrets.d.ts.map +1 -0
- package/dist/hooks/secrets.js +306 -0
- package/dist/hooks/secrets.js.map +1 -0
- package/dist/hooks.d.ts +229 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +352 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +182 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/env.d.ts +25 -0
- package/dist/mcp/env.d.ts.map +1 -0
- package/dist/mcp/env.js +18 -0
- package/dist/mcp/env.js.map +1 -0
- package/dist/mcp/index.d.ts +16 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +17 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/manager.d.ts +184 -0
- package/dist/mcp/manager.d.ts.map +1 -0
- package/dist/mcp/manager.js +446 -0
- package/dist/mcp/manager.js.map +1 -0
- package/dist/mcp/types.d.ts +58 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/mcp/validation.d.ts +119 -0
- package/dist/mcp/validation.d.ts.map +1 -0
- package/dist/mcp/validation.js +407 -0
- package/dist/mcp/validation.js.map +1 -0
- package/dist/mcp/virtual-server.d.ts +78 -0
- package/dist/mcp/virtual-server.d.ts.map +1 -0
- package/dist/mcp/virtual-server.js +137 -0
- package/dist/mcp/virtual-server.js.map +1 -0
- package/dist/memory/filesystem-store.d.ts +217 -0
- package/dist/memory/filesystem-store.d.ts.map +1 -0
- package/dist/memory/filesystem-store.js +343 -0
- package/dist/memory/filesystem-store.js.map +1 -0
- package/dist/memory/index.d.ts +46 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +46 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/loader.d.ts +396 -0
- package/dist/memory/loader.d.ts.map +1 -0
- package/dist/memory/loader.js +419 -0
- package/dist/memory/loader.js.map +1 -0
- package/dist/memory/permissions.d.ts +282 -0
- package/dist/memory/permissions.d.ts.map +1 -0
- package/dist/memory/permissions.js +297 -0
- package/dist/memory/permissions.js.map +1 -0
- package/dist/memory/rules.d.ts +249 -0
- package/dist/memory/rules.d.ts.map +1 -0
- package/dist/memory/rules.js +362 -0
- package/dist/memory/rules.js.map +1 -0
- package/dist/memory/store.d.ts +286 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +263 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/middleware/apply.d.ts +73 -0
- package/dist/middleware/apply.d.ts.map +1 -0
- package/dist/middleware/apply.js +219 -0
- package/dist/middleware/apply.js.map +1 -0
- package/dist/middleware/context.d.ts +33 -0
- package/dist/middleware/context.d.ts.map +1 -0
- package/dist/middleware/context.js +176 -0
- package/dist/middleware/context.js.map +1 -0
- package/dist/middleware/index.d.ts +31 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +32 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/logging.d.ts +137 -0
- package/dist/middleware/logging.d.ts.map +1 -0
- package/dist/middleware/logging.js +374 -0
- package/dist/middleware/logging.js.map +1 -0
- package/dist/middleware/types.d.ts +183 -0
- package/dist/middleware/types.d.ts.map +1 -0
- package/dist/middleware/types.js +11 -0
- package/dist/middleware/types.js.map +1 -0
- package/dist/observability/events.d.ts +183 -0
- package/dist/observability/events.d.ts.map +1 -0
- package/dist/observability/events.js +305 -0
- package/dist/observability/events.js.map +1 -0
- package/dist/observability/index.d.ts +55 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +87 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/logger.d.ts +318 -0
- package/dist/observability/logger.d.ts.map +1 -0
- package/dist/observability/logger.js +436 -0
- package/dist/observability/logger.js.map +1 -0
- package/dist/observability/metrics.d.ts +341 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +490 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/preset.d.ts +161 -0
- package/dist/observability/preset.d.ts.map +1 -0
- package/dist/observability/preset.js +133 -0
- package/dist/observability/preset.js.map +1 -0
- package/dist/observability/streaming.d.ts +113 -0
- package/dist/observability/streaming.d.ts.map +1 -0
- package/dist/observability/streaming.js +114 -0
- package/dist/observability/streaming.js.map +1 -0
- package/dist/observability/tracing.d.ts +378 -0
- package/dist/observability/tracing.d.ts.map +1 -0
- package/dist/observability/tracing.js +539 -0
- package/dist/observability/tracing.js.map +1 -0
- package/dist/plugins.d.ts +55 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +63 -0
- package/dist/plugins.js.map +1 -0
- package/dist/presets/index.d.ts +7 -0
- package/dist/presets/index.d.ts.map +1 -0
- package/dist/presets/index.js +7 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/presets/production.d.ts +262 -0
- package/dist/presets/production.d.ts.map +1 -0
- package/dist/presets/production.js +295 -0
- package/dist/presets/production.js.map +1 -0
- package/dist/security/index.d.ts +179 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +323 -0
- package/dist/security/index.js.map +1 -0
- package/dist/subagents/advanced.d.ts +413 -0
- package/dist/subagents/advanced.d.ts.map +1 -0
- package/dist/subagents/advanced.js +396 -0
- package/dist/subagents/advanced.js.map +1 -0
- package/dist/subagents/index.d.ts +14 -0
- package/dist/subagents/index.d.ts.map +1 -0
- package/dist/subagents/index.js +15 -0
- package/dist/subagents/index.js.map +1 -0
- package/dist/subagents.d.ts +73 -0
- package/dist/subagents.d.ts.map +1 -0
- package/dist/subagents.js +213 -0
- package/dist/subagents.js.map +1 -0
- package/dist/task-store/file-store.d.ts +76 -0
- package/dist/task-store/file-store.d.ts.map +1 -0
- package/dist/task-store/file-store.js +190 -0
- package/dist/task-store/file-store.js.map +1 -0
- package/dist/task-store/index.d.ts +11 -0
- package/dist/task-store/index.d.ts.map +1 -0
- package/dist/task-store/index.js +10 -0
- package/dist/task-store/index.js.map +1 -0
- package/dist/task-store/kv-store.d.ts +140 -0
- package/dist/task-store/kv-store.d.ts.map +1 -0
- package/dist/task-store/kv-store.js +169 -0
- package/dist/task-store/kv-store.js.map +1 -0
- package/dist/task-store/memory-store.d.ts +66 -0
- package/dist/task-store/memory-store.d.ts.map +1 -0
- package/dist/task-store/memory-store.js +125 -0
- package/dist/task-store/memory-store.js.map +1 -0
- package/dist/task-store/types.d.ts +235 -0
- package/dist/task-store/types.d.ts.map +1 -0
- package/dist/task-store/types.js +110 -0
- package/dist/task-store/types.js.map +1 -0
- package/dist/testing/assertions.d.ts +401 -0
- package/dist/testing/assertions.d.ts.map +1 -0
- package/dist/testing/assertions.js +630 -0
- package/dist/testing/assertions.js.map +1 -0
- package/dist/testing/index.d.ts +343 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +360 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-agent.d.ts +214 -0
- package/dist/testing/mock-agent.d.ts.map +1 -0
- package/dist/testing/mock-agent.js +448 -0
- package/dist/testing/mock-agent.js.map +1 -0
- package/dist/testing/recorder.d.ts +288 -0
- package/dist/testing/recorder.d.ts.map +1 -0
- package/dist/testing/recorder.js +499 -0
- package/dist/testing/recorder.js.map +1 -0
- package/dist/tools/execute.d.ts +104 -0
- package/dist/tools/execute.d.ts.map +1 -0
- package/dist/tools/execute.js +191 -0
- package/dist/tools/execute.js.map +1 -0
- package/dist/tools/factory.d.ts +260 -0
- package/dist/tools/factory.d.ts.map +1 -0
- package/dist/tools/factory.js +241 -0
- package/dist/tools/factory.js.map +1 -0
- package/dist/tools/filesystem.d.ts +215 -0
- package/dist/tools/filesystem.d.ts.map +1 -0
- package/dist/tools/filesystem.js +311 -0
- package/dist/tools/filesystem.js.map +1 -0
- package/dist/tools/index.d.ts +33 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +33 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/search.d.ts +59 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +94 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/skills.d.ts +354 -0
- package/dist/tools/skills.d.ts.map +1 -0
- package/dist/tools/skills.js +413 -0
- package/dist/tools/skills.js.map +1 -0
- package/dist/tools/task.d.ts +272 -0
- package/dist/tools/task.d.ts.map +1 -0
- package/dist/tools/task.js +521 -0
- package/dist/tools/task.js.map +1 -0
- package/dist/tools/todos.d.ts +131 -0
- package/dist/tools/todos.d.ts.map +1 -0
- package/dist/tools/todos.js +120 -0
- package/dist/tools/todos.js.map +1 -0
- package/dist/tools/tool-registry.d.ts +424 -0
- package/dist/tools/tool-registry.d.ts.map +1 -0
- package/dist/tools/tool-registry.js +607 -0
- package/dist/tools/tool-registry.js.map +1 -0
- package/dist/tools/user-interaction.d.ts +116 -0
- package/dist/tools/user-interaction.d.ts.map +1 -0
- package/dist/tools/user-interaction.js +147 -0
- package/dist/tools/user-interaction.js.map +1 -0
- package/dist/tools/utils.d.ts +124 -0
- package/dist/tools/utils.d.ts.map +1 -0
- package/dist/tools/utils.js +189 -0
- package/dist/tools/utils.js.map +1 -0
- package/dist/tools.d.ts +74 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +73 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +2421 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +55 -0
- package/dist/types.js.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context management for token tracking and auto-compaction.
|
|
3
|
+
*
|
|
4
|
+
* This module provides tools for managing conversation context, including:
|
|
5
|
+
* - Token counting and budget tracking
|
|
6
|
+
* - Automatic context compaction/summarization
|
|
7
|
+
* - Message history management
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Helper function to create a hash for a message for caching purposes.
|
|
13
|
+
* Uses a simple hash of the serialized message content.
|
|
14
|
+
*
|
|
15
|
+
* @param message - The message to hash
|
|
16
|
+
* @returns A hash string for the message
|
|
17
|
+
*/
|
|
18
|
+
function hashMessage(message) {
|
|
19
|
+
// Create a stable string representation
|
|
20
|
+
let content;
|
|
21
|
+
if (typeof message.content === "string") {
|
|
22
|
+
content = message.content;
|
|
23
|
+
}
|
|
24
|
+
else if (Array.isArray(message.content)) {
|
|
25
|
+
// For array content, create a hash-friendly representation
|
|
26
|
+
// For images/files, include type and a marker (not the full data)
|
|
27
|
+
content = message.content
|
|
28
|
+
.map((part) => {
|
|
29
|
+
if ("text" in part)
|
|
30
|
+
return `text:${part.text}`;
|
|
31
|
+
if ("image" in part)
|
|
32
|
+
return `image:${part.type}`;
|
|
33
|
+
if ("data" in part && part.type === "file")
|
|
34
|
+
return `file:${part.type}`;
|
|
35
|
+
if ("toolName" in part)
|
|
36
|
+
return `tool:${part.toolName}`;
|
|
37
|
+
if ("result" in part)
|
|
38
|
+
return `result:${JSON.stringify(part.result)}`;
|
|
39
|
+
if ("output" in part)
|
|
40
|
+
return `output:${JSON.stringify(part.output)}`;
|
|
41
|
+
return JSON.stringify(part);
|
|
42
|
+
})
|
|
43
|
+
.join("|");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
content = JSON.stringify(message.content);
|
|
47
|
+
}
|
|
48
|
+
// Simple hash function (djb2)
|
|
49
|
+
let hash = 5381;
|
|
50
|
+
for (let i = 0; i < content.length; i++) {
|
|
51
|
+
hash = (hash << 5) + hash + content.charCodeAt(i); /* hash * 33 + c */
|
|
52
|
+
}
|
|
53
|
+
return `${message.role}:${hash}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Creates a simple token counter using character approximation.
|
|
57
|
+
*
|
|
58
|
+
* Uses the common approximation of 4 characters per token.
|
|
59
|
+
* This is faster but less accurate than model-specific tokenizers.
|
|
60
|
+
* Includes a message-level cache to avoid re-counting identical messages.
|
|
61
|
+
*
|
|
62
|
+
* @returns A token counter using character approximation
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* const counter = createApproximateTokenCounter();
|
|
67
|
+
* const tokens = counter.count("Hello, world!"); // ~3 tokens
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @category Context
|
|
71
|
+
*/
|
|
72
|
+
export function createApproximateTokenCounter() {
|
|
73
|
+
const CHARS_PER_TOKEN = 4;
|
|
74
|
+
const messageCache = new Map();
|
|
75
|
+
const count = (text) => {
|
|
76
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
77
|
+
};
|
|
78
|
+
const countSingleMessage = (message) => {
|
|
79
|
+
const hash = hashMessage(message);
|
|
80
|
+
// Check cache
|
|
81
|
+
const cached = messageCache.get(hash);
|
|
82
|
+
if (cached !== undefined) {
|
|
83
|
+
return cached;
|
|
84
|
+
}
|
|
85
|
+
let total = 0;
|
|
86
|
+
// Count message overhead (role, structure)
|
|
87
|
+
total += 4; // Approximate overhead per message
|
|
88
|
+
// Count content
|
|
89
|
+
if (typeof message.content === "string") {
|
|
90
|
+
total += count(message.content);
|
|
91
|
+
}
|
|
92
|
+
else if (Array.isArray(message.content)) {
|
|
93
|
+
for (const part of message.content) {
|
|
94
|
+
if ("text" in part && typeof part.text === "string") {
|
|
95
|
+
total += count(part.text);
|
|
96
|
+
}
|
|
97
|
+
else if ("toolName" in part) {
|
|
98
|
+
// Tool call - count name and args
|
|
99
|
+
total += count(part.toolName);
|
|
100
|
+
if ("args" in part) {
|
|
101
|
+
total += count(JSON.stringify(part.args));
|
|
102
|
+
}
|
|
103
|
+
if ("input" in part) {
|
|
104
|
+
total += count(JSON.stringify(part.input));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if ("result" in part || "output" in part) {
|
|
108
|
+
// Tool result - count output
|
|
109
|
+
const output = "result" in part ? part.result : part.output;
|
|
110
|
+
total += count(typeof output === "string" ? output : JSON.stringify(output));
|
|
111
|
+
}
|
|
112
|
+
else if ("image" in part) {
|
|
113
|
+
// Image part - count ~1000 tokens for image (approximate vision model cost)
|
|
114
|
+
// Images are expensive in terms of tokens, varies by size and model
|
|
115
|
+
total += 1000;
|
|
116
|
+
}
|
|
117
|
+
else if ("data" in part && part.type === "file") {
|
|
118
|
+
// File part - count as ~500 tokens per file (placeholder)
|
|
119
|
+
// Actual token count depends on file size and content
|
|
120
|
+
total += 500;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Store in cache
|
|
125
|
+
messageCache.set(hash, total);
|
|
126
|
+
return total;
|
|
127
|
+
};
|
|
128
|
+
const countMessages = (messages) => {
|
|
129
|
+
let total = 0;
|
|
130
|
+
for (const message of messages) {
|
|
131
|
+
total += countSingleMessage(message);
|
|
132
|
+
}
|
|
133
|
+
return total;
|
|
134
|
+
};
|
|
135
|
+
const invalidateCache = () => {
|
|
136
|
+
messageCache.clear();
|
|
137
|
+
};
|
|
138
|
+
return { count, countMessages, invalidateCache };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Creates a token counter with a custom counting function.
|
|
142
|
+
*
|
|
143
|
+
* Use this to integrate model-specific tokenizers for more accurate counts.
|
|
144
|
+
* Includes a message-level cache to avoid re-counting identical messages.
|
|
145
|
+
*
|
|
146
|
+
* @param options - Configuration for the token counter
|
|
147
|
+
* @returns A token counter using the custom function
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* import { encoding_for_model } from "tiktoken";
|
|
152
|
+
*
|
|
153
|
+
* const encoder = encoding_for_model("gpt-4");
|
|
154
|
+
* const counter = createCustomTokenCounter({
|
|
155
|
+
* countFn: (text) => encoder.encode(text).length,
|
|
156
|
+
* });
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* @category Context
|
|
160
|
+
*/
|
|
161
|
+
export function createCustomTokenCounter(options) {
|
|
162
|
+
const { countFn, messageOverhead = 4 } = options;
|
|
163
|
+
const messageCache = new Map();
|
|
164
|
+
const countSingleMessage = (message) => {
|
|
165
|
+
const hash = hashMessage(message);
|
|
166
|
+
// Check cache
|
|
167
|
+
const cached = messageCache.get(hash);
|
|
168
|
+
if (cached !== undefined) {
|
|
169
|
+
return cached;
|
|
170
|
+
}
|
|
171
|
+
let total = messageOverhead;
|
|
172
|
+
if (typeof message.content === "string") {
|
|
173
|
+
total += countFn(message.content);
|
|
174
|
+
}
|
|
175
|
+
else if (Array.isArray(message.content)) {
|
|
176
|
+
for (const part of message.content) {
|
|
177
|
+
if ("text" in part && typeof part.text === "string") {
|
|
178
|
+
total += countFn(part.text);
|
|
179
|
+
}
|
|
180
|
+
else if ("toolName" in part) {
|
|
181
|
+
total += countFn(part.toolName);
|
|
182
|
+
if ("args" in part) {
|
|
183
|
+
total += countFn(JSON.stringify(part.args));
|
|
184
|
+
}
|
|
185
|
+
if ("input" in part) {
|
|
186
|
+
total += countFn(JSON.stringify(part.input));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if ("result" in part || "output" in part) {
|
|
190
|
+
const output = "result" in part ? part.result : part.output;
|
|
191
|
+
total += countFn(typeof output === "string" ? output : JSON.stringify(output));
|
|
192
|
+
}
|
|
193
|
+
else if ("image" in part) {
|
|
194
|
+
// Image part - count ~1000 tokens for image (approximate vision model cost)
|
|
195
|
+
// Images are expensive in terms of tokens, varies by size and model
|
|
196
|
+
total += 1000;
|
|
197
|
+
}
|
|
198
|
+
else if ("data" in part && part.type === "file") {
|
|
199
|
+
// File part - count as ~500 tokens per file (placeholder)
|
|
200
|
+
// Actual token count depends on file size and content
|
|
201
|
+
total += 500;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Store in cache
|
|
206
|
+
messageCache.set(hash, total);
|
|
207
|
+
return total;
|
|
208
|
+
};
|
|
209
|
+
const countMessages = (messages) => {
|
|
210
|
+
let total = 0;
|
|
211
|
+
for (const message of messages) {
|
|
212
|
+
total += countSingleMessage(message);
|
|
213
|
+
}
|
|
214
|
+
return total;
|
|
215
|
+
};
|
|
216
|
+
const invalidateCache = () => {
|
|
217
|
+
messageCache.clear();
|
|
218
|
+
};
|
|
219
|
+
return { count: countFn, countMessages, invalidateCache };
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Creates a token budget from current usage.
|
|
223
|
+
*
|
|
224
|
+
* @param maxTokens - Maximum tokens allowed
|
|
225
|
+
* @param currentTokens - Current token count
|
|
226
|
+
* @param isActual - Whether this is based on actual model usage (default: false)
|
|
227
|
+
* @returns A token budget object
|
|
228
|
+
*
|
|
229
|
+
* @category Context
|
|
230
|
+
*/
|
|
231
|
+
export function createTokenBudget(maxTokens, currentTokens, isActual = false) {
|
|
232
|
+
return {
|
|
233
|
+
maxTokens,
|
|
234
|
+
currentTokens,
|
|
235
|
+
usage: currentTokens / maxTokens,
|
|
236
|
+
remaining: Math.max(0, maxTokens - currentTokens),
|
|
237
|
+
isActual,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Default compaction policy.
|
|
242
|
+
*
|
|
243
|
+
* @category Context
|
|
244
|
+
*/
|
|
245
|
+
export const DEFAULT_COMPACTION_POLICY = {
|
|
246
|
+
enabled: true,
|
|
247
|
+
tokenThreshold: 0.8,
|
|
248
|
+
hardCapThreshold: 0.95,
|
|
249
|
+
enableGrowthRatePrediction: false,
|
|
250
|
+
enableErrorFallback: true,
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* Default summarization configuration.
|
|
254
|
+
*
|
|
255
|
+
* @category Context
|
|
256
|
+
*/
|
|
257
|
+
export const DEFAULT_SUMMARIZATION_CONFIG = {
|
|
258
|
+
keepMessageCount: 10,
|
|
259
|
+
keepToolResultCount: 5,
|
|
260
|
+
strategy: "rollup",
|
|
261
|
+
enableTieredSummaries: false,
|
|
262
|
+
maxSummaryTiers: 3,
|
|
263
|
+
messagesPerTier: 5,
|
|
264
|
+
enableStructuredSummary: false,
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* Creates a compaction scheduler for managing background compaction tasks.
|
|
268
|
+
*
|
|
269
|
+
* @param contextManager - The context manager to use for compaction
|
|
270
|
+
* @param options - Scheduler configuration options
|
|
271
|
+
* @returns A compaction scheduler instance
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```typescript
|
|
275
|
+
* const scheduler = createCompactionScheduler(contextManager, {
|
|
276
|
+
* enableBackgroundCompaction: true,
|
|
277
|
+
* debounceDelayMs: 5000,
|
|
278
|
+
* onTaskComplete: (task) => {
|
|
279
|
+
* console.log(`Compaction saved ${task.result?.tokensBefore - task.result?.tokensAfter} tokens`);
|
|
280
|
+
* },
|
|
281
|
+
* });
|
|
282
|
+
*
|
|
283
|
+
* // Schedule a compaction
|
|
284
|
+
* const taskId = scheduler.schedule(messages, agent, "token_threshold");
|
|
285
|
+
*
|
|
286
|
+
* // Later, get the result
|
|
287
|
+
* const result = scheduler.getLatestResult();
|
|
288
|
+
* if (result) {
|
|
289
|
+
* messages = result.newMessages;
|
|
290
|
+
* }
|
|
291
|
+
* ```
|
|
292
|
+
*
|
|
293
|
+
* @category Context
|
|
294
|
+
*/
|
|
295
|
+
export function createCompactionScheduler(contextManager, options = {}) {
|
|
296
|
+
const { enableBackgroundCompaction = false, debounceDelayMs = 5000, maxPendingTasks = 3, onTaskComplete, onTaskError, } = options;
|
|
297
|
+
const tasks = new Map();
|
|
298
|
+
const taskAgents = new Map(); // Store agents for each task
|
|
299
|
+
let taskIdCounter = 0;
|
|
300
|
+
let debounceTimer = null;
|
|
301
|
+
let isShutdown = false;
|
|
302
|
+
const schedule = (messages, agent, trigger) => {
|
|
303
|
+
if (isShutdown) {
|
|
304
|
+
throw new Error("Scheduler has been shut down");
|
|
305
|
+
}
|
|
306
|
+
if (!enableBackgroundCompaction) {
|
|
307
|
+
// When background compaction is disabled, return a placeholder ID
|
|
308
|
+
// The caller should handle compaction synchronously
|
|
309
|
+
return `sync-${Date.now()}`;
|
|
310
|
+
}
|
|
311
|
+
const taskId = `task-${++taskIdCounter}`;
|
|
312
|
+
const task = {
|
|
313
|
+
id: taskId,
|
|
314
|
+
status: "pending",
|
|
315
|
+
messages: [...messages], // Clone to avoid mutation
|
|
316
|
+
createdAt: Date.now(),
|
|
317
|
+
trigger,
|
|
318
|
+
};
|
|
319
|
+
// Enforce max pending tasks - drop oldest pending task
|
|
320
|
+
const pendingTasks = Array.from(tasks.values())
|
|
321
|
+
.filter((t) => t.status === "pending")
|
|
322
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
323
|
+
if (pendingTasks.length >= maxPendingTasks) {
|
|
324
|
+
const oldestTask = pendingTasks[0];
|
|
325
|
+
if (oldestTask) {
|
|
326
|
+
tasks.delete(oldestTask.id);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
tasks.set(taskId, task);
|
|
330
|
+
taskAgents.set(taskId, agent); // Store agent for this task
|
|
331
|
+
// Debounce execution
|
|
332
|
+
if (debounceTimer) {
|
|
333
|
+
clearTimeout(debounceTimer);
|
|
334
|
+
}
|
|
335
|
+
debounceTimer = setTimeout(() => {
|
|
336
|
+
executeNextTask();
|
|
337
|
+
debounceTimer = null;
|
|
338
|
+
}, debounceDelayMs);
|
|
339
|
+
return taskId;
|
|
340
|
+
};
|
|
341
|
+
const executeNextTask = async () => {
|
|
342
|
+
if (isShutdown) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Find the oldest pending task
|
|
346
|
+
const pendingTask = Array.from(tasks.values())
|
|
347
|
+
.filter((t) => t.status === "pending")
|
|
348
|
+
.sort((a, b) => a.createdAt - b.createdAt)[0];
|
|
349
|
+
if (!pendingTask) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Get the agent for this task
|
|
353
|
+
const agent = taskAgents.get(pendingTask.id);
|
|
354
|
+
if (!agent) {
|
|
355
|
+
// This shouldn't happen, but handle gracefully
|
|
356
|
+
pendingTask.status = "failed";
|
|
357
|
+
pendingTask.completedAt = Date.now();
|
|
358
|
+
pendingTask.error = new Error("Agent not found for task");
|
|
359
|
+
onTaskError?.(pendingTask);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// Mark as running
|
|
363
|
+
pendingTask.status = "running";
|
|
364
|
+
pendingTask.startedAt = Date.now();
|
|
365
|
+
try {
|
|
366
|
+
// Execute compaction
|
|
367
|
+
const result = await contextManager.compact(pendingTask.messages, agent, pendingTask.trigger);
|
|
368
|
+
// Mark as completed
|
|
369
|
+
pendingTask.status = "completed";
|
|
370
|
+
pendingTask.completedAt = Date.now();
|
|
371
|
+
pendingTask.result = result;
|
|
372
|
+
onTaskComplete?.(pendingTask);
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
// Mark as failed
|
|
376
|
+
pendingTask.status = "failed";
|
|
377
|
+
pendingTask.completedAt = Date.now();
|
|
378
|
+
pendingTask.error = error instanceof Error ? error : new Error(String(error));
|
|
379
|
+
onTaskError?.(pendingTask);
|
|
380
|
+
}
|
|
381
|
+
// Execute next task if any
|
|
382
|
+
const hasMorePending = Array.from(tasks.values()).some((t) => t.status === "pending");
|
|
383
|
+
if (hasMorePending) {
|
|
384
|
+
// Schedule next task with debounce
|
|
385
|
+
if (debounceTimer) {
|
|
386
|
+
clearTimeout(debounceTimer);
|
|
387
|
+
}
|
|
388
|
+
debounceTimer = setTimeout(() => {
|
|
389
|
+
executeNextTask();
|
|
390
|
+
debounceTimer = null;
|
|
391
|
+
}, debounceDelayMs);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
const getTask = (id) => {
|
|
395
|
+
return tasks.get(id);
|
|
396
|
+
};
|
|
397
|
+
const getPendingTasks = () => {
|
|
398
|
+
return Array.from(tasks.values())
|
|
399
|
+
.filter((t) => t.status === "pending")
|
|
400
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
401
|
+
};
|
|
402
|
+
const getLatestResult = () => {
|
|
403
|
+
// Find the most recent completed task
|
|
404
|
+
const completedTasks = Array.from(tasks.values())
|
|
405
|
+
.filter((t) => t.status === "completed" && t.result)
|
|
406
|
+
.sort((a, b) => (b.completedAt ?? 0) - (a.completedAt ?? 0));
|
|
407
|
+
return completedTasks[0]?.result;
|
|
408
|
+
};
|
|
409
|
+
const cancel = (id) => {
|
|
410
|
+
const task = tasks.get(id);
|
|
411
|
+
if (!task || task.status !== "pending") {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
tasks.delete(id);
|
|
415
|
+
taskAgents.delete(id); // Clean up agent reference
|
|
416
|
+
return true;
|
|
417
|
+
};
|
|
418
|
+
const cleanup = () => {
|
|
419
|
+
// Remove completed and failed tasks
|
|
420
|
+
for (const [id, task] of tasks.entries()) {
|
|
421
|
+
if (task.status === "completed" || task.status === "failed") {
|
|
422
|
+
tasks.delete(id);
|
|
423
|
+
taskAgents.delete(id); // Clean up agent reference
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
const shutdown = () => {
|
|
428
|
+
isShutdown = true;
|
|
429
|
+
// Clear debounce timer
|
|
430
|
+
if (debounceTimer) {
|
|
431
|
+
clearTimeout(debounceTimer);
|
|
432
|
+
debounceTimer = null;
|
|
433
|
+
}
|
|
434
|
+
// Cancel all pending tasks
|
|
435
|
+
for (const [_id, task] of tasks.entries()) {
|
|
436
|
+
if (task.status === "pending") {
|
|
437
|
+
task.status = "failed";
|
|
438
|
+
task.error = new Error("Scheduler shut down");
|
|
439
|
+
task.completedAt = Date.now();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
return {
|
|
444
|
+
schedule,
|
|
445
|
+
getTask,
|
|
446
|
+
getPendingTasks,
|
|
447
|
+
getLatestResult,
|
|
448
|
+
cancel,
|
|
449
|
+
cleanup,
|
|
450
|
+
shutdown,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Default prompt for generating conversation summaries.
|
|
455
|
+
*/
|
|
456
|
+
const DEFAULT_SUMMARY_PROMPT = `You are summarizing a conversation for context management. Create a concise summary that:
|
|
457
|
+
|
|
458
|
+
1. Preserves key decisions, facts, and user preferences
|
|
459
|
+
2. Maintains important technical details and code references
|
|
460
|
+
3. Notes any pending tasks or unresolved questions
|
|
461
|
+
4. Captures references to images, files, and media (e.g., "discussed screenshot of X", "analyzed document Y")
|
|
462
|
+
5. Uses bullet points for clarity
|
|
463
|
+
6. Is approximately 200-500 words
|
|
464
|
+
|
|
465
|
+
Do not include:
|
|
466
|
+
- Pleasantries or greetings
|
|
467
|
+
- Redundant information
|
|
468
|
+
- Tool call details (just outcomes)
|
|
469
|
+
- Verbose explanations
|
|
470
|
+
- Full image/file contents (just references and context)
|
|
471
|
+
|
|
472
|
+
Format:
|
|
473
|
+
## Conversation Summary
|
|
474
|
+
[Your summary here]`;
|
|
475
|
+
/**
|
|
476
|
+
* Prompt for generating structured summaries with sections.
|
|
477
|
+
*/
|
|
478
|
+
const STRUCTURED_SUMMARY_PROMPT = `You are summarizing a conversation for context management. Generate a structured summary in JSON format with the following sections:
|
|
479
|
+
|
|
480
|
+
{
|
|
481
|
+
"decisions": ["Key decision 1", "Key decision 2", ...],
|
|
482
|
+
"preferences": ["User preference 1", "User preference 2", ...],
|
|
483
|
+
"currentState": ["Current state fact 1", "Current state fact 2", ...],
|
|
484
|
+
"openQuestions": ["Unresolved question 1", "Unresolved question 2", ...],
|
|
485
|
+
"references": ["file.ts:123", "userId: abc123", "URL: https://...", "Image: screenshot.png showing X", "File: document.pdf containing Y", ...]
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
Guidelines:
|
|
489
|
+
- **decisions**: Important choices made (architecture, approach, tools)
|
|
490
|
+
- **preferences**: User requirements, constraints, style preferences
|
|
491
|
+
- **currentState**: What's been implemented, current progress, known issues
|
|
492
|
+
- **openQuestions**: Unresolved issues, pending decisions, items to revisit
|
|
493
|
+
- **references**: File paths, identifiers, URLs, config values, images, documents that may be needed later
|
|
494
|
+
|
|
495
|
+
Keep each item concise (1-2 sentences). Focus on information that will be useful for continuing the conversation.
|
|
496
|
+
For images and files, include what they contained or depicted, not just that they were present.
|
|
497
|
+
Respond ONLY with valid JSON, no additional text.`;
|
|
498
|
+
/**
|
|
499
|
+
* Prompt for generating tiered summaries (summary of summaries).
|
|
500
|
+
*/
|
|
501
|
+
const TIERED_SUMMARY_PROMPT = `You are creating a higher-level summary from multiple conversation summaries. Consolidate the key information from these summaries:
|
|
502
|
+
|
|
503
|
+
Guidelines:
|
|
504
|
+
1. Merge related decisions and facts
|
|
505
|
+
2. Preserve the most important details
|
|
506
|
+
3. Remove redundancy across summaries
|
|
507
|
+
4. Maintain chronological context where relevant
|
|
508
|
+
5. Keep the summary concise but comprehensive
|
|
509
|
+
|
|
510
|
+
Format:
|
|
511
|
+
## Tiered Summary (Level {tier})
|
|
512
|
+
[Your consolidated summary here]`;
|
|
513
|
+
/**
|
|
514
|
+
* Creates a context manager for tracking and managing conversation context.
|
|
515
|
+
*
|
|
516
|
+
* @param options - Configuration options
|
|
517
|
+
* @returns A context manager instance
|
|
518
|
+
*
|
|
519
|
+
* @example
|
|
520
|
+
* ```typescript
|
|
521
|
+
* const contextManager = createContextManager({
|
|
522
|
+
* maxTokens: 100000,
|
|
523
|
+
* summarization: {
|
|
524
|
+
* tokenThreshold: 0.75,
|
|
525
|
+
* keepMessageCount: 15,
|
|
526
|
+
* },
|
|
527
|
+
* onBudgetUpdate: (budget) => {
|
|
528
|
+
* console.log(`Context usage: ${(budget.usage * 100).toFixed(1)}%`);
|
|
529
|
+
* },
|
|
530
|
+
* });
|
|
531
|
+
*
|
|
532
|
+
* // Check budget
|
|
533
|
+
* const budget = contextManager.getBudget(messages);
|
|
534
|
+
*
|
|
535
|
+
* // Process messages (auto-compacts if needed)
|
|
536
|
+
* const processedMessages = await contextManager.process(messages, agent);
|
|
537
|
+
* ```
|
|
538
|
+
*
|
|
539
|
+
* @category Context
|
|
540
|
+
*/
|
|
541
|
+
export function createContextManager(options) {
|
|
542
|
+
const tokenCounter = options.tokenCounter ?? createApproximateTokenCounter();
|
|
543
|
+
const policy = {
|
|
544
|
+
...DEFAULT_COMPACTION_POLICY,
|
|
545
|
+
...options.policy,
|
|
546
|
+
};
|
|
547
|
+
const summarizationConfig = {
|
|
548
|
+
...DEFAULT_SUMMARIZATION_CONFIG,
|
|
549
|
+
...options.summarization,
|
|
550
|
+
};
|
|
551
|
+
const { maxTokens, onBudgetUpdate, onCompact } = options;
|
|
552
|
+
// Track actual usage from model responses
|
|
553
|
+
let lastActualUsage = null;
|
|
554
|
+
// Track pinned messages
|
|
555
|
+
const pinnedMessages = [];
|
|
556
|
+
// Track summary tiers for tiered strategy
|
|
557
|
+
let currentSummaryTier = 0;
|
|
558
|
+
// Create scheduler if background compaction is enabled
|
|
559
|
+
let scheduler;
|
|
560
|
+
if (options.scheduler?.enableBackgroundCompaction) {
|
|
561
|
+
// We'll create the scheduler after defining the manager, to avoid circular dependency
|
|
562
|
+
// For now, set it to undefined and assign it later
|
|
563
|
+
scheduler = undefined;
|
|
564
|
+
}
|
|
565
|
+
const getBudget = (messages) => {
|
|
566
|
+
let currentTokens;
|
|
567
|
+
let isActual = false;
|
|
568
|
+
// If we have actual usage data from the last generation, use it
|
|
569
|
+
if (lastActualUsage && lastActualUsage.totalTokens !== undefined) {
|
|
570
|
+
currentTokens = lastActualUsage.totalTokens;
|
|
571
|
+
isActual = true;
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
// Fall back to estimation
|
|
575
|
+
currentTokens = tokenCounter.countMessages(messages);
|
|
576
|
+
}
|
|
577
|
+
const budget = createTokenBudget(maxTokens, currentTokens, isActual);
|
|
578
|
+
onBudgetUpdate?.(budget);
|
|
579
|
+
return budget;
|
|
580
|
+
};
|
|
581
|
+
const shouldCompact = (messages) => {
|
|
582
|
+
if (!policy.enabled) {
|
|
583
|
+
return { trigger: false };
|
|
584
|
+
}
|
|
585
|
+
const budget = getBudget(messages);
|
|
586
|
+
// Custom policy override
|
|
587
|
+
if (policy.shouldCompact) {
|
|
588
|
+
return policy.shouldCompact(budget, messages);
|
|
589
|
+
}
|
|
590
|
+
// Hard cap safety - force compaction at 95% to prevent errors
|
|
591
|
+
if (budget.usage >= policy.hardCapThreshold) {
|
|
592
|
+
return { trigger: true, reason: "hard_cap" };
|
|
593
|
+
}
|
|
594
|
+
// Standard token threshold
|
|
595
|
+
if (budget.usage >= policy.tokenThreshold) {
|
|
596
|
+
return { trigger: true, reason: "token_threshold" };
|
|
597
|
+
}
|
|
598
|
+
// Growth rate prediction (simplified version)
|
|
599
|
+
if (policy.enableGrowthRatePrediction && messages.length >= 2) {
|
|
600
|
+
// Estimate growth: if last message was large relative to average
|
|
601
|
+
const lastMessage = messages[messages.length - 1];
|
|
602
|
+
if (lastMessage) {
|
|
603
|
+
const lastMessageTokens = tokenCounter.countMessages([lastMessage]);
|
|
604
|
+
const avgMessageTokens = budget.currentTokens / messages.length;
|
|
605
|
+
// If last message is significantly larger than average, predict growth
|
|
606
|
+
if (lastMessageTokens > avgMessageTokens * 2) {
|
|
607
|
+
const predictedNext = budget.currentTokens + lastMessageTokens;
|
|
608
|
+
const predictedUsage = predictedNext / maxTokens;
|
|
609
|
+
if (predictedUsage >= policy.tokenThreshold) {
|
|
610
|
+
return { trigger: true, reason: "growth_rate" };
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return { trigger: false };
|
|
616
|
+
};
|
|
617
|
+
const compact = async (messages, agent, trigger = "token_threshold") => {
|
|
618
|
+
const tokensBefore = tokenCounter.countMessages(messages);
|
|
619
|
+
const messagesBefore = messages.length;
|
|
620
|
+
const { keepMessageCount, strategy = "rollup", enableStructuredSummary, enableTieredSummaries, } = summarizationConfig;
|
|
621
|
+
// Always keep system messages
|
|
622
|
+
const systemMessages = messages.filter((m) => m.role === "system");
|
|
623
|
+
const _nonSystemMessages = messages.filter((m) => m.role !== "system");
|
|
624
|
+
// Filter out pinned messages (keep them separate)
|
|
625
|
+
// pinnedMessages indices refer to the original messages array
|
|
626
|
+
const pinnedIndices = new Set(pinnedMessages.map((p) => p.messageIndex));
|
|
627
|
+
const pinnedMessagesArray = [];
|
|
628
|
+
const unpinnedMessages = [];
|
|
629
|
+
messages.forEach((msg, idx) => {
|
|
630
|
+
// Skip system messages (already handled)
|
|
631
|
+
if (msg.role === "system")
|
|
632
|
+
return;
|
|
633
|
+
if (pinnedIndices.has(idx)) {
|
|
634
|
+
pinnedMessagesArray.push(msg);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
unpinnedMessages.push(msg);
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
// Keep recent messages from unpinned messages
|
|
641
|
+
const recentMessages = unpinnedMessages.slice(-keepMessageCount);
|
|
642
|
+
const oldMessages = unpinnedMessages.slice(0, Math.max(0, unpinnedMessages.length - keepMessageCount));
|
|
643
|
+
// If nothing to compact, return unchanged
|
|
644
|
+
if (oldMessages.length === 0) {
|
|
645
|
+
return {
|
|
646
|
+
messagesBefore,
|
|
647
|
+
messagesAfter: messages.length,
|
|
648
|
+
tokensBefore,
|
|
649
|
+
tokensAfter: tokensBefore,
|
|
650
|
+
summary: "",
|
|
651
|
+
compactedMessages: [],
|
|
652
|
+
newMessages: messages,
|
|
653
|
+
trigger,
|
|
654
|
+
strategy,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
// Execute strategy-specific compaction
|
|
658
|
+
let summary;
|
|
659
|
+
let structuredSummary;
|
|
660
|
+
let summaryTier;
|
|
661
|
+
if (strategy === "tiered" && enableTieredSummaries) {
|
|
662
|
+
// Tiered strategy: check if we have existing summaries to tier
|
|
663
|
+
const existingSummaries = messages.filter((m) => m.role === "assistant" &&
|
|
664
|
+
typeof m.content === "string" &&
|
|
665
|
+
m.content.includes("[Previous conversation summary]"));
|
|
666
|
+
if (existingSummaries.length >= (summarizationConfig.messagesPerTier ?? 5)) {
|
|
667
|
+
// Create a higher-tier summary
|
|
668
|
+
currentSummaryTier++;
|
|
669
|
+
const summariesContent = existingSummaries.map((m) => m.content).join("\n\n");
|
|
670
|
+
const tierPrompt = TIERED_SUMMARY_PROMPT.replace("{tier}", String(currentSummaryTier));
|
|
671
|
+
const summaryResult = await agent.generate({
|
|
672
|
+
messages: [
|
|
673
|
+
{ role: "system", content: tierPrompt },
|
|
674
|
+
{
|
|
675
|
+
role: "user",
|
|
676
|
+
content: `Consolidate these summaries:\n\n${summariesContent}`,
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
maxTokens: 1000,
|
|
680
|
+
_skipCompaction: true, // Prevent recursive compaction during summary generation
|
|
681
|
+
});
|
|
682
|
+
summary = summaryResult.status === "complete" ? summaryResult.text : "";
|
|
683
|
+
summaryTier = currentSummaryTier;
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
// Create a first-tier summary
|
|
687
|
+
const contentToSummarize = formatMessagesForSummary(oldMessages);
|
|
688
|
+
const summaryPrompt = enableStructuredSummary
|
|
689
|
+
? STRUCTURED_SUMMARY_PROMPT
|
|
690
|
+
: (summarizationConfig.summaryPrompt ?? DEFAULT_SUMMARY_PROMPT);
|
|
691
|
+
const summaryResult = await agent.generate({
|
|
692
|
+
messages: [
|
|
693
|
+
{ role: "system", content: summaryPrompt },
|
|
694
|
+
{
|
|
695
|
+
role: "user",
|
|
696
|
+
content: `Please summarize this conversation history:\n\n${contentToSummarize}`,
|
|
697
|
+
},
|
|
698
|
+
],
|
|
699
|
+
maxTokens: 1000,
|
|
700
|
+
_skipCompaction: true, // Prevent recursive compaction during summary generation
|
|
701
|
+
});
|
|
702
|
+
summary = summaryResult.status === "complete" ? summaryResult.text : "";
|
|
703
|
+
summaryTier = 0;
|
|
704
|
+
// Try to parse structured summary if enabled
|
|
705
|
+
if (enableStructuredSummary) {
|
|
706
|
+
try {
|
|
707
|
+
structuredSummary = JSON.parse(summary);
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
// If parsing fails, keep as plain text
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
else if (strategy === "structured" || enableStructuredSummary) {
|
|
716
|
+
// Structured strategy: generate JSON with sections
|
|
717
|
+
const contentToSummarize = formatMessagesForSummary(oldMessages);
|
|
718
|
+
const summaryResult = await agent.generate({
|
|
719
|
+
messages: [
|
|
720
|
+
{ role: "system", content: STRUCTURED_SUMMARY_PROMPT },
|
|
721
|
+
{
|
|
722
|
+
role: "user",
|
|
723
|
+
content: `Please summarize this conversation history:\n\n${contentToSummarize}`,
|
|
724
|
+
},
|
|
725
|
+
],
|
|
726
|
+
maxTokens: 1000,
|
|
727
|
+
_skipCompaction: true, // Prevent recursive compaction during summary generation
|
|
728
|
+
});
|
|
729
|
+
summary = summaryResult.status === "complete" ? summaryResult.text : "";
|
|
730
|
+
// Try to parse structured summary
|
|
731
|
+
try {
|
|
732
|
+
structuredSummary = JSON.parse(summary);
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
// If parsing fails, keep as plain text
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
// Rollup strategy (default): single summary of old messages
|
|
740
|
+
const contentToSummarize = formatMessagesForSummary(oldMessages);
|
|
741
|
+
const summaryPrompt = summarizationConfig.summaryPrompt ?? DEFAULT_SUMMARY_PROMPT;
|
|
742
|
+
const summaryResult = await agent.generate({
|
|
743
|
+
messages: [
|
|
744
|
+
{ role: "system", content: summaryPrompt },
|
|
745
|
+
{
|
|
746
|
+
role: "user",
|
|
747
|
+
content: `Please summarize this conversation history:\n\n${contentToSummarize}`,
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
maxTokens: 1000,
|
|
751
|
+
_skipCompaction: true, // Prevent recursive compaction during summary generation
|
|
752
|
+
});
|
|
753
|
+
summary = summaryResult.status === "complete" ? summaryResult.text : "";
|
|
754
|
+
}
|
|
755
|
+
// Build new message history
|
|
756
|
+
const summaryPrefix = summaryTier !== undefined
|
|
757
|
+
? `[Previous conversation summary - Tier ${summaryTier}]`
|
|
758
|
+
: "[Previous conversation summary]";
|
|
759
|
+
const summaryMessage = {
|
|
760
|
+
role: "assistant",
|
|
761
|
+
content: structuredSummary
|
|
762
|
+
? `${summaryPrefix}\n\n${formatStructuredSummary(structuredSummary)}`
|
|
763
|
+
: `${summaryPrefix}\n\n${summary}`,
|
|
764
|
+
};
|
|
765
|
+
const newMessages = [
|
|
766
|
+
...systemMessages,
|
|
767
|
+
summaryMessage,
|
|
768
|
+
...pinnedMessagesArray, // Include pinned messages
|
|
769
|
+
...recentMessages,
|
|
770
|
+
];
|
|
771
|
+
const tokensAfter = tokenCounter.countMessages(newMessages);
|
|
772
|
+
const result = {
|
|
773
|
+
messagesBefore,
|
|
774
|
+
messagesAfter: newMessages.length,
|
|
775
|
+
tokensBefore,
|
|
776
|
+
tokensAfter,
|
|
777
|
+
summary,
|
|
778
|
+
compactedMessages: oldMessages,
|
|
779
|
+
newMessages,
|
|
780
|
+
trigger,
|
|
781
|
+
strategy,
|
|
782
|
+
structuredSummary,
|
|
783
|
+
summaryTier,
|
|
784
|
+
};
|
|
785
|
+
onCompact?.(result);
|
|
786
|
+
return result;
|
|
787
|
+
};
|
|
788
|
+
const process = async (messages, agent) => {
|
|
789
|
+
const { trigger, reason } = shouldCompact(messages);
|
|
790
|
+
if (trigger && reason) {
|
|
791
|
+
// If background compaction is enabled and scheduler exists
|
|
792
|
+
if (scheduler && options.scheduler?.enableBackgroundCompaction) {
|
|
793
|
+
// Check if there's a pending result from a previous background compaction
|
|
794
|
+
const latestResult = scheduler.getLatestResult();
|
|
795
|
+
if (latestResult) {
|
|
796
|
+
// Apply the background compaction result
|
|
797
|
+
scheduler.cleanup(); // Clean up old completed tasks
|
|
798
|
+
return latestResult.newMessages;
|
|
799
|
+
}
|
|
800
|
+
// Schedule a new background compaction for next time
|
|
801
|
+
scheduler.schedule(messages, agent, reason);
|
|
802
|
+
// Return original messages (compaction will happen in background)
|
|
803
|
+
return messages;
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
// Synchronous compaction (blocking)
|
|
807
|
+
const result = await compact(messages, agent, reason);
|
|
808
|
+
return result.newMessages;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return messages;
|
|
812
|
+
};
|
|
813
|
+
const updateUsage = (usage) => {
|
|
814
|
+
// Store actual usage for next budget calculation
|
|
815
|
+
// Only store if totalTokens is defined
|
|
816
|
+
if (usage.totalTokens !== undefined) {
|
|
817
|
+
lastActualUsage = {
|
|
818
|
+
inputTokens: usage.inputTokens,
|
|
819
|
+
totalTokens: usage.totalTokens,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
const pinMessage = (messageIndex, reason) => {
|
|
824
|
+
// Check if already pinned
|
|
825
|
+
if (pinnedMessages.some((p) => p.messageIndex === messageIndex)) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
pinnedMessages.push({
|
|
829
|
+
messageIndex,
|
|
830
|
+
reason,
|
|
831
|
+
pinnedAt: Date.now(),
|
|
832
|
+
});
|
|
833
|
+
};
|
|
834
|
+
const unpinMessage = (messageIndex) => {
|
|
835
|
+
const index = pinnedMessages.findIndex((p) => p.messageIndex === messageIndex);
|
|
836
|
+
if (index !== -1) {
|
|
837
|
+
pinnedMessages.splice(index, 1);
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
const isPinned = (messageIndex) => {
|
|
841
|
+
return pinnedMessages.some((p) => p.messageIndex === messageIndex);
|
|
842
|
+
};
|
|
843
|
+
// Create the manager object (needed for scheduler creation)
|
|
844
|
+
const manager = {
|
|
845
|
+
tokenCounter,
|
|
846
|
+
policy,
|
|
847
|
+
summarizationConfig,
|
|
848
|
+
maxTokens,
|
|
849
|
+
scheduler: undefined, // Will be set below if needed
|
|
850
|
+
pinnedMessages,
|
|
851
|
+
getBudget,
|
|
852
|
+
shouldCompact,
|
|
853
|
+
compact,
|
|
854
|
+
process,
|
|
855
|
+
updateUsage,
|
|
856
|
+
pinMessage,
|
|
857
|
+
unpinMessage,
|
|
858
|
+
isPinned,
|
|
859
|
+
};
|
|
860
|
+
// Create scheduler after manager is defined (to pass to createCompactionScheduler)
|
|
861
|
+
if (options.scheduler?.enableBackgroundCompaction) {
|
|
862
|
+
scheduler = createCompactionScheduler(manager, options.scheduler);
|
|
863
|
+
manager.scheduler = scheduler;
|
|
864
|
+
}
|
|
865
|
+
return manager;
|
|
866
|
+
}
|
|
867
|
+
// =============================================================================
|
|
868
|
+
// Helper Functions
|
|
869
|
+
// =============================================================================
|
|
870
|
+
/**
|
|
871
|
+
* Formats messages into a string suitable for summarization.
|
|
872
|
+
*
|
|
873
|
+
* @param messages - Messages to format
|
|
874
|
+
* @returns Formatted string representation
|
|
875
|
+
*/
|
|
876
|
+
/**
|
|
877
|
+
* Helper function to extract metadata from image parts for summarization.
|
|
878
|
+
*/
|
|
879
|
+
function extractImageMetadata(part) {
|
|
880
|
+
const image = part.image;
|
|
881
|
+
// Handle URL case
|
|
882
|
+
if (typeof image === "object" && image !== null && "toString" in image) {
|
|
883
|
+
const url = image.toString();
|
|
884
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
885
|
+
return `[Image: ${url}]`;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
// Handle data content case (base64, Buffer, etc.)
|
|
889
|
+
if (typeof image === "string") {
|
|
890
|
+
// Check if it's a data URL
|
|
891
|
+
if (image.startsWith("data:")) {
|
|
892
|
+
const mimeMatch = image.match(/^data:([^;,]+)/);
|
|
893
|
+
const mimeType = mimeMatch ? mimeMatch[1] : "unknown";
|
|
894
|
+
return `[Image: ${mimeType}, base64 data]`;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return "[Image: embedded data]";
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Helper function to extract metadata from file parts for summarization.
|
|
901
|
+
*/
|
|
902
|
+
function extractFileMetadata(part) {
|
|
903
|
+
const mimeType = part.mimeType ?? "unknown";
|
|
904
|
+
// Handle URL case
|
|
905
|
+
const data = part.data;
|
|
906
|
+
if (typeof data === "object" && data !== null && "toString" in data) {
|
|
907
|
+
const url = data.toString();
|
|
908
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
909
|
+
return `[File: ${mimeType}, URL: ${url}]`;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
// Handle data content case
|
|
913
|
+
return `[File: ${mimeType}, embedded data]`;
|
|
914
|
+
}
|
|
915
|
+
function formatMessagesForSummary(messages) {
|
|
916
|
+
const lines = [];
|
|
917
|
+
for (const message of messages) {
|
|
918
|
+
const role = message.role.toUpperCase();
|
|
919
|
+
if (typeof message.content === "string") {
|
|
920
|
+
lines.push(`${role}: ${message.content}`);
|
|
921
|
+
}
|
|
922
|
+
else if (Array.isArray(message.content)) {
|
|
923
|
+
const parts = [];
|
|
924
|
+
for (const part of message.content) {
|
|
925
|
+
if ("text" in part && typeof part.text === "string") {
|
|
926
|
+
parts.push(part.text);
|
|
927
|
+
}
|
|
928
|
+
else if ("toolName" in part) {
|
|
929
|
+
parts.push(`[Tool call: ${part.toolName}]`);
|
|
930
|
+
}
|
|
931
|
+
else if ("result" in part || "output" in part) {
|
|
932
|
+
const output = "result" in part ? part.result : part.output;
|
|
933
|
+
const outputStr = typeof output === "string"
|
|
934
|
+
? output.slice(0, 200)
|
|
935
|
+
: JSON.stringify(output).slice(0, 200);
|
|
936
|
+
parts.push(`[Tool result: ${outputStr}...]`);
|
|
937
|
+
}
|
|
938
|
+
else if ("image" in part && part.type === "image") {
|
|
939
|
+
parts.push(extractImageMetadata(part));
|
|
940
|
+
}
|
|
941
|
+
else if ("data" in part && part.type === "file") {
|
|
942
|
+
parts.push(extractFileMetadata(part));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (parts.length > 0) {
|
|
946
|
+
lines.push(`${role}: ${parts.join("\n")}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return lines.join("\n\n");
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Extracts tool results from messages.
|
|
954
|
+
*
|
|
955
|
+
* @param messages - Messages to search
|
|
956
|
+
* @returns Array of tool results with their context
|
|
957
|
+
*/
|
|
958
|
+
export function extractToolResults(messages) {
|
|
959
|
+
const results = [];
|
|
960
|
+
messages.forEach((message, index) => {
|
|
961
|
+
if (Array.isArray(message.content)) {
|
|
962
|
+
for (const part of message.content) {
|
|
963
|
+
if ("result" in part || "output" in part) {
|
|
964
|
+
results.push({
|
|
965
|
+
toolName: "toolName" in part ? String(part.toolName) : "unknown",
|
|
966
|
+
output: "result" in part ? part.result : part.output,
|
|
967
|
+
messageIndex: index,
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
return results;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Finds the last user message in the conversation.
|
|
977
|
+
*
|
|
978
|
+
* @param messages - Messages to search
|
|
979
|
+
* @returns The last user message content, or undefined
|
|
980
|
+
*/
|
|
981
|
+
export function findLastUserMessage(messages) {
|
|
982
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
983
|
+
const message = messages[i];
|
|
984
|
+
if (!message)
|
|
985
|
+
continue;
|
|
986
|
+
if (message.role === "user") {
|
|
987
|
+
if (typeof message.content === "string") {
|
|
988
|
+
return message.content;
|
|
989
|
+
}
|
|
990
|
+
else if (Array.isArray(message.content)) {
|
|
991
|
+
for (const part of message.content) {
|
|
992
|
+
if ("text" in part && typeof part.text === "string") {
|
|
993
|
+
return part.text;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return undefined;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Counts messages by role.
|
|
1003
|
+
*
|
|
1004
|
+
* @param messages - Messages to count
|
|
1005
|
+
* @returns Object with counts per role
|
|
1006
|
+
*/
|
|
1007
|
+
export function countMessagesByRole(messages) {
|
|
1008
|
+
const counts = {};
|
|
1009
|
+
for (const message of messages) {
|
|
1010
|
+
counts[message.role] = (counts[message.role] ?? 0) + 1;
|
|
1011
|
+
}
|
|
1012
|
+
return counts;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Formats a structured summary into a readable markdown string.
|
|
1016
|
+
*
|
|
1017
|
+
* @param summary - Structured summary to format
|
|
1018
|
+
* @returns Formatted markdown string
|
|
1019
|
+
*/
|
|
1020
|
+
function formatStructuredSummary(summary) {
|
|
1021
|
+
const sections = [];
|
|
1022
|
+
if (summary.decisions.length > 0) {
|
|
1023
|
+
sections.push(`## Decisions\n${summary.decisions.map((d) => `- ${d}`).join("\n")}`);
|
|
1024
|
+
}
|
|
1025
|
+
if (summary.preferences.length > 0) {
|
|
1026
|
+
sections.push(`## Preferences\n${summary.preferences.map((p) => `- ${p}`).join("\n")}`);
|
|
1027
|
+
}
|
|
1028
|
+
if (summary.currentState.length > 0) {
|
|
1029
|
+
sections.push(`## Current State\n${summary.currentState.map((s) => `- ${s}`).join("\n")}`);
|
|
1030
|
+
}
|
|
1031
|
+
if (summary.openQuestions.length > 0) {
|
|
1032
|
+
sections.push(`## Open Questions\n${summary.openQuestions.map((q) => `- ${q}`).join("\n")}`);
|
|
1033
|
+
}
|
|
1034
|
+
if (summary.references.length > 0) {
|
|
1035
|
+
sections.push(`## References\n${summary.references.map((r) => `- ${r}`).join("\n")}`);
|
|
1036
|
+
}
|
|
1037
|
+
return sections.join("\n\n");
|
|
1038
|
+
}
|
|
1039
|
+
//# sourceMappingURL=context-manager.js.map
|