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