@aigne/core 1.72.0-beta.2 → 1.72.0-beta.23

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.
Files changed (175) hide show
  1. package/CHANGELOG.md +261 -0
  2. package/lib/cjs/agents/agent.d.ts +42 -11
  3. package/lib/cjs/agents/agent.js +34 -8
  4. package/lib/cjs/agents/ai-agent.d.ts +63 -4
  5. package/lib/cjs/agents/ai-agent.js +154 -20
  6. package/lib/cjs/agents/chat-model.d.ts +157 -0
  7. package/lib/cjs/agents/chat-model.js +71 -6
  8. package/lib/cjs/agents/image-agent.d.ts +17 -1
  9. package/lib/cjs/agents/image-agent.js +16 -0
  10. package/lib/cjs/agents/image-model.d.ts +12 -2
  11. package/lib/cjs/agents/image-model.js +1 -1
  12. package/lib/cjs/agents/mcp-agent.d.ts +17 -0
  13. package/lib/cjs/agents/mcp-agent.js +18 -0
  14. package/lib/cjs/agents/model.d.ts +3 -3
  15. package/lib/cjs/agents/model.js +2 -2
  16. package/lib/cjs/agents/team-agent.d.ts +55 -0
  17. package/lib/cjs/agents/team-agent.js +31 -0
  18. package/lib/cjs/agents/transform-agent.d.ts +12 -0
  19. package/lib/cjs/agents/transform-agent.js +13 -0
  20. package/lib/cjs/agents/video-model.d.ts +10 -0
  21. package/lib/cjs/agents/video-model.js +1 -1
  22. package/lib/cjs/aigne/context.js +1 -3
  23. package/lib/cjs/aigne/usage.d.ts +4 -0
  24. package/lib/cjs/aigne/usage.js +6 -0
  25. package/lib/cjs/index.d.ts +1 -0
  26. package/lib/cjs/index.js +1 -0
  27. package/lib/cjs/loader/agent-yaml.d.ts +5 -63
  28. package/lib/cjs/loader/agent-yaml.js +4 -129
  29. package/lib/cjs/loader/agents.d.ts +4 -0
  30. package/lib/cjs/loader/agents.js +17 -0
  31. package/lib/cjs/loader/index.d.ts +16 -12
  32. package/lib/cjs/loader/index.js +20 -81
  33. package/lib/cjs/loader/schema.d.ts +21 -6
  34. package/lib/cjs/loader/schema.js +60 -1
  35. package/lib/cjs/memory/recorder.d.ts +4 -4
  36. package/lib/cjs/memory/retriever.d.ts +4 -4
  37. package/lib/cjs/prompt/agent-session.d.ts +163 -0
  38. package/lib/cjs/prompt/agent-session.js +1008 -0
  39. package/lib/cjs/prompt/compact/compactor.d.ts +7 -0
  40. package/lib/cjs/prompt/compact/compactor.js +52 -0
  41. package/lib/cjs/prompt/compact/session-memory-extractor.d.ts +7 -0
  42. package/lib/cjs/prompt/compact/session-memory-extractor.js +143 -0
  43. package/lib/cjs/prompt/compact/types.d.ts +336 -0
  44. package/lib/cjs/prompt/compact/types.js +53 -0
  45. package/lib/cjs/prompt/compact/user-memory-extractor.d.ts +7 -0
  46. package/lib/cjs/prompt/compact/user-memory-extractor.js +124 -0
  47. package/lib/cjs/prompt/context/afs/history.d.ts +5 -1
  48. package/lib/cjs/prompt/context/afs/history.js +3 -2
  49. package/lib/cjs/prompt/context/afs/index.js +8 -1
  50. package/lib/cjs/prompt/prompt-builder.d.ts +11 -9
  51. package/lib/cjs/prompt/prompt-builder.js +79 -120
  52. package/lib/cjs/prompt/skills/afs/agent-skill/agent-skill.d.ts +19 -0
  53. package/lib/cjs/prompt/skills/afs/agent-skill/agent-skill.js +69 -0
  54. package/lib/cjs/prompt/skills/afs/agent-skill/skill-loader.d.ts +12 -0
  55. package/lib/cjs/prompt/skills/afs/agent-skill/skill-loader.js +50 -0
  56. package/lib/cjs/prompt/skills/afs/delete.js +15 -3
  57. package/lib/cjs/prompt/skills/afs/edit.d.ts +6 -9
  58. package/lib/cjs/prompt/skills/afs/edit.js +85 -59
  59. package/lib/cjs/prompt/skills/afs/exec.js +17 -6
  60. package/lib/cjs/prompt/skills/afs/index.js +4 -1
  61. package/lib/cjs/prompt/skills/afs/list.d.ts +2 -0
  62. package/lib/cjs/prompt/skills/afs/list.js +35 -11
  63. package/lib/cjs/prompt/skills/afs/read.d.ts +9 -3
  64. package/lib/cjs/prompt/skills/afs/read.js +67 -15
  65. package/lib/cjs/prompt/skills/afs/rename.js +18 -4
  66. package/lib/cjs/prompt/skills/afs/search.js +21 -5
  67. package/lib/cjs/prompt/skills/afs/write.js +20 -6
  68. package/lib/cjs/prompt/template.d.ts +84 -9
  69. package/lib/cjs/prompt/template.js +46 -17
  70. package/lib/cjs/utils/mcp-utils.js +1 -1
  71. package/lib/cjs/utils/token-estimator.js +1 -1
  72. package/lib/dts/agents/agent.d.ts +42 -11
  73. package/lib/dts/agents/ai-agent.d.ts +63 -4
  74. package/lib/dts/agents/chat-model.d.ts +157 -0
  75. package/lib/dts/agents/image-agent.d.ts +17 -1
  76. package/lib/dts/agents/image-model.d.ts +12 -2
  77. package/lib/dts/agents/mcp-agent.d.ts +17 -0
  78. package/lib/dts/agents/model.d.ts +3 -3
  79. package/lib/dts/agents/team-agent.d.ts +55 -0
  80. package/lib/dts/agents/transform-agent.d.ts +12 -0
  81. package/lib/dts/agents/video-model.d.ts +10 -0
  82. package/lib/dts/aigne/context.d.ts +2 -2
  83. package/lib/dts/aigne/usage.d.ts +4 -0
  84. package/lib/dts/index.d.ts +1 -0
  85. package/lib/dts/loader/agent-yaml.d.ts +5 -63
  86. package/lib/dts/loader/agents.d.ts +4 -0
  87. package/lib/dts/loader/index.d.ts +16 -12
  88. package/lib/dts/loader/schema.d.ts +21 -6
  89. package/lib/dts/memory/recorder.d.ts +4 -4
  90. package/lib/dts/memory/retriever.d.ts +4 -4
  91. package/lib/dts/prompt/agent-session.d.ts +163 -0
  92. package/lib/dts/prompt/compact/compactor.d.ts +7 -0
  93. package/lib/dts/prompt/compact/session-memory-extractor.d.ts +7 -0
  94. package/lib/dts/prompt/compact/types.d.ts +336 -0
  95. package/lib/dts/prompt/compact/user-memory-extractor.d.ts +7 -0
  96. package/lib/dts/prompt/context/afs/history.d.ts +5 -1
  97. package/lib/dts/prompt/prompt-builder.d.ts +11 -9
  98. package/lib/dts/prompt/skills/afs/agent-skill/agent-skill.d.ts +19 -0
  99. package/lib/dts/prompt/skills/afs/agent-skill/skill-loader.d.ts +12 -0
  100. package/lib/dts/prompt/skills/afs/edit.d.ts +6 -9
  101. package/lib/dts/prompt/skills/afs/list.d.ts +2 -0
  102. package/lib/dts/prompt/skills/afs/read.d.ts +9 -3
  103. package/lib/dts/prompt/template.d.ts +84 -9
  104. package/lib/esm/agents/agent.d.ts +42 -11
  105. package/lib/esm/agents/agent.js +34 -8
  106. package/lib/esm/agents/ai-agent.d.ts +63 -4
  107. package/lib/esm/agents/ai-agent.js +154 -20
  108. package/lib/esm/agents/chat-model.d.ts +157 -0
  109. package/lib/esm/agents/chat-model.js +70 -5
  110. package/lib/esm/agents/image-agent.d.ts +17 -1
  111. package/lib/esm/agents/image-agent.js +16 -0
  112. package/lib/esm/agents/image-model.d.ts +12 -2
  113. package/lib/esm/agents/image-model.js +1 -1
  114. package/lib/esm/agents/mcp-agent.d.ts +17 -0
  115. package/lib/esm/agents/mcp-agent.js +18 -0
  116. package/lib/esm/agents/model.d.ts +3 -3
  117. package/lib/esm/agents/model.js +2 -2
  118. package/lib/esm/agents/team-agent.d.ts +55 -0
  119. package/lib/esm/agents/team-agent.js +31 -0
  120. package/lib/esm/agents/transform-agent.d.ts +12 -0
  121. package/lib/esm/agents/transform-agent.js +13 -0
  122. package/lib/esm/agents/video-model.d.ts +10 -0
  123. package/lib/esm/agents/video-model.js +1 -1
  124. package/lib/esm/aigne/context.d.ts +2 -2
  125. package/lib/esm/aigne/context.js +2 -4
  126. package/lib/esm/aigne/usage.d.ts +4 -0
  127. package/lib/esm/aigne/usage.js +6 -0
  128. package/lib/esm/index.d.ts +1 -0
  129. package/lib/esm/index.js +1 -0
  130. package/lib/esm/loader/agent-yaml.d.ts +5 -63
  131. package/lib/esm/loader/agent-yaml.js +4 -128
  132. package/lib/esm/loader/agents.d.ts +4 -0
  133. package/lib/esm/loader/agents.js +14 -0
  134. package/lib/esm/loader/index.d.ts +16 -12
  135. package/lib/esm/loader/index.js +21 -81
  136. package/lib/esm/loader/schema.d.ts +21 -6
  137. package/lib/esm/loader/schema.js +57 -0
  138. package/lib/esm/memory/recorder.d.ts +4 -4
  139. package/lib/esm/memory/retriever.d.ts +4 -4
  140. package/lib/esm/prompt/agent-session.d.ts +163 -0
  141. package/lib/esm/prompt/agent-session.js +968 -0
  142. package/lib/esm/prompt/compact/compactor.d.ts +7 -0
  143. package/lib/esm/prompt/compact/compactor.js +48 -0
  144. package/lib/esm/prompt/compact/session-memory-extractor.d.ts +7 -0
  145. package/lib/esm/prompt/compact/session-memory-extractor.js +139 -0
  146. package/lib/esm/prompt/compact/types.d.ts +336 -0
  147. package/lib/esm/prompt/compact/types.js +50 -0
  148. package/lib/esm/prompt/compact/user-memory-extractor.d.ts +7 -0
  149. package/lib/esm/prompt/compact/user-memory-extractor.js +120 -0
  150. package/lib/esm/prompt/context/afs/history.d.ts +5 -1
  151. package/lib/esm/prompt/context/afs/history.js +3 -2
  152. package/lib/esm/prompt/context/afs/index.js +8 -1
  153. package/lib/esm/prompt/prompt-builder.d.ts +11 -9
  154. package/lib/esm/prompt/prompt-builder.js +80 -121
  155. package/lib/esm/prompt/skills/afs/agent-skill/agent-skill.d.ts +19 -0
  156. package/lib/esm/prompt/skills/afs/agent-skill/agent-skill.js +65 -0
  157. package/lib/esm/prompt/skills/afs/agent-skill/skill-loader.d.ts +12 -0
  158. package/lib/esm/prompt/skills/afs/agent-skill/skill-loader.js +43 -0
  159. package/lib/esm/prompt/skills/afs/delete.js +15 -3
  160. package/lib/esm/prompt/skills/afs/edit.d.ts +6 -9
  161. package/lib/esm/prompt/skills/afs/edit.js +85 -59
  162. package/lib/esm/prompt/skills/afs/exec.js +17 -6
  163. package/lib/esm/prompt/skills/afs/index.js +4 -1
  164. package/lib/esm/prompt/skills/afs/list.d.ts +2 -0
  165. package/lib/esm/prompt/skills/afs/list.js +35 -11
  166. package/lib/esm/prompt/skills/afs/read.d.ts +9 -3
  167. package/lib/esm/prompt/skills/afs/read.js +67 -15
  168. package/lib/esm/prompt/skills/afs/rename.js +18 -4
  169. package/lib/esm/prompt/skills/afs/search.js +21 -5
  170. package/lib/esm/prompt/skills/afs/write.js +20 -6
  171. package/lib/esm/prompt/template.d.ts +84 -9
  172. package/lib/esm/prompt/template.js +46 -17
  173. package/lib/esm/utils/mcp-utils.js +1 -1
  174. package/lib/esm/utils/token-estimator.js +1 -1
  175. package/package.json +7 -6
