@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +349 -0
  2. package/lib/cjs/agents/agent.d.ts +5 -0
  3. package/lib/cjs/agents/agent.js +5 -0
  4. package/lib/cjs/agents/ai-agent.d.ts +41 -5
  5. package/lib/cjs/agents/ai-agent.js +89 -29
  6. package/lib/cjs/agents/chat-model.d.ts +3 -0
  7. package/lib/cjs/agents/chat-model.js +18 -1
  8. package/lib/cjs/agents/image-model.js +1 -1
  9. package/lib/cjs/agents/model.d.ts +3 -3
  10. package/lib/cjs/agents/model.js +2 -2
  11. package/lib/cjs/agents/video-model.js +1 -1
  12. package/lib/cjs/aigne/context.js +1 -3
  13. package/lib/cjs/prompt/agent-session.d.ts +115 -5
  14. package/lib/cjs/prompt/agent-session.js +752 -85
  15. package/lib/cjs/prompt/compact/compactor.js +4 -0
  16. package/lib/cjs/prompt/compact/session-memory-extractor.d.ts +7 -0
  17. package/lib/cjs/prompt/compact/session-memory-extractor.js +143 -0
  18. package/lib/cjs/prompt/compact/types.d.ts +257 -0
  19. package/lib/cjs/prompt/compact/types.js +35 -1
  20. package/lib/cjs/prompt/compact/user-memory-extractor.d.ts +7 -0
  21. package/lib/cjs/prompt/compact/user-memory-extractor.js +124 -0
  22. package/lib/cjs/prompt/prompt-builder.js +3 -3
  23. package/lib/cjs/prompt/skills/afs/agent-skill/agent-skill.d.ts +2 -0
  24. package/lib/cjs/prompt/skills/afs/agent-skill/agent-skill.js +6 -0
  25. package/lib/cjs/prompt/skills/afs/agent-skill/skill-loader.d.ts +1 -2
  26. package/lib/cjs/prompt/skills/afs/agent-skill/skill-loader.js +15 -26
  27. package/lib/cjs/prompt/skills/afs/list.d.ts +2 -0
  28. package/lib/cjs/prompt/skills/afs/list.js +9 -1
  29. package/lib/cjs/prompt/skills/afs/read.d.ts +3 -1
  30. package/lib/cjs/prompt/skills/afs/read.js +5 -0
  31. package/lib/cjs/utils/mcp-utils.js +1 -1
  32. package/lib/cjs/utils/token-estimator.js +1 -1
  33. package/lib/cjs/utils/type-utils.js +0 -1
  34. package/lib/dts/agents/agent.d.ts +5 -0
  35. package/lib/dts/agents/ai-agent.d.ts +41 -5
  36. package/lib/dts/agents/chat-model.d.ts +3 -0
  37. package/lib/dts/agents/model.d.ts +3 -3
  38. package/lib/dts/prompt/agent-session.d.ts +115 -5
  39. package/lib/dts/prompt/compact/session-memory-extractor.d.ts +7 -0
  40. package/lib/dts/prompt/compact/types.d.ts +257 -0
  41. package/lib/dts/prompt/compact/user-memory-extractor.d.ts +7 -0
  42. package/lib/dts/prompt/skills/afs/agent-skill/agent-skill.d.ts +2 -0
  43. package/lib/dts/prompt/skills/afs/agent-skill/skill-loader.d.ts +1 -2
  44. package/lib/dts/prompt/skills/afs/list.d.ts +2 -0
  45. package/lib/dts/prompt/skills/afs/read.d.ts +3 -1
  46. package/lib/esm/agents/agent.d.ts +5 -0
  47. package/lib/esm/agents/agent.js +5 -0
  48. package/lib/esm/agents/ai-agent.d.ts +41 -5
  49. package/lib/esm/agents/ai-agent.js +89 -29
  50. package/lib/esm/agents/chat-model.d.ts +3 -0
  51. package/lib/esm/agents/chat-model.js +18 -1
  52. package/lib/esm/agents/image-model.js +1 -1
  53. package/lib/esm/agents/model.d.ts +3 -3
  54. package/lib/esm/agents/model.js +2 -2
  55. package/lib/esm/agents/video-model.js +1 -1
  56. package/lib/esm/aigne/context.js +2 -4
  57. package/lib/esm/prompt/agent-session.d.ts +115 -5
  58. package/lib/esm/prompt/agent-session.js +750 -86
  59. package/lib/esm/prompt/compact/compactor.js +4 -0
  60. package/lib/esm/prompt/compact/session-memory-extractor.d.ts +7 -0
  61. package/lib/esm/prompt/compact/session-memory-extractor.js +139 -0
  62. package/lib/esm/prompt/compact/types.d.ts +257 -0
  63. package/lib/esm/prompt/compact/types.js +34 -0
  64. package/lib/esm/prompt/compact/user-memory-extractor.d.ts +7 -0
  65. package/lib/esm/prompt/compact/user-memory-extractor.js +120 -0
  66. package/lib/esm/prompt/prompt-builder.js +3 -3
  67. package/lib/esm/prompt/skills/afs/agent-skill/agent-skill.d.ts +2 -0
  68. package/lib/esm/prompt/skills/afs/agent-skill/agent-skill.js +6 -0
  69. package/lib/esm/prompt/skills/afs/agent-skill/skill-loader.d.ts +1 -2
  70. package/lib/esm/prompt/skills/afs/agent-skill/skill-loader.js +14 -24
  71. package/lib/esm/prompt/skills/afs/list.d.ts +2 -0
  72. package/lib/esm/prompt/skills/afs/list.js +9 -1
  73. package/lib/esm/prompt/skills/afs/read.d.ts +3 -1
  74. package/lib/esm/prompt/skills/afs/read.js +5 -0
  75. package/lib/esm/utils/mcp-utils.js +1 -1
  76. package/lib/esm/utils/token-estimator.js +1 -1
  77. package/lib/esm/utils/type-utils.js +0 -1
  78. package/package.json +6 -6
