@aigne/core 1.72.0-beta.9 → 1.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +342 -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 +746 -83
  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 +14 -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 +744 -84
  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 +13 -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,51 +1,110 @@
1
1
  import { AFSHistory } from "@aigne/afs-history";
2
2
  import { v7 } from "@aigne/uuid";
3
3
  import { joinURL } from "ufo";
4
+ import { stringify } from "yaml";
5
+ import { logger } from "../utils/logger.js";
4
6
  import { estimateTokens } from "../utils/token-estimator.js";
5
7
  import { isNonNullable } from "../utils/type-utils.js";
6
- import { DEFAULT_COMPACT_ASYNC, DEFAULT_COMPACT_MODE, DEFAULT_KEEP_RECENT_RATIO, DEFAULT_MAX_TOKENS, } from "./compact/types.js";
8
+ import { DEFAULT_COMPACT_ASYNC, DEFAULT_COMPACT_MODE, DEFAULT_KEEP_RECENT_RATIO, DEFAULT_MAX_TOKENS, DEFAULT_MEMORY_QUERY_LIMIT, DEFAULT_MEMORY_RATIO, DEFAULT_SESSION_MEMORY_ASYNC, DEFAULT_SESSION_MEMORY_MODE, DEFAULT_SESSION_MODE, DEFAULT_USER_MEMORY_ASYNC, DEFAULT_USER_MEMORY_MODE, } from "./compact/types.js";
9
+ export * from "./compact/types.js";
7
10
  export class AgentSession {
8
11
  sessionId;
9
12
  userId;
10
13
  agentId;
11
14
  afs;
12
15
  historyModulePath;
16
+ mode;
13
17
  compactConfig;
18
+ sessionMemoryConfig;
19
+ userMemoryConfig;
14
20
  runtimeState;
15
21
  initialized;
16
22
  compactionPromise;
23
+ sessionMemoryUpdatePromise;
24
+ userMemoryUpdatePromise;
17
25
  constructor(options) {
18
26
  this.sessionId = options.sessionId;
19
27
  this.userId = options.userId;
20
28
  this.agentId = options.agentId;
21
29
  this.afs = options.afs;
30
+ this.mode = options.mode ?? DEFAULT_SESSION_MODE;
22
31
  this.compactConfig = options.compact ?? {};
32
+ this.sessionMemoryConfig = options.sessionMemory ?? {};
33
+ this.userMemoryConfig = options.userMemory ?? {};
23
34
  this.runtimeState = {
24
35
  historyEntries: [],
25
36
  currentEntry: null,
26
37
  };
27
38
  }
39
+ /**
40
+ * Check if memory extraction is enabled
41
+ * Memory extraction requires mode to be "auto" AND AFS history module to be available
42
+ */
43
+ get isMemoryEnabled() {
44
+ return this.mode === "auto" && !!this.afs && !!this.historyModulePath;
45
+ }
28
46
  async setSystemMessages(...messages) {
29
47
  await this.ensureInitialized();
30
48
  this.runtimeState.systemMessages = messages;
31
49
  }
32
50
  async getMessages() {
33
51
  await this.ensureInitialized();
34
- const { systemMessages, 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
107
+ // Filter out thinking messages and truncate large messages
49
108
  return messages
50
109
  .map((msg) => {
51
110
  if (!msg.content || typeof msg.content === "string") {
@@ -57,27 +116,98 @@ export class AgentSession {
57
116
  return null;
58
117
  return { ...msg, content: filteredContent };
59
118
  })
60
- .filter(isNonNullable);
119
+ .filter(isNonNullable)
120
+ .map((msg) => this.truncateLargeMessage(msg));
121
+ }
122
+ /**
123
+ * Format user memory facts into a system message
124
+ * Applies token budget limit to ensure memory injection fits within constraints
125
+ */
126
+ formatUserMemory(memoryEntries) {
127
+ const memoryRatio = this.userMemoryConfig.memoryRatio ?? DEFAULT_MEMORY_RATIO;
128
+ const maxTokens = Math.floor((this.compactConfig.maxTokens ?? DEFAULT_MAX_TOKENS) * memoryRatio);
129
+ const header = "[User Memory Facts]";
130
+ let currentTokens = estimateTokens(header);
131
+ const facts = [];
132
+ for (const entry of memoryEntries) {
133
+ const fact = entry.content?.fact;
134
+ if (!fact)
135
+ continue;
136
+ const factTokens = estimateTokens(fact);
137
+ // Check if adding this fact would exceed token budget
138
+ if (currentTokens + factTokens > maxTokens) {
139
+ break; // Stop adding facts
140
+ }
141
+ facts.push(fact);
142
+ currentTokens += factTokens;
143
+ }
144
+ return {
145
+ role: "system",
146
+ content: this.formatMemoryTemplate({ header, data: facts }),
147
+ };
148
+ }
149
+ /**
150
+ * Format session memory facts into a system message
151
+ * Applies token budget limit to ensure memory injection fits within constraints
152
+ */
153
+ formatSessionMemory(memoryEntries) {
154
+ const memoryRatio = this.sessionMemoryConfig.memoryRatio ?? DEFAULT_MEMORY_RATIO;
155
+ const maxTokens = Math.floor((this.compactConfig.maxTokens ?? DEFAULT_MAX_TOKENS) * memoryRatio);
156
+ const header = "[Session Memory Facts]";
157
+ let currentTokens = estimateTokens(header);
158
+ const facts = [];
159
+ for (const entry of memoryEntries) {
160
+ const fact = entry.content?.fact;
161
+ if (!fact)
162
+ continue;
163
+ const factTokens = estimateTokens(fact);
164
+ // Check if adding this fact would exceed token budget
165
+ if (currentTokens + factTokens > maxTokens) {
166
+ break; // Stop adding facts
167
+ }
168
+ facts.push(fact);
169
+ currentTokens += factTokens;
170
+ }
171
+ return {
172
+ role: "system",
173
+ content: this.formatMemoryTemplate({ header, data: facts }),
174
+ };
175
+ }
176
+ formatMemoryTemplate({ header, data }) {
177
+ return `\
178
+ ${header}
179
+
180
+ ${"```yaml"}
181
+ ${stringify(data)}
182
+ ${"```"}
183
+ `;
61
184
  }
62
185
  async startMessage(input, message, options) {
63
186
  await this.ensureInitialized();
64
- await this.maybeAutoCompact(options);
65
- // Always wait for compaction to complete before starting a new message
66
- // This ensures data consistency even in async compact mode
67
- if (this.compactionPromise)
68
- 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;
69
196
  this.runtimeState.currentEntry = { input, messages: [message] };
70
197
  }
71
- async endMessage(output, options) {
198
+ async endMessage(output, message, options) {
72
199
  await this.ensureInitialized();
73
200
  if (!this.runtimeState.currentEntry?.input ||
74
201
  !this.runtimeState.currentEntry.messages?.length) {
75
202
  throw new Error("No current entry to end. Call startMessage() first.");
76
203
  }
204
+ if (message)
205
+ this.runtimeState.currentEntry.messages.push(message);
77
206
  this.runtimeState.currentEntry.output = output;
78
207
  let newEntry;
79
- if (this.afs && this.historyModulePath) {
80
- 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"), {
81
211
  userId: this.userId,
82
212
  sessionId: this.sessionId,
83
213
  agentId: this.agentId,
@@ -85,6 +215,7 @@ export class AgentSession {
85
215
  })).data;
86
216
  }
87
217
  else {
218
+ // Create in-memory entry for runtime state
88
219
  const id = v7();
89
220
  newEntry = {
90
221
  id,
@@ -97,8 +228,16 @@ export class AgentSession {
97
228
  }
98
229
  this.runtimeState.historyEntries.push(newEntry);
99
230
  this.runtimeState.currentEntry = null;
100
- // Check if auto-compact should be triggered
101
- 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
+ }
102
241
  }
103
242
  /**
104
243
  * Manually trigger compaction
@@ -113,36 +252,34 @@ export class AgentSession {
113
252
  this.compactionPromise = this.doCompact(options).finally(() => {
114
253
  this.compactionPromise = undefined;
115
254
  });
116
- return this.compactionPromise;
255
+ const isAsync = this.compactConfig.async ?? DEFAULT_COMPACT_ASYNC;
256
+ if (!isAsync)
257
+ await this.compactionPromise;
117
258
  }
118
259
  /**
119
260
  * Internal method that performs the actual compaction
120
261
  */
121
262
  async doCompact(options) {
122
- const { compactor, keepRecentRatio } = this.compactConfig ?? {};
263
+ const { compactor } = this.compactConfig ?? {};
123
264
  if (!compactor) {
124
265
  throw new Error("Cannot compact without a compactor agent configured.");
125
266
  }
126
267
  const historyEntries = this.runtimeState.historyEntries;
127
268
  if (historyEntries.length === 0)
128
269
  return;
129
- // Calculate token budget for keeping recent messages
130
- const ratio = keepRecentRatio ?? DEFAULT_KEEP_RECENT_RATIO;
131
- const maxTokens = this.compactConfig?.maxTokens ?? DEFAULT_MAX_TOKENS;
132
- let keepTokenBudget = Math.floor(maxTokens * ratio);
133
- // Calculate tokens for system messages
134
- const systemTokens = (this.runtimeState.systemMessages ?? []).reduce((sum, msg) => {
135
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
136
- return sum + estimateTokens(content);
137
- }, 0);
138
- // Calculate tokens for current entry messages
139
- const currentTokens = (this.runtimeState.currentEntry?.messages ?? []).reduce((sum, msg) => {
140
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
141
- return sum + estimateTokens(content);
142
- }, 0);
143
- // Subtract system and current tokens from budget
144
- // This ensures total tokens (system + current + kept history) stays within ratio budget
145
- keepTokenBudget = Math.max(0, keepTokenBudget - systemTokens - currentTokens);
270
+ const maxTokens = this.maxTokens;
271
+ // Target to keep only 50% of keepRecentTokens to leave buffer room
272
+ // This avoids triggering compression again shortly after compaction
273
+ // Similar to compactCurrentEntry, we compress more aggressively to leave headroom
274
+ //
275
+ // Note: We don't subtract systemTokens or currentEntry tokens because:
276
+ // 1. keepRecentTokens is already a relative ratio (e.g., 50% of maxTokens)
277
+ // 2. systemTokens overhead is typically small (~1-2k, ~1-2% of maxTokens)
278
+ // 3. currentEntry is still being constructed (not yet added to history)
279
+ // 4. In tool use scenarios, currentEntry can be very large (many tool calls)
280
+ // 5. Subtracting them would complicate logic without significant benefit
281
+ // 6. Total token limit is enforced by maybeAutoCompact trigger condition
282
+ const keepRecentTokens = this.keepRecentTokens * 0.5;
146
283
  // Find split point by iterating backwards from most recent entry
147
284
  // The split point divides history into: [compact] | [keep]
148
285
  let splitIndex = historyEntries.length; // Default: keep all (no compaction)
@@ -153,7 +290,7 @@ export class AgentSession {
153
290
  continue;
154
291
  const entryTokens = this.estimateMessagesTokens(entry.content?.messages ?? []);
155
292
  // Check if adding this entry would exceed token budget
156
- if (accumulatedTokens + entryTokens > keepTokenBudget) {
293
+ if (accumulatedTokens + entryTokens > keepRecentTokens) {
157
294
  // Would exceed budget, split here (this entry and earlier ones will be compacted)
158
295
  splitIndex = i + 1;
159
296
  break;
@@ -175,58 +312,179 @@ export class AgentSession {
175
312
  // Split into batches to avoid context overflow
176
313
  const batches = this.splitIntoBatches(entriesToCompact, maxTokens);
177
314
  // Process batches incrementally, each summary becomes input for the next
178
- let currentSummary = this.runtimeState.compactSummary;
315
+ let currentSummary = this.runtimeState.historyCompact?.summary;
179
316
  for (const batch of batches) {
317
+ const messages = batch
318
+ .flatMap((e) => e.content?.messages ?? [])
319
+ .filter(isNonNullable)
320
+ .map((msg) => this.truncateLargeMessage(msg));
180
321
  const result = await options.context.invoke(compactor, {
181
322
  previousSummary: [currentSummary].filter(isNonNullable),
182
- messages: batch.flatMap((e) => e.content?.messages ?? []).filter(isNonNullable),
323
+ messages,
183
324
  });
184
325
  currentSummary = result.summary;
185
326
  }
327
+ // Extract last Agent Skill from entries to compact
328
+ let lastAgentSkill = this.findLastAgentSkill(entriesToCompact);
329
+ // If no skill found in entries to compact, inherit from previous compact
330
+ if (!lastAgentSkill && this.runtimeState.historyCompact?.lastAgentSkill) {
331
+ lastAgentSkill = this.runtimeState.historyCompact.lastAgentSkill;
332
+ }
333
+ // Create compact content
334
+ const historyCompact = {
335
+ summary: currentSummary ?? "",
336
+ lastAgentSkill,
337
+ };
186
338
  // Write compact entry to AFS
187
339
  if (this.afs && this.historyModulePath) {
188
340
  await this.afs.write(joinURL(this.historyModulePath, "by-session", this.sessionId, "@metadata/compact/new"), {
189
341
  userId: this.userId,
190
342
  agentId: this.agentId,
191
- content: { summary: currentSummary },
343
+ content: historyCompact,
192
344
  metadata: {
193
345
  latestEntryId: latestCompactedEntry.id,
194
346
  },
195
347
  });
196
348
  }
197
349
  // Update runtime state: keep the summary and recent entries
198
- this.runtimeState.compactSummary = currentSummary;
350
+ this.runtimeState.historyCompact = historyCompact;
199
351
  this.runtimeState.historyEntries = entriesToKeep;
200
352
  }
353
+ async compactCurrentEntry(options) {
354
+ const { compactor } = this.compactConfig ?? {};
355
+ if (!compactor)
356
+ return;
357
+ const currentEntry = this.runtimeState.currentEntry;
358
+ if (!currentEntry?.messages?.length)
359
+ return;
360
+ const alreadyCompressedCount = this.runtimeState.currentEntryCompact?.compressedCount ?? 0;
361
+ const uncompressedMessages = currentEntry.messages.slice(alreadyCompressedCount);
362
+ if (uncompressedMessages.length === 0)
363
+ return;
364
+ // Target to keep only 50% of keepTokenBudget to leave buffer room
365
+ // This avoids frequent small-batch compressions in tool use scenarios
366
+ const keepTokenBudget = this.keepRecentTokens * 0.5;
367
+ let splitIndex = uncompressedMessages.length;
368
+ let accumulatedTokens = 0;
369
+ for (let i = uncompressedMessages.length - 1; i >= 0; i--) {
370
+ const msg = uncompressedMessages[i];
371
+ if (!msg)
372
+ continue;
373
+ const msgTokens = this.estimateMessagesTokens([msg]);
374
+ if (accumulatedTokens + msgTokens > keepTokenBudget) {
375
+ splitIndex = i + 1;
376
+ break;
377
+ }
378
+ accumulatedTokens += msgTokens;
379
+ splitIndex = i;
380
+ }
381
+ const keptMessages = uncompressedMessages.slice(splitIndex);
382
+ const requiredToolCallIds = new Set();
383
+ for (const msg of keptMessages) {
384
+ if (msg.role === "tool" && msg.toolCallId) {
385
+ requiredToolCallIds.add(msg.toolCallId);
386
+ }
387
+ }
388
+ if (requiredToolCallIds.size > 0) {
389
+ for (let i = splitIndex - 1; i >= 0; i--) {
390
+ const msg = uncompressedMessages[i];
391
+ if (!msg?.toolCalls)
392
+ continue;
393
+ for (const toolCall of msg.toolCalls) {
394
+ if (requiredToolCallIds.has(toolCall.id)) {
395
+ splitIndex = i;
396
+ break;
397
+ }
398
+ }
399
+ }
400
+ }
401
+ const messagesToCompact = uncompressedMessages
402
+ .slice(0, splitIndex)
403
+ .map((msg) => this.truncateLargeMessage(msg));
404
+ if (messagesToCompact.length === 0)
405
+ return;
406
+ const result = await options.context.invoke(compactor, {
407
+ previousSummary: this.runtimeState.currentEntryCompact?.summary
408
+ ? [this.runtimeState.currentEntryCompact.summary]
409
+ : undefined,
410
+ messages: messagesToCompact,
411
+ });
412
+ // Find last Agent Skill from messages being compacted
413
+ const lastAgentSkill = this.findLastAgentSkillFromMessages(messagesToCompact) ??
414
+ this.runtimeState.currentEntryCompact?.lastAgentSkill;
415
+ this.runtimeState.currentEntryCompact = {
416
+ summary: result.summary,
417
+ lastAgentSkill,
418
+ compressedCount: alreadyCompressedCount + messagesToCompact.length,
419
+ };
420
+ }
421
+ async maybeCompactCurrentEntry(options) {
422
+ const currentEntry = this.runtimeState.currentEntry;
423
+ if (!currentEntry?.messages?.length)
424
+ return;
425
+ const compressedCount = this.runtimeState.currentEntryCompact?.compressedCount ?? 0;
426
+ const uncompressedMessages = currentEntry.messages.slice(compressedCount);
427
+ const threshold = this.keepRecentTokens;
428
+ const currentTokens = this.estimateMessagesTokens(uncompressedMessages);
429
+ if (currentTokens > threshold) {
430
+ await this.compactCurrentEntry(options);
431
+ }
432
+ }
201
433
  async maybeAutoCompact(options) {
202
434
  if (this.compactionPromise)
203
435
  await this.compactionPromise;
204
- if (!this.compactConfig)
205
- return;
206
- // Check if compaction is disabled
207
436
  const mode = this.compactConfig.mode ?? DEFAULT_COMPACT_MODE;
208
437
  if (mode === "disabled")
209
438
  return;
210
439
  const { compactor } = this.compactConfig;
211
- const maxTokens = this.compactConfig.maxTokens ?? DEFAULT_MAX_TOKENS;
212
440
  if (!compactor)
213
441
  return;
214
- const currentTokens = this.estimateMessagesTokens(await this.getMessages());
442
+ const maxTokens = this.maxTokens;
443
+ const messages = await this.getMessages();
444
+ const currentTokens = this.estimateMessagesTokens(messages);
215
445
  if (currentTokens >= maxTokens) {
216
- this.compact(options);
217
- const isAsync = this.compactConfig.async ?? DEFAULT_COMPACT_ASYNC;
218
- if (!isAsync)
219
- await this.compactionPromise;
446
+ await this.compact(options);
220
447
  }
221
448
  }
222
449
  /**
223
- * Estimate token count for 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
224
453
  */
225
- estimateMessagesTokens(messages) {
226
- return messages.reduce((sum, msg) => {
227
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
228
- return sum + estimateTokens(content);
229
- }, 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;
230
488
  }
231
489
  /**
232
490
  * Split entries into batches based on token limit
@@ -255,12 +513,63 @@ export class AgentSession {
255
513
  }
256
514
  return batches;
257
515
  }
258
- async appendCurrentMessages(...messages) {
516
+ async appendCurrentMessages(messages, options) {
259
517
  await this.ensureInitialized();
260
518
  if (!this.runtimeState.currentEntry || !this.runtimeState.currentEntry.messages?.length) {
261
519
  throw new Error("No current entry to append messages. Call startMessage() first.");
262
520
  }
263
- this.runtimeState.currentEntry.messages.push(...messages);
521
+ this.runtimeState.currentEntry.messages.push(...[messages].flat());
522
+ await this.maybeCompactCurrentEntry(options);
523
+ }
524
+ /**
525
+ * Truncate text content to fit within target token limit
526
+ * @param text The text to truncate
527
+ * @param currentTokens Current token count of the text
528
+ * @param targetTokens Target token count after truncation
529
+ * @returns Truncated text
530
+ */
531
+ truncateText(text, currentTokens, targetTokens) {
532
+ if (currentTokens <= targetTokens)
533
+ return text;
534
+ const keepRatio = (targetTokens / currentTokens) * 0.9;
535
+ const keepLength = Math.floor(text.length * keepRatio);
536
+ const headLength = Math.floor(keepLength * 0.7);
537
+ const tailLength = Math.floor(keepLength * 0.3);
538
+ return (text.slice(0, headLength) +
539
+ `\n\n[... truncated ${currentTokens - targetTokens} tokens ...]\n\n` +
540
+ text.slice(-tailLength));
541
+ }
542
+ truncateLargeMessage(msg) {
543
+ const singleMessageLimit = this.singleMessageLimit;
544
+ // Handle string content
545
+ if (typeof msg.content === "string") {
546
+ const tokens = estimateTokens(msg.content);
547
+ if (tokens <= singleMessageLimit)
548
+ return msg;
549
+ const truncated = this.truncateText(msg.content, tokens, singleMessageLimit);
550
+ return { ...msg, content: truncated };
551
+ }
552
+ // Handle array content (UnionContent[])
553
+ if (Array.isArray(msg.content)) {
554
+ // Truncate each text block individually if it exceeds the limit
555
+ const truncatedContent = msg.content.map((block) => {
556
+ // Keep non-text blocks unchanged
557
+ if (block.type !== "text" || typeof block.text !== "string") {
558
+ return block;
559
+ }
560
+ // Check if this text block needs truncation
561
+ const blockTokens = estimateTokens(block.text);
562
+ if (blockTokens <= singleMessageLimit) {
563
+ return block;
564
+ }
565
+ // Truncate this text block independently
566
+ const truncatedText = this.truncateText(block.text, blockTokens, singleMessageLimit);
567
+ return { ...block, text: truncatedText };
568
+ });
569
+ return { ...msg, content: truncatedContent };
570
+ }
571
+ // Unknown content type, return as-is
572
+ return msg;
264
573
  }
265
574
  async ensureInitialized() {
266
575
  this.initialized ??= this.initialize();
@@ -270,39 +579,390 @@ export class AgentSession {
270
579
  if (this.initialized)
271
580
  return;
272
581
  await this.initializeDefaultCompactor();
582
+ await this.initializeDefaultSessionMemoryExtractor();
583
+ await this.initializeDefaultUserMemoryExtractor();
273
584
  const historyModule = (await this.afs?.listModules())?.find((m) => m.module instanceof AFSHistory);
274
585
  this.historyModulePath = historyModule?.path;
275
586
  if (this.afs && this.historyModulePath) {
276
- // Load latest compact entry if exists
277
- const compactPath = joinURL(this.historyModulePath, "by-session", this.sessionId, "@metadata/compact");
278
- const compactResult = await this.afs.list(compactPath, {
279
- filter: { userId: this.userId, agentId: this.agentId },
280
- orderBy: [["createdAt", "desc"]],
281
- 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);
282
694
  });
283
- const latestCompact = compactResult.data[0];
284
- if (latestCompact?.content?.summary) {
285
- 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
+ }
286
900
  }
287
- // Load history entries (after compact point if exists)
288
- const afsEntries = (await this.afs.list(joinURL(this.historyModulePath, "by-session", this.sessionId), {
289
- filter: {
290
- userId: this.userId,
291
- agentId: this.agentId,
292
- // Only load entries after the latest compact
293
- after: latestCompact?.createdAt?.toISOString(),
294
- },
295
- orderBy: [["createdAt", "desc"]],
296
- // Set a very large limit to load all history entries
297
- // The default limit is 10 which would cause history truncation
298
- limit: 10000,
299
- })).data;
300
- this.runtimeState.historyEntries = afsEntries
301
- .reverse()
302
- .filter((entry) => isNonNullable(entry.content));
303
901
  }
304
902
  }
903
+ /**
904
+ * Find Agent Skill content from a single message
905
+ * @param msg - Message to search in
906
+ * @returns The skill content text if found, undefined otherwise
907
+ */
908
+ findSkillContentInMessage(msg) {
909
+ if (msg.role === "user" && Array.isArray(msg.content)) {
910
+ const skillBlock = msg.content.find((block) => block.type === "text" && block.isAgentSkill === true);
911
+ if (skillBlock && skillBlock.type === "text") {
912
+ return skillBlock.text;
913
+ }
914
+ }
915
+ return undefined;
916
+ }
917
+ /**
918
+ * Find the last Agent Skill from a list of messages
919
+ * @param messages - Messages to search through
920
+ * @returns The last Agent Skill found, or undefined if none found
921
+ */
922
+ findLastAgentSkillFromMessages(messages) {
923
+ // Search backwards through messages to find the last Agent Skill
924
+ for (let i = messages.length - 1; i >= 0; i--) {
925
+ const msg = messages[i];
926
+ if (!msg)
927
+ continue;
928
+ const skillContent = this.findSkillContentInMessage(msg);
929
+ if (skillContent) {
930
+ return {
931
+ content: skillContent,
932
+ };
933
+ }
934
+ }
935
+ return undefined;
936
+ }
937
+ /**
938
+ * Find the last Agent Skill from a list of history entries
939
+ * @param entries - History entries to search through
940
+ * @returns The last Agent Skill found, or undefined if none found
941
+ */
942
+ findLastAgentSkill(entries) {
943
+ // Flatten all messages from entries
944
+ const allMessages = entries.flatMap((entry) => entry.content?.messages ?? []);
945
+ return this.findLastAgentSkillFromMessages(allMessages);
946
+ }
305
947
  async initializeDefaultCompactor() {
306
948
  this.compactConfig.compactor ??= await import("./compact/compactor.js").then((m) => new m.AISessionCompactor());
307
949
  }
950
+ async initializeDefaultSessionMemoryExtractor() {
951
+ this.sessionMemoryConfig.extractor ??= await import("./compact/session-memory-extractor.js").then((m) => new m.AISessionMemoryExtractor());
952
+ }
953
+ async initializeDefaultUserMemoryExtractor() {
954
+ this.userMemoryConfig.extractor ??= await import("./compact/user-memory-extractor.js").then((m) => new m.AIUserMemoryExtractor());
955
+ }
956
+ get maxTokens() {
957
+ return this.compactConfig?.maxTokens ?? DEFAULT_MAX_TOKENS;
958
+ }
959
+ get keepRecentRatio() {
960
+ return this.compactConfig?.keepRecentRatio ?? DEFAULT_KEEP_RECENT_RATIO;
961
+ }
962
+ get keepRecentTokens() {
963
+ return Math.floor(this.maxTokens * this.keepRecentRatio);
964
+ }
965
+ get singleMessageLimit() {
966
+ return this.keepRecentTokens * 0.5;
967
+ }
308
968
  }