@@ -0,0 +1,968 @@
1
+ import { AFSHistory } from "@aigne/afs-history";
2
+ import { v7 } from "@aigne/uuid";
3
+ import { joinURL } from "ufo";
4
+ import { stringify } from "yaml";
5
+ import { logger } from "../utils/logger.js";
6
+ import { estimateTokens } from "../utils/token-estimator.js";
7
+ import { isNonNullable } from "../utils/type-utils.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";
10
+ export class AgentSession {
11
+ sessionId;
12
+ userId;
13
+ agentId;
14
+ afs;
15
+ historyModulePath;
16
+ mode;
17
+ compactConfig;
18
+ sessionMemoryConfig;
19
+ userMemoryConfig;
20
+ runtimeState;
21
+ initialized;
22
+ compactionPromise;
23
+ sessionMemoryUpdatePromise;
24
+ userMemoryUpdatePromise;
25
+ constructor(options) {
26
+ this.sessionId = options.sessionId;
27
+ this.userId = options.userId;
28
+ this.agentId = options.agentId;
29
+ this.afs = options.afs;
30
+ this.mode = options.mode ?? DEFAULT_SESSION_MODE;
31
+ this.compactConfig = options.compact ?? {};
32
+ this.sessionMemoryConfig = options.sessionMemory ?? {};
33
+ this.userMemoryConfig = options.userMemory ?? {};
34
+ this.runtimeState = {
35
+ historyEntries: [],
36
+ currentEntry: null,
37
+ };
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
+ }
46
+ async setSystemMessages(...messages) {
47
+ await this.ensureInitialized();
48
+ this.runtimeState.systemMessages = messages;
49
+ }
50
+ async getMessages() {
51
+ await this.ensureInitialized();
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;
76
+ const messages = [
77
+ ...(systemMessages ?? []),
78
+ ...(userMemory && userMemory.length > 0 ? [this.formatUserMemory(userMemory)] : []),
79
+ ...(sessionMemory && sessionMemory.length > 0
80
+ ? [this.formatSessionMemory(sessionMemory)]
81
+ : []),
82
+ ...(historyCompact?.summary
83
+ ? [
84
+ {
85
+ role: "system",
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
+ ],
101
+ },
102
+ ]
103
+ : []),
104
+ ...historyMessages,
105
+ ...currentMessages,
106
+ ];
107
+ // Filter out thinking messages and truncate large messages
108
+ return messages
109
+ .map((msg) => {
110
+ if (!msg.content || typeof msg.content === "string") {
111
+ return msg;
112
+ }
113
+ // Filter out thinking from UnionContent[]
114
+ const filteredContent = msg.content.filter((c) => !(c.type === "text" && c.isThinking));
115
+ if (filteredContent.length === 0)
116
+ return null;
117
+ return { ...msg, content: filteredContent };
118
+ })
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
+ `;
184
+ }
185
+ async startMessage(input, message, options) {
186
+ await this.ensureInitialized();
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;
196
+ this.runtimeState.currentEntry = { input, messages: [message] };
197
+ }
198
+ async endMessage(output, message, options) {
199
+ await this.ensureInitialized();
200
+ if (!this.runtimeState.currentEntry?.input ||
201
+ !this.runtimeState.currentEntry.messages?.length) {
202
+ throw new Error("No current entry to end. Call startMessage() first.");
203
+ }
204
+ if (message)
205
+ this.runtimeState.currentEntry.messages.push(message);
206
+ this.runtimeState.currentEntry.output = output;
207
+ let newEntry;
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"), {
211
+ userId: this.userId,
212
+ sessionId: this.sessionId,
213
+ agentId: this.agentId,
214
+ content: this.runtimeState.currentEntry,
215
+ })).data;
216
+ }
217
+ else {
218
+ // Create in-memory entry for runtime state
219
+ const id = v7();
220
+ newEntry = {
221
+ id,
222
+ path: `/history/${id}`,
223
+ userId: this.userId,
224
+ sessionId: this.sessionId,
225
+ agentId: this.agentId,
226
+ content: this.runtimeState.currentEntry,
227
+ };
228
+ }
229
+ this.runtimeState.historyEntries.push(newEntry);
230
+ this.runtimeState.currentEntry = null;
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
+ }
241
+ }
242
+ /**
243
+ * Manually trigger compaction
244
+ */
245
+ async compact(options) {
246
+ await this.ensureInitialized();
247
+ // If compaction is already in progress, wait for it to complete
248
+ if (this.compactionPromise) {
249
+ return this.compactionPromise;
250
+ }
251
+ // Start new compaction task
252
+ this.compactionPromise = this.doCompact(options).finally(() => {
253
+ this.compactionPromise = undefined;
254
+ });
255
+ const isAsync = this.compactConfig.async ?? DEFAULT_COMPACT_ASYNC;
256
+ if (!isAsync)
257
+ await this.compactionPromise;
258
+ }
259
+ /**
260
+ * Internal method that performs the actual compaction
261
+ */
262
+ async doCompact(options) {
263
+ const { compactor } = this.compactConfig ?? {};
264
+ if (!compactor) {
265
+ throw new Error("Cannot compact without a compactor agent configured.");
266
+ }
267
+ const historyEntries = this.runtimeState.historyEntries;
268
+ if (historyEntries.length === 0)
269
+ return;
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;
283
+ // Find split point by iterating backwards from most recent entry
284
+ // The split point divides history into: [compact] | [keep]
285
+ let splitIndex = historyEntries.length; // Default: keep all (no compaction)
286
+ let accumulatedTokens = 0;
287
+ for (let i = historyEntries.length - 1; i >= 0; i--) {
288
+ const entry = historyEntries[i];
289
+ if (!entry)
290
+ continue;
291
+ const entryTokens = this.estimateMessagesTokens(entry.content?.messages ?? []);
292
+ // Check if adding this entry would exceed token budget
293
+ if (accumulatedTokens + entryTokens > keepRecentTokens) {
294
+ // Would exceed budget, split here (this entry and earlier ones will be compacted)
295
+ splitIndex = i + 1;
296
+ break;
297
+ }
298
+ // Can keep this entry, accumulate and continue
299
+ accumulatedTokens += entryTokens;
300
+ splitIndex = i;
301
+ }
302
+ // Split history at the found point
303
+ const entriesToCompact = historyEntries.slice(0, splitIndex);
304
+ const entriesToKeep = historyEntries.slice(splitIndex);
305
+ // If nothing to compact, return
306
+ if (entriesToCompact.length === 0) {
307
+ return;
308
+ }
309
+ const latestCompactedEntry = entriesToCompact.at(-1);
310
+ if (!latestCompactedEntry)
311
+ return;
312
+ // Split into batches to avoid context overflow
313
+ const batches = this.splitIntoBatches(entriesToCompact, maxTokens);
314
+ // Process batches incrementally, each summary becomes input for the next
315
+ let currentSummary = this.runtimeState.historyCompact?.summary;
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));
321
+ const result = await options.context.invoke(compactor, {
322
+ previousSummary: [currentSummary].filter(isNonNullable),
323
+ messages,
324
+ });
325
+ currentSummary = result.summary;
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
+ };
338
+ // Write compact entry to AFS
339
+ if (this.afs && this.historyModulePath) {
340
+ await this.afs.write(joinURL(this.historyModulePath, "by-session", this.sessionId, "@metadata/compact/new"), {
341
+ userId: this.userId,
342
+ agentId: this.agentId,
343
+ content: historyCompact,
344
+ metadata: {
345
+ latestEntryId: latestCompactedEntry.id,
346
+ },
347
+ });
348
+ }
349
+ // Update runtime state: keep the summary and recent entries
350
+ this.runtimeState.historyCompact = historyCompact;
351
+ this.runtimeState.historyEntries = entriesToKeep;
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
+ }
433
+ async maybeAutoCompact(options) {
434
+ if (this.compactionPromise)
435
+ await this.compactionPromise;
436
+ const mode = this.compactConfig.mode ?? DEFAULT_COMPACT_MODE;
437
+ if (mode === "disabled")
438
+ return;
439
+ const { compactor } = this.compactConfig;
440
+ if (!compactor)
441
+ return;
442
+ const maxTokens = this.maxTokens;
443
+ const messages = await this.getMessages();
444
+ const currentTokens = this.estimateMessagesTokens(messages);
445
+ if (currentTokens >= maxTokens) {
446
+ await this.compact(options);
447
+ }
448
+ }
449
+ /**
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
453
+ */
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;
488
+ }
489
+ /**
490
+ * Split entries into batches based on token limit
491
+ * Each batch will not exceed the specified maxTokens
492
+ */
493
+ splitIntoBatches(entries, maxTokens) {
494
+ const batches = [];
495
+ let currentBatch = [];
496
+ let currentTokens = 0;
497
+ for (const entry of entries) {
498
+ const entryTokens = this.estimateMessagesTokens(entry.content?.messages ?? []);
499
+ // If adding this entry exceeds limit and we have entries in current batch, start new batch
500
+ if (currentTokens + entryTokens > maxTokens && currentBatch.length > 0) {
501
+ batches.push(currentBatch);
502
+ currentBatch = [entry];
503
+ currentTokens = entryTokens;
504
+ }
505
+ else {
506
+ currentBatch.push(entry);
507
+ currentTokens += entryTokens;
508
+ }
509
+ }
510
+ // Add remaining entries
511
+ if (currentBatch.length > 0) {
512
+ batches.push(currentBatch);
513
+ }
514
+ return batches;
515
+ }
516
+ async appendCurrentMessages(messages, options) {
517
+ await this.ensureInitialized();
518
+ if (!this.runtimeState.currentEntry || !this.runtimeState.currentEntry.messages?.length) {
519
+ throw new Error("No current entry to append messages. Call startMessage() first.");
520
+ }
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;
573
+ }
574
+ async ensureInitialized() {
575
+ this.initialized ??= this.initialize();
576
+ await this.initialized;
577
+ }
578
+ async initialize() {
579
+ if (this.initialized)
580
+ return;
581
+ await this.initializeDefaultCompactor();
582
+ await this.initializeDefaultSessionMemoryExtractor();
583
+ await this.initializeDefaultUserMemoryExtractor();
584
+ const historyModule = (await this.afs?.listModules())?.find((m) => m.module instanceof AFSHistory);
585
+ this.historyModulePath = historyModule?.path;
586
+ if (this.afs && this.historyModulePath) {
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);
694
+ });
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
+ }
900
+ }
901
+ }
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
+ }
947
+ async initializeDefaultCompactor() {
948
+ this.compactConfig.compactor ??= await import("./compact/compactor.js").then((m) => new m.AISessionCompactor());
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
+ }
968
+ }