@@ -1,79 +1,213 @@
1
1
  import { AFSHistory } from "@aigne/afs-history";
2
2
  import { v7 } from "@aigne/uuid";
3
3
  import { joinURL } from "ufo";
4
+ import { stringify } from "yaml";
5
+ import { logger } from "../utils/logger.js";
4
6
  import { estimateTokens } from "../utils/token-estimator.js";
5
7
  import { isNonNullable } from "../utils/type-utils.js";
6
- import { DEFAULT_COMPACT_ASYNC, DEFAULT_COMPACT_MODE, DEFAULT_KEEP_RECENT_RATIO, DEFAULT_MAX_TOKENS, } from "./compact/types.js";
8
+ import { DEFAULT_COMPACT_ASYNC, DEFAULT_COMPACT_MODE, DEFAULT_KEEP_RECENT_RATIO, DEFAULT_MAX_TOKENS, DEFAULT_MEMORY_QUERY_LIMIT, DEFAULT_MEMORY_RATIO, DEFAULT_SESSION_MEMORY_ASYNC, DEFAULT_SESSION_MEMORY_MODE, DEFAULT_SESSION_MODE, DEFAULT_USER_MEMORY_ASYNC, DEFAULT_USER_MEMORY_MODE, } from "./compact/types.js";
9
+ export * from "./compact/types.js";
7
10
  export class AgentSession {
8
11
  sessionId;
9
12
  userId;
10
13
  agentId;
11
14
  afs;
12
15
  historyModulePath;
16
+ mode;
13
17
  compactConfig;
18
+ sessionMemoryConfig;
19
+ userMemoryConfig;
14
20
  runtimeState;
15
21
  initialized;
16
22
  compactionPromise;
23
+ sessionMemoryUpdatePromise;
24
+ userMemoryUpdatePromise;
17
25
  constructor(options) {
18
26
  this.sessionId = options.sessionId;
19
27
  this.userId = options.userId;
20
28
  this.agentId = options.agentId;
21
29
  this.afs = options.afs;
30
+ this.mode = options.mode ?? DEFAULT_SESSION_MODE;
22
31
  this.compactConfig = options.compact ?? {};
32
+ this.sessionMemoryConfig = options.sessionMemory ?? {};
33
+ this.userMemoryConfig = options.userMemory ?? {};
23
34
  this.runtimeState = {
24
35
  historyEntries: [],
25
36
  currentEntry: null,
26
37
  };
27
38
  }
39
+ /**
40
+ * Check if memory extraction is enabled
41
+ * Memory extraction requires mode to be "auto" AND AFS history module to be available
42
+ */
43
+ get isMemoryEnabled() {
44
+ return this.mode === "auto" && !!this.afs && !!this.historyModulePath;
45
+ }
28
46
  async setSystemMessages(...messages) {
29
47
  await this.ensureInitialized();
30
48
  this.runtimeState.systemMessages = messages;
31
49
  }
32
50
  async getMessages() {
33
51
  await this.ensureInitialized();
34
- const { systemMessages, compactSummary, historyEntries, currentEntry } = this.runtimeState;
52
+ const { systemMessages, userMemory, sessionMemory, historyCompact, historyEntries, currentEntry, currentEntryCompact, } = this.runtimeState;
53
+ let currentMessages = [];
54
+ if (currentEntry?.messages?.length) {
55
+ if (currentEntryCompact) {
56
+ const { compressedCount, summary } = currentEntryCompact;
57
+ const summaryMessage = {
58
+ role: "user",
59
+ content: `[Earlier messages in this conversation (${compressedCount} messages compressed)]\n${summary}`,
60
+ };
61
+ const remainingMessages = currentEntry.messages.slice(compressedCount);
62
+ currentMessages = [summaryMessage, ...remainingMessages];
63
+ }
64
+ else {
65
+ currentMessages = currentEntry.messages;
66
+ }
67
+ }
68
+ // Flatten history entries messages once
69
+ const historyMessages = historyEntries.flatMap((entry) => entry.content?.messages ?? []);
70
+ // Check if there's an Agent Skill in current uncompressed messages
71
+ const hasSkillInCurrentMessages = [...historyMessages, ...currentMessages].some((msg) => msg.role === "user" &&
72
+ Array.isArray(msg.content) &&
73
+ msg.content.some((block) => block.type === "text" && block.isAgentSkill === true));
74
+ // Prefer currentEntryCompact's lastAgentSkill over historyCompact's (newer takes priority)
75
+ const lastAgentSkillToInject = currentEntryCompact?.lastAgentSkill ?? historyCompact?.lastAgentSkill;
35
76
  const messages = [
36
77
  ...(systemMessages ?? []),
37
- ...(compactSummary
78
+ ...(userMemory && userMemory.length > 0 ? [this.formatUserMemory(userMemory)] : []),
79
+ ...(sessionMemory && sessionMemory.length > 0
80
+ ? [this.formatSessionMemory(sessionMemory)]
81
+ : []),
82
+ ...(historyCompact?.summary
38
83
  ? [
39
84
  {
40
85
  role: "system",
41
- content: `Previous conversation summary:\n${compactSummary}`,
86
+ content: `Previous conversation summary:\n${historyCompact.summary}`,
87
+ },
88
+ ]
89
+ : []),
90
+ // Only inject lastAgentSkill if there's no skill in current messages
91
+ ...(lastAgentSkillToInject && !hasSkillInCurrentMessages
92
+ ? [
93
+ {
94
+ role: "user",
95
+ content: [
96
+ {
97
+ type: "text",
98
+ text: lastAgentSkillToInject.content,
99
+ },
100
+ ],
42
101
  },
43
102
  ]
44
103
  : []),
45
- ...historyEntries.flatMap((entry) => entry.content?.messages ?? []),
46
- ...(currentEntry?.messages ?? []),
104
+ ...historyMessages,
105
+ ...currentMessages,
47
106
  ];
48
- // Filter out thinking messages from content
49
- return messages.map((msg) => {
107
+ // Filter out thinking messages and truncate large messages
108
+ return messages
109
+ .map((msg) => {
50
110
  if (!msg.content || typeof msg.content === "string") {
51
111
  return msg;
52
112
  }
53
113
  // Filter out thinking from UnionContent[]
54
114
  const filteredContent = msg.content.filter((c) => !(c.type === "text" && c.isThinking));
55
- return { ...msg, content: filteredContent.length > 0 ? filteredContent : undefined };
56
- });
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
+ `;
57
184
  }
58
185
  async startMessage(input, message, options) {
59
186
  await this.ensureInitialized();
60
- await this.maybeAutoCompact(options);
61
- // Always wait for compaction to complete before starting a new message
62
- // This ensures data consistency even in async compact mode
63
- if (this.compactionPromise)
64
- await this.compactionPromise;
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;
65
196
  this.runtimeState.currentEntry = { input, messages: [message] };
66
197
  }
67
- async endMessage(output, options) {
198
+ async endMessage(output, message, options) {
68
199
  await this.ensureInitialized();
69
200
  if (!this.runtimeState.currentEntry?.input ||
70
201
  !this.runtimeState.currentEntry.messages?.length) {
71
202
  throw new Error("No current entry to end. Call startMessage() first.");
72
203
  }
204
+ if (message)
205
+ this.runtimeState.currentEntry.messages.push(message);
73
206
  this.runtimeState.currentEntry.output = output;
74
207
  let newEntry;
75
- if (this.afs && this.historyModulePath) {
76
- newEntry = (await this.afs.write(joinURL(this.historyModulePath, "new"), {
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"), {
77
211
  userId: this.userId,
78
212
  sessionId: this.sessionId,
79
213
  agentId: this.agentId,
@@ -81,6 +215,7 @@ export class AgentSession {
81
215
  })).data;
82
216
  }
83
217
  else {
218
+ // Create in-memory entry for runtime state
84
219
  const id = v7();
85
220
  newEntry = {
86
221
  id,
@@ -93,8 +228,16 @@ export class AgentSession {
93
228
  }
94
229
  this.runtimeState.historyEntries.push(newEntry);
95
230
  this.runtimeState.currentEntry = null;
96
- // Check if auto-compact should be triggered
97
- await this.maybeAutoCompact(options);
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
+ }
98
241
  }
99
242
  /**
100
243
  * Manually trigger compaction
@@ -109,36 +252,34 @@ export class AgentSession {
109
252
  this.compactionPromise = this.doCompact(options).finally(() => {
110
253
  this.compactionPromise = undefined;
111
254
  });
112
- return this.compactionPromise;
255
+ const isAsync = this.compactConfig.async ?? DEFAULT_COMPACT_ASYNC;
256
+ if (!isAsync)
257
+ await this.compactionPromise;
113
258
  }
114
259
  /**
115
260
  * Internal method that performs the actual compaction
116
261
  */
117
262
  async doCompact(options) {
118
- const { compactor, keepRecentRatio } = this.compactConfig ?? {};
263
+ const { compactor } = this.compactConfig ?? {};
119
264
  if (!compactor) {
120
265
  throw new Error("Cannot compact without a compactor agent configured.");
121
266
  }
122
267
  const historyEntries = this.runtimeState.historyEntries;
123
268
  if (historyEntries.length === 0)
124
269
  return;
125
- // Calculate token budget for keeping recent messages
126
- const ratio = keepRecentRatio ?? DEFAULT_KEEP_RECENT_RATIO;
127
- const maxTokens = this.compactConfig?.maxTokens ?? DEFAULT_MAX_TOKENS;
128
- let keepTokenBudget = Math.floor(maxTokens * ratio);
129
- // Calculate tokens for system messages
130
- const systemTokens = (this.runtimeState.systemMessages ?? []).reduce((sum, msg) => {
131
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
132
- return sum + estimateTokens(content);
133
- }, 0);
134
- // Calculate tokens for current entry messages
135
- const currentTokens = (this.runtimeState.currentEntry?.messages ?? []).reduce((sum, msg) => {
136
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
137
- return sum + estimateTokens(content);
138
- }, 0);
139
- // Subtract system and current tokens from budget
140
- // This ensures total tokens (system + current + kept history) stays within ratio budget
141
- keepTokenBudget = Math.max(0, keepTokenBudget - systemTokens - currentTokens);
270
+ const maxTokens = this.maxTokens;
271
+ // Target to keep only 50% of keepRecentTokens to leave buffer room
272
+ // This avoids triggering compression again shortly after compaction
273
+ // Similar to compactCurrentEntry, we compress more aggressively to leave headroom
274
+ //
275
+ // Note: We don't subtract systemTokens or currentEntry tokens because:
276
+ // 1. keepRecentTokens is already a relative ratio (e.g., 50% of maxTokens)
277
+ // 2. systemTokens overhead is typically small (~1-2k, ~1-2% of maxTokens)
278
+ // 3. currentEntry is still being constructed (not yet added to history)
279
+ // 4. In tool use scenarios, currentEntry can be very large (many tool calls)
280
+ // 5. Subtracting them would complicate logic without significant benefit
281
+ // 6. Total token limit is enforced by maybeAutoCompact trigger condition
282
+ const keepRecentTokens = this.keepRecentTokens * 0.5;
142
283
  // Find split point by iterating backwards from most recent entry
143
284
  // The split point divides history into: [compact] | [keep]
144
285
  let splitIndex = historyEntries.length; // Default: keep all (no compaction)
@@ -149,7 +290,7 @@ export class AgentSession {
149
290
  continue;
150
291
  const entryTokens = this.estimateMessagesTokens(entry.content?.messages ?? []);
151
292
  // Check if adding this entry would exceed token budget
152
- if (accumulatedTokens + entryTokens > keepTokenBudget) {
293
+ if (accumulatedTokens + entryTokens > keepRecentTokens) {
153
294
  // Would exceed budget, split here (this entry and earlier ones will be compacted)
154
295
  splitIndex = i + 1;
155
296
  break;
@@ -171,58 +312,179 @@ export class AgentSession {
171
312
  // Split into batches to avoid context overflow
172
313
  const batches = this.splitIntoBatches(entriesToCompact, maxTokens);
173
314
  // Process batches incrementally, each summary becomes input for the next
174
- let currentSummary = this.runtimeState.compactSummary;
315
+ let currentSummary = this.runtimeState.historyCompact?.summary;
175
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));
176
321
  const result = await options.context.invoke(compactor, {
177
322
  previousSummary: [currentSummary].filter(isNonNullable),
178
- messages: batch.flatMap((e) => e.content?.messages ?? []).filter(isNonNullable),
323
+ messages,
179
324
  });
180
325
  currentSummary = result.summary;
181
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
+ };
182
338
  // Write compact entry to AFS
183
339
  if (this.afs && this.historyModulePath) {
184
340
  await this.afs.write(joinURL(this.historyModulePath, "by-session", this.sessionId, "@metadata/compact/new"), {
185
341
  userId: this.userId,
186
342
  agentId: this.agentId,
187
- content: { summary: currentSummary },
343
+ content: historyCompact,
188
344
  metadata: {
189
345
  latestEntryId: latestCompactedEntry.id,
190
346
  },
191
347
  });
192
348
  }
193
349
  // Update runtime state: keep the summary and recent entries
194
- this.runtimeState.compactSummary = currentSummary;
350
+ this.runtimeState.historyCompact = historyCompact;
195
351
  this.runtimeState.historyEntries = entriesToKeep;
196
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
+ }
197
433
  async maybeAutoCompact(options) {
198
434
  if (this.compactionPromise)
199
435
  await this.compactionPromise;
200
- if (!this.compactConfig)
201
- return;
202
- // Check if compaction is disabled
203
436
  const mode = this.compactConfig.mode ?? DEFAULT_COMPACT_MODE;
204
437
  if (mode === "disabled")
205
438
  return;
206
439
  const { compactor } = this.compactConfig;
207
- const maxTokens = this.compactConfig.maxTokens ?? DEFAULT_MAX_TOKENS;
208
440
  if (!compactor)
209
441
  return;
210
- const currentTokens = this.estimateMessagesTokens(await this.getMessages());
442
+ const maxTokens = this.maxTokens;
443
+ const messages = await this.getMessages();
444
+ const currentTokens = this.estimateMessagesTokens(messages);
211
445
  if (currentTokens >= maxTokens) {
212
- this.compact(options);
213
- const isAsync = this.compactConfig.async ?? DEFAULT_COMPACT_ASYNC;
214
- if (!isAsync)
215
- await this.compactionPromise;
446
+ await this.compact(options);
216
447
  }
217
448
  }
218
449
  /**
219
- * Estimate token count for an array of messages
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
220
453
  */
221
- estimateMessagesTokens(messages) {
222
- return messages.reduce((sum, msg) => {
223
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
224
- return sum + estimateTokens(content);
225
- }, 0);
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;
226
488
  }
227
489
  /**
228
490
  * Split entries into batches based on token limit
@@ -251,12 +513,63 @@ export class AgentSession {
251
513
  }
252
514
  return batches;
253
515
  }
254
- async appendCurrentMessages(...messages) {
516
+ async appendCurrentMessages(messages, options) {
255
517
  await this.ensureInitialized();
256
518
  if (!this.runtimeState.currentEntry || !this.runtimeState.currentEntry.messages?.length) {
257
519
  throw new Error("No current entry to append messages. Call startMessage() first.");
258
520
  }
259
- this.runtimeState.currentEntry.messages.push(...messages);
521
+ this.runtimeState.currentEntry.messages.push(...[messages].flat());
522
+ await this.maybeCompactCurrentEntry(options);
523
+ }
524
+ /**
525
+ * Truncate text content to fit within target token limit
526
+ * @param text The text to truncate
527
+ * @param currentTokens Current token count of the text
528
+ * @param targetTokens Target token count after truncation
529
+ * @returns Truncated text
530
+ */
531
+ truncateText(text, currentTokens, targetTokens) {
532
+ if (currentTokens <= targetTokens)
533
+ return text;
534
+ const keepRatio = (targetTokens / currentTokens) * 0.9;
535
+ const keepLength = Math.floor(text.length * keepRatio);
536
+ const headLength = Math.floor(keepLength * 0.7);
537
+ const tailLength = Math.floor(keepLength * 0.3);
538
+ return (text.slice(0, headLength) +
539
+ `\n\n[... truncated ${currentTokens - targetTokens} tokens ...]\n\n` +
540
+ text.slice(-tailLength));
541
+ }
542
+ truncateLargeMessage(msg) {
543
+ const singleMessageLimit = this.singleMessageLimit;
544
+ // Handle string content
545
+ if (typeof msg.content === "string") {
546
+ const tokens = estimateTokens(msg.content);
547
+ if (tokens <= singleMessageLimit)
548
+ return msg;
549
+ const truncated = this.truncateText(msg.content, tokens, singleMessageLimit);
550
+ return { ...msg, content: truncated };
551
+ }
552
+ // Handle array content (UnionContent[])
553
+ if (Array.isArray(msg.content)) {
554
+ // Truncate each text block individually if it exceeds the limit
555
+ const truncatedContent = msg.content.map((block) => {
556
+ // Keep non-text blocks unchanged
557
+ if (block.type !== "text" || typeof block.text !== "string") {
558
+ return block;
559
+ }
560
+ // Check if this text block needs truncation
561
+ const blockTokens = estimateTokens(block.text);
562
+ if (blockTokens <= singleMessageLimit) {
563
+ return block;
564
+ }
565
+ // Truncate this text block independently
566
+ const truncatedText = this.truncateText(block.text, blockTokens, singleMessageLimit);
567
+ return { ...block, text: truncatedText };
568
+ });
569
+ return { ...msg, content: truncatedContent };
570
+ }
571
+ // Unknown content type, return as-is
572
+ return msg;
260
573
  }
261
574
  async ensureInitialized() {
262
575
  this.initialized ??= this.initialize();
@@ -266,39 +579,390 @@ export class AgentSession {
266
579
  if (this.initialized)
267
580
  return;
268
581
  await this.initializeDefaultCompactor();
582
+ await this.initializeDefaultSessionMemoryExtractor();
583
+ await this.initializeDefaultUserMemoryExtractor();
269
584
  const historyModule = (await this.afs?.listModules())?.find((m) => m.module instanceof AFSHistory);
270
585
  this.historyModulePath = historyModule?.path;
271
586
  if (this.afs && this.historyModulePath) {
272
- // Load latest compact entry if exists
273
- const compactPath = joinURL(this.historyModulePath, "by-session", this.sessionId, "@metadata/compact");
274
- const compactResult = await this.afs.list(compactPath, {
275
- filter: { userId: this.userId, agentId: this.agentId },
276
- orderBy: [["createdAt", "desc"]],
277
- limit: 1,
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);
278
694
  });
279
- const latestCompact = compactResult.data[0];
280
- if (latestCompact?.content?.summary) {
281
- this.runtimeState.compactSummary = latestCompact.content.summary;
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
+ }
282
900
  }
283
- // Load history entries (after compact point if exists)
284
- const afsEntries = (await this.afs.list(joinURL(this.historyModulePath, "by-session", this.sessionId), {
285
- filter: {
286
- userId: this.userId,
287
- agentId: this.agentId,
288
- // Only load entries after the latest compact
289
- after: latestCompact?.createdAt?.toISOString(),
290
- },
291
- orderBy: [["createdAt", "desc"]],
292
- // Set a very large limit to load all history entries
293
- // The default limit is 10 which would cause history truncation
294
- limit: 10000,
295
- })).data;
296
- this.runtimeState.historyEntries = afsEntries
297
- .reverse()
298
- .filter((entry) => isNonNullable(entry.content));
299
901
  }
300
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
+ }
301
947
  async initializeDefaultCompactor() {
302
948
  this.compactConfig.compactor ??= await import("./compact/compactor.js").then((m) => new m.AISessionCompactor());
303
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
+ }
304
968
  }