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