@aigne/core 1.72.0-beta.9 → 1.72.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 +342 -0
- package/lib/cjs/agents/agent.d.ts +5 -0
- package/lib/cjs/agents/agent.js +5 -0
- package/lib/cjs/agents/ai-agent.d.ts +41 -5
- package/lib/cjs/agents/ai-agent.js +89 -29
- package/lib/cjs/agents/chat-model.d.ts +3 -0
- package/lib/cjs/agents/chat-model.js +18 -1
- package/lib/cjs/agents/image-model.js +1 -1
- package/lib/cjs/agents/model.d.ts +3 -3
- package/lib/cjs/agents/model.js +2 -2
- package/lib/cjs/agents/video-model.js +1 -1
- package/lib/cjs/aigne/context.js +1 -3
- package/lib/cjs/prompt/agent-session.d.ts +115 -5
- package/lib/cjs/prompt/agent-session.js +746 -83
- package/lib/cjs/prompt/compact/compactor.js +4 -0
- package/lib/cjs/prompt/compact/session-memory-extractor.d.ts +7 -0
- package/lib/cjs/prompt/compact/session-memory-extractor.js +143 -0
- package/lib/cjs/prompt/compact/types.d.ts +257 -0
- package/lib/cjs/prompt/compact/types.js +35 -1
- package/lib/cjs/prompt/compact/user-memory-extractor.d.ts +7 -0
- package/lib/cjs/prompt/compact/user-memory-extractor.js +124 -0
- package/lib/cjs/prompt/prompt-builder.js +3 -3
- package/lib/cjs/prompt/skills/afs/agent-skill/agent-skill.d.ts +2 -0
- package/lib/cjs/prompt/skills/afs/agent-skill/agent-skill.js +6 -0
- package/lib/cjs/prompt/skills/afs/agent-skill/skill-loader.d.ts +1 -2
- package/lib/cjs/prompt/skills/afs/agent-skill/skill-loader.js +14 -26
- package/lib/cjs/prompt/skills/afs/list.d.ts +2 -0
- package/lib/cjs/prompt/skills/afs/list.js +9 -1
- package/lib/cjs/prompt/skills/afs/read.d.ts +3 -1
- package/lib/cjs/prompt/skills/afs/read.js +5 -0
- package/lib/cjs/utils/mcp-utils.js +1 -1
- package/lib/cjs/utils/token-estimator.js +1 -1
- package/lib/cjs/utils/type-utils.js +0 -1
- package/lib/dts/agents/agent.d.ts +5 -0
- package/lib/dts/agents/ai-agent.d.ts +41 -5
- package/lib/dts/agents/chat-model.d.ts +3 -0
- package/lib/dts/agents/model.d.ts +3 -3
- package/lib/dts/prompt/agent-session.d.ts +115 -5
- package/lib/dts/prompt/compact/session-memory-extractor.d.ts +7 -0
- package/lib/dts/prompt/compact/types.d.ts +257 -0
- package/lib/dts/prompt/compact/user-memory-extractor.d.ts +7 -0
- package/lib/dts/prompt/skills/afs/agent-skill/agent-skill.d.ts +2 -0
- package/lib/dts/prompt/skills/afs/agent-skill/skill-loader.d.ts +1 -2
- package/lib/dts/prompt/skills/afs/list.d.ts +2 -0
- package/lib/dts/prompt/skills/afs/read.d.ts +3 -1
- package/lib/esm/agents/agent.d.ts +5 -0
- package/lib/esm/agents/agent.js +5 -0
- package/lib/esm/agents/ai-agent.d.ts +41 -5
- package/lib/esm/agents/ai-agent.js +89 -29
- package/lib/esm/agents/chat-model.d.ts +3 -0
- package/lib/esm/agents/chat-model.js +18 -1
- package/lib/esm/agents/image-model.js +1 -1
- package/lib/esm/agents/model.d.ts +3 -3
- package/lib/esm/agents/model.js +2 -2
- package/lib/esm/agents/video-model.js +1 -1
- package/lib/esm/aigne/context.js +2 -4
- package/lib/esm/prompt/agent-session.d.ts +115 -5
- package/lib/esm/prompt/agent-session.js +744 -84
- package/lib/esm/prompt/compact/compactor.js +4 -0
- package/lib/esm/prompt/compact/session-memory-extractor.d.ts +7 -0
- package/lib/esm/prompt/compact/session-memory-extractor.js +139 -0
- package/lib/esm/prompt/compact/types.d.ts +257 -0
- package/lib/esm/prompt/compact/types.js +34 -0
- package/lib/esm/prompt/compact/user-memory-extractor.d.ts +7 -0
- package/lib/esm/prompt/compact/user-memory-extractor.js +120 -0
- package/lib/esm/prompt/prompt-builder.js +3 -3
- package/lib/esm/prompt/skills/afs/agent-skill/agent-skill.d.ts +2 -0
- package/lib/esm/prompt/skills/afs/agent-skill/agent-skill.js +6 -0
- package/lib/esm/prompt/skills/afs/agent-skill/skill-loader.d.ts +1 -2
- package/lib/esm/prompt/skills/afs/agent-skill/skill-loader.js +13 -24
- package/lib/esm/prompt/skills/afs/list.d.ts +2 -0
- package/lib/esm/prompt/skills/afs/list.js +9 -1
- package/lib/esm/prompt/skills/afs/read.d.ts +3 -1
- package/lib/esm/prompt/skills/afs/read.js +5 -0
- package/lib/esm/utils/mcp-utils.js +1 -1
- package/lib/esm/utils/token-estimator.js +1 -1
- package/lib/esm/utils/type-utils.js +0 -1
- package/package.json +6 -6
|
@@ -1,51 +1,110 @@
|
|
|
1
1
|
import { AFSHistory } from "@aigne/afs-history";
|
|
2
2
|
import { v7 } from "@aigne/uuid";
|
|
3
3
|
import { joinURL } from "ufo";
|
|
4
|
+
import { stringify } from "yaml";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
4
6
|
import { estimateTokens } from "../utils/token-estimator.js";
|
|
5
7
|
import { isNonNullable } from "../utils/type-utils.js";
|
|
6
|
-
import { DEFAULT_COMPACT_ASYNC, DEFAULT_COMPACT_MODE, DEFAULT_KEEP_RECENT_RATIO, DEFAULT_MAX_TOKENS, } from "./compact/types.js";
|
|
8
|
+
import { DEFAULT_COMPACT_ASYNC, DEFAULT_COMPACT_MODE, DEFAULT_KEEP_RECENT_RATIO, DEFAULT_MAX_TOKENS, DEFAULT_MEMORY_QUERY_LIMIT, DEFAULT_MEMORY_RATIO, DEFAULT_SESSION_MEMORY_ASYNC, DEFAULT_SESSION_MEMORY_MODE, DEFAULT_SESSION_MODE, DEFAULT_USER_MEMORY_ASYNC, DEFAULT_USER_MEMORY_MODE, } from "./compact/types.js";
|
|
9
|
+
export * from "./compact/types.js";
|
|
7
10
|
export class AgentSession {
|
|
8
11
|
sessionId;
|
|
9
12
|
userId;
|
|
10
13
|
agentId;
|
|
11
14
|
afs;
|
|
12
15
|
historyModulePath;
|
|
16
|
+
mode;
|
|
13
17
|
compactConfig;
|
|
18
|
+
sessionMemoryConfig;
|
|
19
|
+
userMemoryConfig;
|
|
14
20
|
runtimeState;
|
|
15
21
|
initialized;
|
|
16
22
|
compactionPromise;
|
|
23
|
+
sessionMemoryUpdatePromise;
|
|
24
|
+
userMemoryUpdatePromise;
|
|
17
25
|
constructor(options) {
|
|
18
26
|
this.sessionId = options.sessionId;
|
|
19
27
|
this.userId = options.userId;
|
|
20
28
|
this.agentId = options.agentId;
|
|
21
29
|
this.afs = options.afs;
|
|
30
|
+
this.mode = options.mode ?? DEFAULT_SESSION_MODE;
|
|
22
31
|
this.compactConfig = options.compact ?? {};
|
|
32
|
+
this.sessionMemoryConfig = options.sessionMemory ?? {};
|
|
33
|
+
this.userMemoryConfig = options.userMemory ?? {};
|
|
23
34
|
this.runtimeState = {
|
|
24
35
|
historyEntries: [],
|
|
25
36
|
currentEntry: null,
|
|
26
37
|
};
|
|
27
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if memory extraction is enabled
|
|
41
|
+
* Memory extraction requires mode to be "auto" AND AFS history module to be available
|
|
42
|
+
*/
|
|
43
|
+
get isMemoryEnabled() {
|
|
44
|
+
return this.mode === "auto" && !!this.afs && !!this.historyModulePath;
|
|
45
|
+
}
|
|
28
46
|
async setSystemMessages(...messages) {
|
|
29
47
|
await this.ensureInitialized();
|
|
30
48
|
this.runtimeState.systemMessages = messages;
|
|
31
49
|
}
|
|
32
50
|
async getMessages() {
|
|
33
51
|
await this.ensureInitialized();
|
|
34
|
-
const { systemMessages,
|
|
52
|
+
const { systemMessages, userMemory, sessionMemory, historyCompact, historyEntries, currentEntry, currentEntryCompact, } = this.runtimeState;
|
|
53
|
+
let currentMessages = [];
|
|
54
|
+
if (currentEntry?.messages?.length) {
|
|
55
|
+
if (currentEntryCompact) {
|
|
56
|
+
const { compressedCount, summary } = currentEntryCompact;
|
|
57
|
+
const summaryMessage = {
|
|
58
|
+
role: "user",
|
|
59
|
+
content: `[Earlier messages in this conversation (${compressedCount} messages compressed)]\n${summary}`,
|
|
60
|
+
};
|
|
61
|
+
const remainingMessages = currentEntry.messages.slice(compressedCount);
|
|
62
|
+
currentMessages = [summaryMessage, ...remainingMessages];
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
currentMessages = currentEntry.messages;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Flatten history entries messages once
|
|
69
|
+
const historyMessages = historyEntries.flatMap((entry) => entry.content?.messages ?? []);
|
|
70
|
+
// Check if there's an Agent Skill in current uncompressed messages
|
|
71
|
+
const hasSkillInCurrentMessages = [...historyMessages, ...currentMessages].some((msg) => msg.role === "user" &&
|
|
72
|
+
Array.isArray(msg.content) &&
|
|
73
|
+
msg.content.some((block) => block.type === "text" && block.isAgentSkill === true));
|
|
74
|
+
// Prefer currentEntryCompact's lastAgentSkill over historyCompact's (newer takes priority)
|
|
75
|
+
const lastAgentSkillToInject = currentEntryCompact?.lastAgentSkill ?? historyCompact?.lastAgentSkill;
|
|
35
76
|
const messages = [
|
|
36
77
|
...(systemMessages ?? []),
|
|
37
|
-
...(
|
|
78
|
+
...(userMemory && userMemory.length > 0 ? [this.formatUserMemory(userMemory)] : []),
|
|
79
|
+
...(sessionMemory && sessionMemory.length > 0
|
|
80
|
+
? [this.formatSessionMemory(sessionMemory)]
|
|
81
|
+
: []),
|
|
82
|
+
...(historyCompact?.summary
|
|
38
83
|
? [
|
|
39
84
|
{
|
|
40
85
|
role: "system",
|
|
41
|
-
content: `Previous conversation summary:\n${
|
|
86
|
+
content: `Previous conversation summary:\n${historyCompact.summary}`,
|
|
87
|
+
},
|
|
88
|
+
]
|
|
89
|
+
: []),
|
|
90
|
+
// Only inject lastAgentSkill if there's no skill in current messages
|
|
91
|
+
...(lastAgentSkillToInject && !hasSkillInCurrentMessages
|
|
92
|
+
? [
|
|
93
|
+
{
|
|
94
|
+
role: "user",
|
|
95
|
+
content: [
|
|
96
|
+
{
|
|
97
|
+
type: "text",
|
|
98
|
+
text: lastAgentSkillToInject.content,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
42
101
|
},
|
|
43
102
|
]
|
|
44
103
|
: []),
|
|
45
|
-
...
|
|
46
|
-
...
|
|
104
|
+
...historyMessages,
|
|
105
|
+
...currentMessages,
|
|
47
106
|
];
|
|
48
|
-
// Filter out thinking messages
|
|
107
|
+
// Filter out thinking messages and truncate large messages
|
|
49
108
|
return messages
|
|
50
109
|
.map((msg) => {
|
|
51
110
|
if (!msg.content || typeof msg.content === "string") {
|
|
@@ -57,27 +116,98 @@ export class AgentSession {
|
|
|
57
116
|
return null;
|
|
58
117
|
return { ...msg, content: filteredContent };
|
|
59
118
|
})
|
|
60
|
-
.filter(isNonNullable)
|
|
119
|
+
.filter(isNonNullable)
|
|
120
|
+
.map((msg) => this.truncateLargeMessage(msg));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Format user memory facts into a system message
|
|
124
|
+
* Applies token budget limit to ensure memory injection fits within constraints
|
|
125
|
+
*/
|
|
126
|
+
formatUserMemory(memoryEntries) {
|
|
127
|
+
const memoryRatio = this.userMemoryConfig.memoryRatio ?? DEFAULT_MEMORY_RATIO;
|
|
128
|
+
const maxTokens = Math.floor((this.compactConfig.maxTokens ?? DEFAULT_MAX_TOKENS) * memoryRatio);
|
|
129
|
+
const header = "[User Memory Facts]";
|
|
130
|
+
let currentTokens = estimateTokens(header);
|
|
131
|
+
const facts = [];
|
|
132
|
+
for (const entry of memoryEntries) {
|
|
133
|
+
const fact = entry.content?.fact;
|
|
134
|
+
if (!fact)
|
|
135
|
+
continue;
|
|
136
|
+
const factTokens = estimateTokens(fact);
|
|
137
|
+
// Check if adding this fact would exceed token budget
|
|
138
|
+
if (currentTokens + factTokens > maxTokens) {
|
|
139
|
+
break; // Stop adding facts
|
|
140
|
+
}
|
|
141
|
+
facts.push(fact);
|
|
142
|
+
currentTokens += factTokens;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
role: "system",
|
|
146
|
+
content: this.formatMemoryTemplate({ header, data: facts }),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Format session memory facts into a system message
|
|
151
|
+
* Applies token budget limit to ensure memory injection fits within constraints
|
|
152
|
+
*/
|
|
153
|
+
formatSessionMemory(memoryEntries) {
|
|
154
|
+
const memoryRatio = this.sessionMemoryConfig.memoryRatio ?? DEFAULT_MEMORY_RATIO;
|
|
155
|
+
const maxTokens = Math.floor((this.compactConfig.maxTokens ?? DEFAULT_MAX_TOKENS) * memoryRatio);
|
|
156
|
+
const header = "[Session Memory Facts]";
|
|
157
|
+
let currentTokens = estimateTokens(header);
|
|
158
|
+
const facts = [];
|
|
159
|
+
for (const entry of memoryEntries) {
|
|
160
|
+
const fact = entry.content?.fact;
|
|
161
|
+
if (!fact)
|
|
162
|
+
continue;
|
|
163
|
+
const factTokens = estimateTokens(fact);
|
|
164
|
+
// Check if adding this fact would exceed token budget
|
|
165
|
+
if (currentTokens + factTokens > maxTokens) {
|
|
166
|
+
break; // Stop adding facts
|
|
167
|
+
}
|
|
168
|
+
facts.push(fact);
|
|
169
|
+
currentTokens += factTokens;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
role: "system",
|
|
173
|
+
content: this.formatMemoryTemplate({ header, data: facts }),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
formatMemoryTemplate({ header, data }) {
|
|
177
|
+
return `\
|
|
178
|
+
${header}
|
|
179
|
+
|
|
180
|
+
${"```yaml"}
|
|
181
|
+
${stringify(data)}
|
|
182
|
+
${"```"}
|
|
183
|
+
`;
|
|
61
184
|
}
|
|
62
185
|
async startMessage(input, message, options) {
|
|
63
186
|
await this.ensureInitialized();
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
187
|
+
// Only run compact if mode is not disabled
|
|
188
|
+
if (this.mode !== "disabled") {
|
|
189
|
+
await this.maybeAutoCompact(options);
|
|
190
|
+
// Always wait for compaction to complete before starting a new message
|
|
191
|
+
// This ensures data consistency even in async compact mode
|
|
192
|
+
if (this.compactionPromise)
|
|
193
|
+
await this.compactionPromise;
|
|
194
|
+
}
|
|
195
|
+
this.runtimeState.currentEntryCompact = undefined;
|
|
69
196
|
this.runtimeState.currentEntry = { input, messages: [message] };
|
|
70
197
|
}
|
|
71
|
-
async endMessage(output, options) {
|
|
198
|
+
async endMessage(output, message, options) {
|
|
72
199
|
await this.ensureInitialized();
|
|
73
200
|
if (!this.runtimeState.currentEntry?.input ||
|
|
74
201
|
!this.runtimeState.currentEntry.messages?.length) {
|
|
75
202
|
throw new Error("No current entry to end. Call startMessage() first.");
|
|
76
203
|
}
|
|
204
|
+
if (message)
|
|
205
|
+
this.runtimeState.currentEntry.messages.push(message);
|
|
77
206
|
this.runtimeState.currentEntry.output = output;
|
|
78
207
|
let newEntry;
|
|
79
|
-
if
|
|
80
|
-
|
|
208
|
+
// Only persist to AFS if mode is not disabled
|
|
209
|
+
if (this.mode !== "disabled" && this.afs && this.historyModulePath) {
|
|
210
|
+
newEntry = (await this.afs.write(joinURL(this.historyModulePath, "by-session", this.sessionId, "new"), {
|
|
81
211
|
userId: this.userId,
|
|
82
212
|
sessionId: this.sessionId,
|
|
83
213
|
agentId: this.agentId,
|
|
@@ -85,6 +215,7 @@ export class AgentSession {
|
|
|
85
215
|
})).data;
|
|
86
216
|
}
|
|
87
217
|
else {
|
|
218
|
+
// Create in-memory entry for runtime state
|
|
88
219
|
const id = v7();
|
|
89
220
|
newEntry = {
|
|
90
221
|
id,
|
|
@@ -97,8 +228,16 @@ export class AgentSession {
|
|
|
97
228
|
}
|
|
98
229
|
this.runtimeState.historyEntries.push(newEntry);
|
|
99
230
|
this.runtimeState.currentEntry = null;
|
|
100
|
-
|
|
101
|
-
|
|
231
|
+
this.runtimeState.currentEntryCompact = undefined;
|
|
232
|
+
// Only run compact and memory extraction if mode is not disabled
|
|
233
|
+
if (this.mode !== "disabled") {
|
|
234
|
+
await Promise.all([
|
|
235
|
+
// Check if auto-compact should be triggered
|
|
236
|
+
this.maybeAutoCompact(options),
|
|
237
|
+
// Check if auto-update session memory should be triggered
|
|
238
|
+
this.maybeAutoUpdateSessionMemory(options),
|
|
239
|
+
]);
|
|
240
|
+
}
|
|
102
241
|
}
|
|
103
242
|
/**
|
|
104
243
|
* Manually trigger compaction
|
|
@@ -113,36 +252,34 @@ export class AgentSession {
|
|
|
113
252
|
this.compactionPromise = this.doCompact(options).finally(() => {
|
|
114
253
|
this.compactionPromise = undefined;
|
|
115
254
|
});
|
|
116
|
-
|
|
255
|
+
const isAsync = this.compactConfig.async ?? DEFAULT_COMPACT_ASYNC;
|
|
256
|
+
if (!isAsync)
|
|
257
|
+
await this.compactionPromise;
|
|
117
258
|
}
|
|
118
259
|
/**
|
|
119
260
|
* Internal method that performs the actual compaction
|
|
120
261
|
*/
|
|
121
262
|
async doCompact(options) {
|
|
122
|
-
const { compactor
|
|
263
|
+
const { compactor } = this.compactConfig ?? {};
|
|
123
264
|
if (!compactor) {
|
|
124
265
|
throw new Error("Cannot compact without a compactor agent configured.");
|
|
125
266
|
}
|
|
126
267
|
const historyEntries = this.runtimeState.historyEntries;
|
|
127
268
|
if (historyEntries.length === 0)
|
|
128
269
|
return;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}, 0);
|
|
143
|
-
// Subtract system and current tokens from budget
|
|
144
|
-
// This ensures total tokens (system + current + kept history) stays within ratio budget
|
|
145
|
-
keepTokenBudget = Math.max(0, keepTokenBudget - systemTokens - currentTokens);
|
|
270
|
+
const maxTokens = this.maxTokens;
|
|
271
|
+
// Target to keep only 50% of keepRecentTokens to leave buffer room
|
|
272
|
+
// This avoids triggering compression again shortly after compaction
|
|
273
|
+
// Similar to compactCurrentEntry, we compress more aggressively to leave headroom
|
|
274
|
+
//
|
|
275
|
+
// Note: We don't subtract systemTokens or currentEntry tokens because:
|
|
276
|
+
// 1. keepRecentTokens is already a relative ratio (e.g., 50% of maxTokens)
|
|
277
|
+
// 2. systemTokens overhead is typically small (~1-2k, ~1-2% of maxTokens)
|
|
278
|
+
// 3. currentEntry is still being constructed (not yet added to history)
|
|
279
|
+
// 4. In tool use scenarios, currentEntry can be very large (many tool calls)
|
|
280
|
+
// 5. Subtracting them would complicate logic without significant benefit
|
|
281
|
+
// 6. Total token limit is enforced by maybeAutoCompact trigger condition
|
|
282
|
+
const keepRecentTokens = this.keepRecentTokens * 0.5;
|
|
146
283
|
// Find split point by iterating backwards from most recent entry
|
|
147
284
|
// The split point divides history into: [compact] | [keep]
|
|
148
285
|
let splitIndex = historyEntries.length; // Default: keep all (no compaction)
|
|
@@ -153,7 +290,7 @@ export class AgentSession {
|
|
|
153
290
|
continue;
|
|
154
291
|
const entryTokens = this.estimateMessagesTokens(entry.content?.messages ?? []);
|
|
155
292
|
// Check if adding this entry would exceed token budget
|
|
156
|
-
if (accumulatedTokens + entryTokens >
|
|
293
|
+
if (accumulatedTokens + entryTokens > keepRecentTokens) {
|
|
157
294
|
// Would exceed budget, split here (this entry and earlier ones will be compacted)
|
|
158
295
|
splitIndex = i + 1;
|
|
159
296
|
break;
|
|
@@ -175,58 +312,179 @@ export class AgentSession {
|
|
|
175
312
|
// Split into batches to avoid context overflow
|
|
176
313
|
const batches = this.splitIntoBatches(entriesToCompact, maxTokens);
|
|
177
314
|
// Process batches incrementally, each summary becomes input for the next
|
|
178
|
-
let currentSummary = this.runtimeState.
|
|
315
|
+
let currentSummary = this.runtimeState.historyCompact?.summary;
|
|
179
316
|
for (const batch of batches) {
|
|
317
|
+
const messages = batch
|
|
318
|
+
.flatMap((e) => e.content?.messages ?? [])
|
|
319
|
+
.filter(isNonNullable)
|
|
320
|
+
.map((msg) => this.truncateLargeMessage(msg));
|
|
180
321
|
const result = await options.context.invoke(compactor, {
|
|
181
322
|
previousSummary: [currentSummary].filter(isNonNullable),
|
|
182
|
-
messages
|
|
323
|
+
messages,
|
|
183
324
|
});
|
|
184
325
|
currentSummary = result.summary;
|
|
185
326
|
}
|
|
327
|
+
// Extract last Agent Skill from entries to compact
|
|
328
|
+
let lastAgentSkill = this.findLastAgentSkill(entriesToCompact);
|
|
329
|
+
// If no skill found in entries to compact, inherit from previous compact
|
|
330
|
+
if (!lastAgentSkill && this.runtimeState.historyCompact?.lastAgentSkill) {
|
|
331
|
+
lastAgentSkill = this.runtimeState.historyCompact.lastAgentSkill;
|
|
332
|
+
}
|
|
333
|
+
// Create compact content
|
|
334
|
+
const historyCompact = {
|
|
335
|
+
summary: currentSummary ?? "",
|
|
336
|
+
lastAgentSkill,
|
|
337
|
+
};
|
|
186
338
|
// Write compact entry to AFS
|
|
187
339
|
if (this.afs && this.historyModulePath) {
|
|
188
340
|
await this.afs.write(joinURL(this.historyModulePath, "by-session", this.sessionId, "@metadata/compact/new"), {
|
|
189
341
|
userId: this.userId,
|
|
190
342
|
agentId: this.agentId,
|
|
191
|
-
content:
|
|
343
|
+
content: historyCompact,
|
|
192
344
|
metadata: {
|
|
193
345
|
latestEntryId: latestCompactedEntry.id,
|
|
194
346
|
},
|
|
195
347
|
});
|
|
196
348
|
}
|
|
197
349
|
// Update runtime state: keep the summary and recent entries
|
|
198
|
-
this.runtimeState.
|
|
350
|
+
this.runtimeState.historyCompact = historyCompact;
|
|
199
351
|
this.runtimeState.historyEntries = entriesToKeep;
|
|
200
352
|
}
|
|
353
|
+
async compactCurrentEntry(options) {
|
|
354
|
+
const { compactor } = this.compactConfig ?? {};
|
|
355
|
+
if (!compactor)
|
|
356
|
+
return;
|
|
357
|
+
const currentEntry = this.runtimeState.currentEntry;
|
|
358
|
+
if (!currentEntry?.messages?.length)
|
|
359
|
+
return;
|
|
360
|
+
const alreadyCompressedCount = this.runtimeState.currentEntryCompact?.compressedCount ?? 0;
|
|
361
|
+
const uncompressedMessages = currentEntry.messages.slice(alreadyCompressedCount);
|
|
362
|
+
if (uncompressedMessages.length === 0)
|
|
363
|
+
return;
|
|
364
|
+
// Target to keep only 50% of keepTokenBudget to leave buffer room
|
|
365
|
+
// This avoids frequent small-batch compressions in tool use scenarios
|
|
366
|
+
const keepTokenBudget = this.keepRecentTokens * 0.5;
|
|
367
|
+
let splitIndex = uncompressedMessages.length;
|
|
368
|
+
let accumulatedTokens = 0;
|
|
369
|
+
for (let i = uncompressedMessages.length - 1; i >= 0; i--) {
|
|
370
|
+
const msg = uncompressedMessages[i];
|
|
371
|
+
if (!msg)
|
|
372
|
+
continue;
|
|
373
|
+
const msgTokens = this.estimateMessagesTokens([msg]);
|
|
374
|
+
if (accumulatedTokens + msgTokens > keepTokenBudget) {
|
|
375
|
+
splitIndex = i + 1;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
accumulatedTokens += msgTokens;
|
|
379
|
+
splitIndex = i;
|
|
380
|
+
}
|
|
381
|
+
const keptMessages = uncompressedMessages.slice(splitIndex);
|
|
382
|
+
const requiredToolCallIds = new Set();
|
|
383
|
+
for (const msg of keptMessages) {
|
|
384
|
+
if (msg.role === "tool" && msg.toolCallId) {
|
|
385
|
+
requiredToolCallIds.add(msg.toolCallId);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (requiredToolCallIds.size > 0) {
|
|
389
|
+
for (let i = splitIndex - 1; i >= 0; i--) {
|
|
390
|
+
const msg = uncompressedMessages[i];
|
|
391
|
+
if (!msg?.toolCalls)
|
|
392
|
+
continue;
|
|
393
|
+
for (const toolCall of msg.toolCalls) {
|
|
394
|
+
if (requiredToolCallIds.has(toolCall.id)) {
|
|
395
|
+
splitIndex = i;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const messagesToCompact = uncompressedMessages
|
|
402
|
+
.slice(0, splitIndex)
|
|
403
|
+
.map((msg) => this.truncateLargeMessage(msg));
|
|
404
|
+
if (messagesToCompact.length === 0)
|
|
405
|
+
return;
|
|
406
|
+
const result = await options.context.invoke(compactor, {
|
|
407
|
+
previousSummary: this.runtimeState.currentEntryCompact?.summary
|
|
408
|
+
? [this.runtimeState.currentEntryCompact.summary]
|
|
409
|
+
: undefined,
|
|
410
|
+
messages: messagesToCompact,
|
|
411
|
+
});
|
|
412
|
+
// Find last Agent Skill from messages being compacted
|
|
413
|
+
const lastAgentSkill = this.findLastAgentSkillFromMessages(messagesToCompact) ??
|
|
414
|
+
this.runtimeState.currentEntryCompact?.lastAgentSkill;
|
|
415
|
+
this.runtimeState.currentEntryCompact = {
|
|
416
|
+
summary: result.summary,
|
|
417
|
+
lastAgentSkill,
|
|
418
|
+
compressedCount: alreadyCompressedCount + messagesToCompact.length,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
async maybeCompactCurrentEntry(options) {
|
|
422
|
+
const currentEntry = this.runtimeState.currentEntry;
|
|
423
|
+
if (!currentEntry?.messages?.length)
|
|
424
|
+
return;
|
|
425
|
+
const compressedCount = this.runtimeState.currentEntryCompact?.compressedCount ?? 0;
|
|
426
|
+
const uncompressedMessages = currentEntry.messages.slice(compressedCount);
|
|
427
|
+
const threshold = this.keepRecentTokens;
|
|
428
|
+
const currentTokens = this.estimateMessagesTokens(uncompressedMessages);
|
|
429
|
+
if (currentTokens > threshold) {
|
|
430
|
+
await this.compactCurrentEntry(options);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
201
433
|
async maybeAutoCompact(options) {
|
|
202
434
|
if (this.compactionPromise)
|
|
203
435
|
await this.compactionPromise;
|
|
204
|
-
if (!this.compactConfig)
|
|
205
|
-
return;
|
|
206
|
-
// Check if compaction is disabled
|
|
207
436
|
const mode = this.compactConfig.mode ?? DEFAULT_COMPACT_MODE;
|
|
208
437
|
if (mode === "disabled")
|
|
209
438
|
return;
|
|
210
439
|
const { compactor } = this.compactConfig;
|
|
211
|
-
const maxTokens = this.compactConfig.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
212
440
|
if (!compactor)
|
|
213
441
|
return;
|
|
214
|
-
const
|
|
442
|
+
const maxTokens = this.maxTokens;
|
|
443
|
+
const messages = await this.getMessages();
|
|
444
|
+
const currentTokens = this.estimateMessagesTokens(messages);
|
|
215
445
|
if (currentTokens >= maxTokens) {
|
|
216
|
-
this.compact(options);
|
|
217
|
-
const isAsync = this.compactConfig.async ?? DEFAULT_COMPACT_ASYNC;
|
|
218
|
-
if (!isAsync)
|
|
219
|
-
await this.compactionPromise;
|
|
446
|
+
await this.compact(options);
|
|
220
447
|
}
|
|
221
448
|
}
|
|
222
449
|
/**
|
|
223
|
-
* Estimate token count for
|
|
450
|
+
* Estimate token count for messages
|
|
451
|
+
* Applies singleMessageLimit to each text block individually
|
|
452
|
+
* Non-text tokens (images, tool calls) are always counted in full
|
|
224
453
|
*/
|
|
225
|
-
estimateMessagesTokens(messages) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
454
|
+
estimateMessagesTokens(messages, singleMessageLimit = this.singleMessageLimit) {
|
|
455
|
+
let totalTokens = 0;
|
|
456
|
+
for (const msg of messages) {
|
|
457
|
+
// 1. Estimate content tokens
|
|
458
|
+
if (typeof msg.content === "string") {
|
|
459
|
+
const textTokens = estimateTokens(msg.content);
|
|
460
|
+
const effectiveTokens = textTokens > singleMessageLimit ? singleMessageLimit : textTokens;
|
|
461
|
+
totalTokens += effectiveTokens;
|
|
462
|
+
}
|
|
463
|
+
else if (Array.isArray(msg.content)) {
|
|
464
|
+
for (const block of msg.content) {
|
|
465
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
466
|
+
// Text tokens (can be truncated) - apply limit to each block individually
|
|
467
|
+
const textTokens = estimateTokens(block.text);
|
|
468
|
+
const effectiveTokens = textTokens > singleMessageLimit ? singleMessageLimit : textTokens;
|
|
469
|
+
totalTokens += effectiveTokens;
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
// Non-text blocks - always counted in full
|
|
473
|
+
totalTokens += 1000;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// 2. Estimate tool calls tokens (cannot be truncated)
|
|
478
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
479
|
+
for (const toolCall of msg.toolCalls) {
|
|
480
|
+
// Function name + arguments + overhead
|
|
481
|
+
totalTokens += estimateTokens(toolCall.function.name);
|
|
482
|
+
totalTokens += estimateTokens(stringify(toolCall.function.arguments).replace(/\s+/g, " "));
|
|
483
|
+
totalTokens += 10; // Structure overhead
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return totalTokens;
|
|
230
488
|
}
|
|
231
489
|
/**
|
|
232
490
|
* Split entries into batches based on token limit
|
|
@@ -255,12 +513,63 @@ export class AgentSession {
|
|
|
255
513
|
}
|
|
256
514
|
return batches;
|
|
257
515
|
}
|
|
258
|
-
async appendCurrentMessages(
|
|
516
|
+
async appendCurrentMessages(messages, options) {
|
|
259
517
|
await this.ensureInitialized();
|
|
260
518
|
if (!this.runtimeState.currentEntry || !this.runtimeState.currentEntry.messages?.length) {
|
|
261
519
|
throw new Error("No current entry to append messages. Call startMessage() first.");
|
|
262
520
|
}
|
|
263
|
-
this.runtimeState.currentEntry.messages.push(...messages);
|
|
521
|
+
this.runtimeState.currentEntry.messages.push(...[messages].flat());
|
|
522
|
+
await this.maybeCompactCurrentEntry(options);
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Truncate text content to fit within target token limit
|
|
526
|
+
* @param text The text to truncate
|
|
527
|
+
* @param currentTokens Current token count of the text
|
|
528
|
+
* @param targetTokens Target token count after truncation
|
|
529
|
+
* @returns Truncated text
|
|
530
|
+
*/
|
|
531
|
+
truncateText(text, currentTokens, targetTokens) {
|
|
532
|
+
if (currentTokens <= targetTokens)
|
|
533
|
+
return text;
|
|
534
|
+
const keepRatio = (targetTokens / currentTokens) * 0.9;
|
|
535
|
+
const keepLength = Math.floor(text.length * keepRatio);
|
|
536
|
+
const headLength = Math.floor(keepLength * 0.7);
|
|
537
|
+
const tailLength = Math.floor(keepLength * 0.3);
|
|
538
|
+
return (text.slice(0, headLength) +
|
|
539
|
+
`\n\n[... truncated ${currentTokens - targetTokens} tokens ...]\n\n` +
|
|
540
|
+
text.slice(-tailLength));
|
|
541
|
+
}
|
|
542
|
+
truncateLargeMessage(msg) {
|
|
543
|
+
const singleMessageLimit = this.singleMessageLimit;
|
|
544
|
+
// Handle string content
|
|
545
|
+
if (typeof msg.content === "string") {
|
|
546
|
+
const tokens = estimateTokens(msg.content);
|
|
547
|
+
if (tokens <= singleMessageLimit)
|
|
548
|
+
return msg;
|
|
549
|
+
const truncated = this.truncateText(msg.content, tokens, singleMessageLimit);
|
|
550
|
+
return { ...msg, content: truncated };
|
|
551
|
+
}
|
|
552
|
+
// Handle array content (UnionContent[])
|
|
553
|
+
if (Array.isArray(msg.content)) {
|
|
554
|
+
// Truncate each text block individually if it exceeds the limit
|
|
555
|
+
const truncatedContent = msg.content.map((block) => {
|
|
556
|
+
// Keep non-text blocks unchanged
|
|
557
|
+
if (block.type !== "text" || typeof block.text !== "string") {
|
|
558
|
+
return block;
|
|
559
|
+
}
|
|
560
|
+
// Check if this text block needs truncation
|
|
561
|
+
const blockTokens = estimateTokens(block.text);
|
|
562
|
+
if (blockTokens <= singleMessageLimit) {
|
|
563
|
+
return block;
|
|
564
|
+
}
|
|
565
|
+
// Truncate this text block independently
|
|
566
|
+
const truncatedText = this.truncateText(block.text, blockTokens, singleMessageLimit);
|
|
567
|
+
return { ...block, text: truncatedText };
|
|
568
|
+
});
|
|
569
|
+
return { ...msg, content: truncatedContent };
|
|
570
|
+
}
|
|
571
|
+
// Unknown content type, return as-is
|
|
572
|
+
return msg;
|
|
264
573
|
}
|
|
265
574
|
async ensureInitialized() {
|
|
266
575
|
this.initialized ??= this.initialize();
|
|
@@ -270,39 +579,390 @@ export class AgentSession {
|
|
|
270
579
|
if (this.initialized)
|
|
271
580
|
return;
|
|
272
581
|
await this.initializeDefaultCompactor();
|
|
582
|
+
await this.initializeDefaultSessionMemoryExtractor();
|
|
583
|
+
await this.initializeDefaultUserMemoryExtractor();
|
|
273
584
|
const historyModule = (await this.afs?.listModules())?.find((m) => m.module instanceof AFSHistory);
|
|
274
585
|
this.historyModulePath = historyModule?.path;
|
|
275
586
|
if (this.afs && this.historyModulePath) {
|
|
276
|
-
// Load
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
587
|
+
// Load user memory, session memory, and session history in parallel
|
|
588
|
+
const [userMemory, sessionMemory, sessionHistory] = await Promise.all([
|
|
589
|
+
this.loadUserMemory(),
|
|
590
|
+
this.loadSessionMemory(),
|
|
591
|
+
this.loadSessionHistory(),
|
|
592
|
+
]);
|
|
593
|
+
// Update runtime state with loaded data
|
|
594
|
+
this.runtimeState.userMemory = userMemory;
|
|
595
|
+
this.runtimeState.sessionMemory = sessionMemory;
|
|
596
|
+
this.runtimeState.historyCompact = sessionHistory.historyCompact;
|
|
597
|
+
this.runtimeState.historyEntries = sessionHistory.historyEntries;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Load session memory facts
|
|
602
|
+
* @returns Array of memory fact entries for the current session
|
|
603
|
+
*/
|
|
604
|
+
async loadSessionMemory() {
|
|
605
|
+
if (!this.afs || !this.historyModulePath)
|
|
606
|
+
return [];
|
|
607
|
+
// Check if session memory is disabled
|
|
608
|
+
const mode = this.sessionMemoryConfig.mode ?? DEFAULT_SESSION_MEMORY_MODE;
|
|
609
|
+
if (mode === "disabled")
|
|
610
|
+
return [];
|
|
611
|
+
const sessionMemoryPath = joinURL(this.historyModulePath, "by-session", this.sessionId, "@metadata/memory");
|
|
612
|
+
const queryLimit = this.sessionMemoryConfig.queryLimit ?? DEFAULT_MEMORY_QUERY_LIMIT;
|
|
613
|
+
const memoryResult = await this.afs.list(sessionMemoryPath, {
|
|
614
|
+
filter: { userId: this.userId, agentId: this.agentId },
|
|
615
|
+
orderBy: [["updatedAt", "desc"]],
|
|
616
|
+
limit: queryLimit,
|
|
617
|
+
});
|
|
618
|
+
// Filter out entries without content
|
|
619
|
+
const facts = memoryResult.data
|
|
620
|
+
.reverse()
|
|
621
|
+
.filter((entry) => isNonNullable(entry.content));
|
|
622
|
+
return facts;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Load user memory facts
|
|
626
|
+
* @returns Array of memory fact entries for the current user
|
|
627
|
+
*/
|
|
628
|
+
async loadUserMemory() {
|
|
629
|
+
if (!this.afs || !this.historyModulePath || !this.userId)
|
|
630
|
+
return [];
|
|
631
|
+
// Check if user memory is disabled
|
|
632
|
+
const mode = this.userMemoryConfig.mode ?? DEFAULT_USER_MEMORY_MODE;
|
|
633
|
+
if (mode === "disabled")
|
|
634
|
+
return [];
|
|
635
|
+
const userMemoryPath = joinURL(this.historyModulePath, "by-user", this.userId, "@metadata/memory");
|
|
636
|
+
const queryLimit = this.userMemoryConfig.queryLimit ?? DEFAULT_MEMORY_QUERY_LIMIT;
|
|
637
|
+
const memoryResult = await this.afs.list(userMemoryPath, {
|
|
638
|
+
filter: { userId: this.userId, agentId: this.agentId },
|
|
639
|
+
orderBy: [["updatedAt", "desc"]],
|
|
640
|
+
limit: queryLimit,
|
|
641
|
+
});
|
|
642
|
+
// Filter out entries without content
|
|
643
|
+
const facts = memoryResult.data
|
|
644
|
+
.reverse()
|
|
645
|
+
.filter((entry) => isNonNullable(entry.content));
|
|
646
|
+
return facts;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Load session history including compact content and history entries
|
|
650
|
+
* @returns Object containing history compact and history entries
|
|
651
|
+
*/
|
|
652
|
+
async loadSessionHistory() {
|
|
653
|
+
if (!this.afs || !this.historyModulePath) {
|
|
654
|
+
return { historyEntries: [] };
|
|
655
|
+
}
|
|
656
|
+
// Load latest compact entry if exists
|
|
657
|
+
const compactPath = joinURL(this.historyModulePath, "by-session", this.sessionId, "@metadata/compact");
|
|
658
|
+
const compactResult = await this.afs.list(compactPath, {
|
|
659
|
+
filter: { userId: this.userId, agentId: this.agentId },
|
|
660
|
+
orderBy: [["createdAt", "desc"]],
|
|
661
|
+
limit: 1,
|
|
662
|
+
});
|
|
663
|
+
const latestCompact = compactResult.data[0];
|
|
664
|
+
const historyCompact = latestCompact?.content;
|
|
665
|
+
// Load history entries (after compact point if exists)
|
|
666
|
+
const afsEntries = (await this.afs.list(joinURL(this.historyModulePath, "by-session", this.sessionId), {
|
|
667
|
+
filter: {
|
|
668
|
+
userId: this.userId,
|
|
669
|
+
agentId: this.agentId,
|
|
670
|
+
// Only load entries after the latest compact
|
|
671
|
+
after: latestCompact?.createdAt?.toISOString(),
|
|
672
|
+
},
|
|
673
|
+
orderBy: [["createdAt", "desc"]],
|
|
674
|
+
// Set a very large limit to load all history entries
|
|
675
|
+
// The default limit is 10 which would cause history truncation
|
|
676
|
+
limit: 10000,
|
|
677
|
+
})).data;
|
|
678
|
+
const historyEntries = afsEntries.reverse().filter((entry) => isNonNullable(entry.content));
|
|
679
|
+
return {
|
|
680
|
+
historyCompact,
|
|
681
|
+
historyEntries,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Manually trigger session memory update
|
|
686
|
+
*/
|
|
687
|
+
async updateSessionMemory(options) {
|
|
688
|
+
await this.ensureInitialized();
|
|
689
|
+
this.sessionMemoryUpdatePromise ??= this.doUpdateSessionMemory(options)
|
|
690
|
+
.then(() => {
|
|
691
|
+
// After session memory update succeeds, potentially trigger user memory consolidation
|
|
692
|
+
this.maybeAutoUpdateUserMemory(options).catch((err) => {
|
|
693
|
+
logger.error("User memory update failed:", err);
|
|
282
694
|
});
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
695
|
+
})
|
|
696
|
+
.finally(() => {
|
|
697
|
+
this.sessionMemoryUpdatePromise = undefined;
|
|
698
|
+
});
|
|
699
|
+
return this.sessionMemoryUpdatePromise;
|
|
700
|
+
}
|
|
701
|
+
async maybeAutoUpdateSessionMemory(options) {
|
|
702
|
+
// Check if memory extraction is enabled (requires AFS history module)
|
|
703
|
+
if (!this.isMemoryEnabled)
|
|
704
|
+
return;
|
|
705
|
+
// Check if mode is disabled
|
|
706
|
+
const mode = this.sessionMemoryConfig.mode ?? DEFAULT_SESSION_MEMORY_MODE;
|
|
707
|
+
if (mode === "disabled")
|
|
708
|
+
return;
|
|
709
|
+
// Trigger session memory update
|
|
710
|
+
this.updateSessionMemory(options).catch((err) => {
|
|
711
|
+
logger.error("Session memory update failed:", err);
|
|
712
|
+
});
|
|
713
|
+
const isAsync = this.sessionMemoryConfig.async ?? DEFAULT_SESSION_MEMORY_ASYNC;
|
|
714
|
+
if (!isAsync)
|
|
715
|
+
await this.sessionMemoryUpdatePromise;
|
|
716
|
+
}
|
|
717
|
+
async maybeAutoUpdateUserMemory(options) {
|
|
718
|
+
// Check if memory extraction is enabled (requires AFS history module)
|
|
719
|
+
if (!this.isMemoryEnabled || !this.userId)
|
|
720
|
+
return;
|
|
721
|
+
// Check if mode is disabled
|
|
722
|
+
const mode = this.userMemoryConfig.mode ?? DEFAULT_USER_MEMORY_MODE;
|
|
723
|
+
if (mode === "disabled")
|
|
724
|
+
return;
|
|
725
|
+
// Trigger user memory consolidation
|
|
726
|
+
this.updateUserMemory(options).catch((err) => {
|
|
727
|
+
logger.error("User memory update failed:", err);
|
|
728
|
+
});
|
|
729
|
+
const isAsync = this.userMemoryConfig.async ?? DEFAULT_USER_MEMORY_ASYNC;
|
|
730
|
+
if (!isAsync)
|
|
731
|
+
await this.userMemoryUpdatePromise;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Internal method that performs the actual session memory update
|
|
735
|
+
*/
|
|
736
|
+
async doUpdateSessionMemory(options) {
|
|
737
|
+
const { extractor } = this.sessionMemoryConfig ?? {};
|
|
738
|
+
if (!extractor) {
|
|
739
|
+
throw new Error("Cannot update session memory without an extractor agent configured.");
|
|
740
|
+
}
|
|
741
|
+
// Get latestEntryId from the most recent memory entry's metadata
|
|
742
|
+
// This tells us which history entries have already been processed
|
|
743
|
+
const latestEntryId = this.runtimeState.sessionMemory?.at(-1)?.metadata?.latestEntryId;
|
|
744
|
+
// Filter unextracted entries based on latestEntryId
|
|
745
|
+
// Similar to compact mechanism, we find the position of the last extracted entry
|
|
746
|
+
// and only process entries after that point
|
|
747
|
+
const lastExtractedIndex = latestEntryId
|
|
748
|
+
? this.runtimeState.historyEntries.findIndex((e) => e.id === latestEntryId)
|
|
749
|
+
: -1;
|
|
750
|
+
const unextractedEntries = lastExtractedIndex >= 0
|
|
751
|
+
? this.runtimeState.historyEntries.slice(lastExtractedIndex + 1)
|
|
752
|
+
: this.runtimeState.historyEntries;
|
|
753
|
+
if (unextractedEntries.length === 0)
|
|
754
|
+
return;
|
|
755
|
+
// Get recent conversation messages for extraction
|
|
756
|
+
const recentMessages = unextractedEntries
|
|
757
|
+
.flatMap((entry) => entry.content?.messages ?? [])
|
|
758
|
+
.filter(isNonNullable);
|
|
759
|
+
if (recentMessages.length === 0)
|
|
760
|
+
return;
|
|
761
|
+
// Get existing session memory facts for context
|
|
762
|
+
const existingFacts = this.runtimeState.sessionMemory?.map((entry) => entry.content).filter(isNonNullable) ?? [];
|
|
763
|
+
// Get user memory facts to avoid duplication
|
|
764
|
+
const existingUserFacts = this.runtimeState.userMemory?.map((entry) => entry.content).filter(isNonNullable) ?? [];
|
|
765
|
+
// Extract new facts from conversation
|
|
766
|
+
const result = await options.context.invoke(extractor, {
|
|
767
|
+
existingUserFacts,
|
|
768
|
+
existingFacts,
|
|
769
|
+
messages: recentMessages,
|
|
770
|
+
});
|
|
771
|
+
// If no changes, nothing to do
|
|
772
|
+
if (!result.newFacts.length && !result.removeFacts?.length) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
// Get the last entry to record its ID for metadata
|
|
776
|
+
const latestExtractedEntry = unextractedEntries.at(-1);
|
|
777
|
+
if (this.afs && this.historyModulePath) {
|
|
778
|
+
// Handle fact removal
|
|
779
|
+
if (result.removeFacts?.length && this.runtimeState.sessionMemory) {
|
|
780
|
+
const entriesToRemove = [];
|
|
781
|
+
for (const label of result.removeFacts) {
|
|
782
|
+
const entry = this.runtimeState.sessionMemory.find((e) => e.content?.label === label);
|
|
783
|
+
if (entry)
|
|
784
|
+
entriesToRemove.push(entry);
|
|
785
|
+
}
|
|
786
|
+
// Remove from AFS storage and runtime state
|
|
787
|
+
for (const entryToRemove of entriesToRemove) {
|
|
788
|
+
// Delete from AFS storage
|
|
789
|
+
const memoryEntryPath = joinURL(this.historyModulePath, "by-session", this.sessionId, "@metadata/memory", entryToRemove.id);
|
|
790
|
+
await this.afs.delete(memoryEntryPath);
|
|
791
|
+
// Remove from runtime state
|
|
792
|
+
const index = this.runtimeState.sessionMemory.indexOf(entryToRemove);
|
|
793
|
+
if (index !== -1) {
|
|
794
|
+
this.runtimeState.sessionMemory.splice(index, 1);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
// Handle new facts
|
|
799
|
+
if (result.newFacts.length) {
|
|
800
|
+
const sessionMemoryPath = joinURL(this.historyModulePath, "by-session", this.sessionId, "@metadata/memory/new");
|
|
801
|
+
for (const fact of result.newFacts) {
|
|
802
|
+
const newEntry = await this.afs.write(sessionMemoryPath, {
|
|
803
|
+
userId: this.userId,
|
|
804
|
+
sessionId: this.sessionId,
|
|
805
|
+
agentId: this.agentId,
|
|
806
|
+
content: fact,
|
|
807
|
+
metadata: {
|
|
808
|
+
latestEntryId: latestExtractedEntry?.id,
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
// Add to runtime state
|
|
812
|
+
this.runtimeState.sessionMemory ??= [];
|
|
813
|
+
this.runtimeState.sessionMemory.push(newEntry.data);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Manually trigger user memory update
|
|
820
|
+
*/
|
|
821
|
+
async updateUserMemory(options) {
|
|
822
|
+
await this.ensureInitialized();
|
|
823
|
+
// Start new user memory update task
|
|
824
|
+
this.userMemoryUpdatePromise ??= this.doUpdateUserMemory(options).finally(() => {
|
|
825
|
+
this.userMemoryUpdatePromise = undefined;
|
|
826
|
+
});
|
|
827
|
+
return this.userMemoryUpdatePromise;
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Internal method that performs the actual user memory extraction
|
|
831
|
+
*/
|
|
832
|
+
async doUpdateUserMemory(options) {
|
|
833
|
+
const { extractor } = this.userMemoryConfig ?? {};
|
|
834
|
+
if (!extractor) {
|
|
835
|
+
throw new Error("Cannot update user memory without an extractor agent configured.");
|
|
836
|
+
}
|
|
837
|
+
// Get session memory facts as the source for consolidation
|
|
838
|
+
const sessionFacts = this.runtimeState.sessionMemory?.map((entry) => entry.content).filter(isNonNullable) ?? [];
|
|
839
|
+
if (sessionFacts.length === 0)
|
|
840
|
+
return;
|
|
841
|
+
// Get existing user memory facts for context and deduplication
|
|
842
|
+
const existingUserFacts = this.runtimeState.userMemory?.map((entry) => entry.content).filter(isNonNullable) ?? [];
|
|
843
|
+
// Extract user memory facts from session memory
|
|
844
|
+
const result = await options.context.invoke(extractor, {
|
|
845
|
+
sessionFacts,
|
|
846
|
+
existingUserFacts,
|
|
847
|
+
});
|
|
848
|
+
// If no changes, nothing to do
|
|
849
|
+
if (!result.newFacts.length && !result.removeFacts?.length) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (this.afs && this.historyModulePath && this.userId) {
|
|
853
|
+
// Handle fact removal
|
|
854
|
+
if (result.removeFacts?.length && this.runtimeState.userMemory) {
|
|
855
|
+
const entriesToRemove = [];
|
|
856
|
+
for (const label of result.removeFacts) {
|
|
857
|
+
const entry = this.runtimeState.userMemory.find((e) => e.content?.label === label);
|
|
858
|
+
if (entry)
|
|
859
|
+
entriesToRemove.push(entry);
|
|
860
|
+
}
|
|
861
|
+
// Remove from AFS storage and runtime state
|
|
862
|
+
for (const entryToRemove of entriesToRemove) {
|
|
863
|
+
const memoryEntryPath = joinURL(this.historyModulePath, "by-user", this.userId, "@metadata/memory", entryToRemove.id);
|
|
864
|
+
await this.afs.delete(memoryEntryPath);
|
|
865
|
+
const index = this.runtimeState.userMemory.indexOf(entryToRemove);
|
|
866
|
+
if (index !== -1) {
|
|
867
|
+
this.runtimeState.userMemory.splice(index, 1);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
// Handle new/updated facts
|
|
872
|
+
// For user memory, labels are unique - replace existing facts with same label
|
|
873
|
+
if (result.newFacts.length) {
|
|
874
|
+
const userMemoryPath = joinURL(this.historyModulePath, "by-user", this.userId, "@metadata/memory/new");
|
|
875
|
+
for (const fact of result.newFacts) {
|
|
876
|
+
// Check if fact with same label already exists
|
|
877
|
+
const existingEntry = this.runtimeState.userMemory?.find((e) => e.content?.label === fact.label);
|
|
878
|
+
if (existingEntry) {
|
|
879
|
+
// Delete old entry
|
|
880
|
+
const oldEntryPath = joinURL(this.historyModulePath, "by-user", this.userId, "@metadata/memory", existingEntry.id);
|
|
881
|
+
await this.afs.delete(oldEntryPath);
|
|
882
|
+
// Remove from runtime state
|
|
883
|
+
if (this.runtimeState.userMemory) {
|
|
884
|
+
const index = this.runtimeState.userMemory.indexOf(existingEntry);
|
|
885
|
+
if (index !== -1) {
|
|
886
|
+
this.runtimeState.userMemory.splice(index, 1);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
// Create new entry
|
|
891
|
+
const newEntry = await this.afs.write(userMemoryPath, {
|
|
892
|
+
userId: this.userId,
|
|
893
|
+
agentId: this.agentId,
|
|
894
|
+
content: fact,
|
|
895
|
+
});
|
|
896
|
+
// Add to runtime state
|
|
897
|
+
this.runtimeState.userMemory ??= [];
|
|
898
|
+
this.runtimeState.userMemory.push(newEntry.data);
|
|
899
|
+
}
|
|
286
900
|
}
|
|
287
|
-
// Load history entries (after compact point if exists)
|
|
288
|
-
const afsEntries = (await this.afs.list(joinURL(this.historyModulePath, "by-session", this.sessionId), {
|
|
289
|
-
filter: {
|
|
290
|
-
userId: this.userId,
|
|
291
|
-
agentId: this.agentId,
|
|
292
|
-
// Only load entries after the latest compact
|
|
293
|
-
after: latestCompact?.createdAt?.toISOString(),
|
|
294
|
-
},
|
|
295
|
-
orderBy: [["createdAt", "desc"]],
|
|
296
|
-
// Set a very large limit to load all history entries
|
|
297
|
-
// The default limit is 10 which would cause history truncation
|
|
298
|
-
limit: 10000,
|
|
299
|
-
})).data;
|
|
300
|
-
this.runtimeState.historyEntries = afsEntries
|
|
301
|
-
.reverse()
|
|
302
|
-
.filter((entry) => isNonNullable(entry.content));
|
|
303
901
|
}
|
|
304
902
|
}
|
|
903
|
+
/**
|
|
904
|
+
* Find Agent Skill content from a single message
|
|
905
|
+
* @param msg - Message to search in
|
|
906
|
+
* @returns The skill content text if found, undefined otherwise
|
|
907
|
+
*/
|
|
908
|
+
findSkillContentInMessage(msg) {
|
|
909
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
910
|
+
const skillBlock = msg.content.find((block) => block.type === "text" && block.isAgentSkill === true);
|
|
911
|
+
if (skillBlock && skillBlock.type === "text") {
|
|
912
|
+
return skillBlock.text;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return undefined;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Find the last Agent Skill from a list of messages
|
|
919
|
+
* @param messages - Messages to search through
|
|
920
|
+
* @returns The last Agent Skill found, or undefined if none found
|
|
921
|
+
*/
|
|
922
|
+
findLastAgentSkillFromMessages(messages) {
|
|
923
|
+
// Search backwards through messages to find the last Agent Skill
|
|
924
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
925
|
+
const msg = messages[i];
|
|
926
|
+
if (!msg)
|
|
927
|
+
continue;
|
|
928
|
+
const skillContent = this.findSkillContentInMessage(msg);
|
|
929
|
+
if (skillContent) {
|
|
930
|
+
return {
|
|
931
|
+
content: skillContent,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return undefined;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Find the last Agent Skill from a list of history entries
|
|
939
|
+
* @param entries - History entries to search through
|
|
940
|
+
* @returns The last Agent Skill found, or undefined if none found
|
|
941
|
+
*/
|
|
942
|
+
findLastAgentSkill(entries) {
|
|
943
|
+
// Flatten all messages from entries
|
|
944
|
+
const allMessages = entries.flatMap((entry) => entry.content?.messages ?? []);
|
|
945
|
+
return this.findLastAgentSkillFromMessages(allMessages);
|
|
946
|
+
}
|
|
305
947
|
async initializeDefaultCompactor() {
|
|
306
948
|
this.compactConfig.compactor ??= await import("./compact/compactor.js").then((m) => new m.AISessionCompactor());
|
|
307
949
|
}
|
|
950
|
+
async initializeDefaultSessionMemoryExtractor() {
|
|
951
|
+
this.sessionMemoryConfig.extractor ??= await import("./compact/session-memory-extractor.js").then((m) => new m.AISessionMemoryExtractor());
|
|
952
|
+
}
|
|
953
|
+
async initializeDefaultUserMemoryExtractor() {
|
|
954
|
+
this.userMemoryConfig.extractor ??= await import("./compact/user-memory-extractor.js").then((m) => new m.AIUserMemoryExtractor());
|
|
955
|
+
}
|
|
956
|
+
get maxTokens() {
|
|
957
|
+
return this.compactConfig?.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
958
|
+
}
|
|
959
|
+
get keepRecentRatio() {
|
|
960
|
+
return this.compactConfig?.keepRecentRatio ?? DEFAULT_KEEP_RECENT_RATIO;
|
|
961
|
+
}
|
|
962
|
+
get keepRecentTokens() {
|
|
963
|
+
return Math.floor(this.maxTokens * this.keepRecentRatio);
|
|
964
|
+
}
|
|
965
|
+
get singleMessageLimit() {
|
|
966
|
+
return this.keepRecentTokens * 0.5;
|
|
967
|
+
}
|
|
308
968
|
}
|