@aigne/core 1.72.0-beta.8 → 1.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +349 -0
  2. package/lib/cjs/agents/agent.d.ts +5 -0
  3. package/lib/cjs/agents/agent.js +5 -0
  4. package/lib/cjs/agents/ai-agent.d.ts +41 -5
  5. package/lib/cjs/agents/ai-agent.js +89 -29
  6. package/lib/cjs/agents/chat-model.d.ts +3 -0
  7. package/lib/cjs/agents/chat-model.js +18 -1
  8. package/lib/cjs/agents/image-model.js +1 -1
  9. package/lib/cjs/agents/model.d.ts +3 -3
  10. package/lib/cjs/agents/model.js +2 -2
  11. package/lib/cjs/agents/video-model.js +1 -1
  12. package/lib/cjs/aigne/context.js +1 -3
  13. package/lib/cjs/prompt/agent-session.d.ts +115 -5
  14. package/lib/cjs/prompt/agent-session.js +752 -85
  15. package/lib/cjs/prompt/compact/compactor.js +4 -0
  16. package/lib/cjs/prompt/compact/session-memory-extractor.d.ts +7 -0
  17. package/lib/cjs/prompt/compact/session-memory-extractor.js +143 -0
  18. package/lib/cjs/prompt/compact/types.d.ts +257 -0
  19. package/lib/cjs/prompt/compact/types.js +35 -1
  20. package/lib/cjs/prompt/compact/user-memory-extractor.d.ts +7 -0
  21. package/lib/cjs/prompt/compact/user-memory-extractor.js +124 -0
  22. package/lib/cjs/prompt/prompt-builder.js +3 -3
  23. package/lib/cjs/prompt/skills/afs/agent-skill/agent-skill.d.ts +2 -0
  24. package/lib/cjs/prompt/skills/afs/agent-skill/agent-skill.js +6 -0
  25. package/lib/cjs/prompt/skills/afs/agent-skill/skill-loader.d.ts +1 -2
  26. package/lib/cjs/prompt/skills/afs/agent-skill/skill-loader.js +15 -26
  27. package/lib/cjs/prompt/skills/afs/list.d.ts +2 -0
  28. package/lib/cjs/prompt/skills/afs/list.js +9 -1
  29. package/lib/cjs/prompt/skills/afs/read.d.ts +3 -1
  30. package/lib/cjs/prompt/skills/afs/read.js +5 -0
  31. package/lib/cjs/utils/mcp-utils.js +1 -1
  32. package/lib/cjs/utils/token-estimator.js +1 -1
  33. package/lib/cjs/utils/type-utils.js +0 -1
  34. package/lib/dts/agents/agent.d.ts +5 -0
  35. package/lib/dts/agents/ai-agent.d.ts +41 -5
  36. package/lib/dts/agents/chat-model.d.ts +3 -0
  37. package/lib/dts/agents/model.d.ts +3 -3
  38. package/lib/dts/prompt/agent-session.d.ts +115 -5
  39. package/lib/dts/prompt/compact/session-memory-extractor.d.ts +7 -0
  40. package/lib/dts/prompt/compact/types.d.ts +257 -0
  41. package/lib/dts/prompt/compact/user-memory-extractor.d.ts +7 -0
  42. package/lib/dts/prompt/skills/afs/agent-skill/agent-skill.d.ts +2 -0
  43. package/lib/dts/prompt/skills/afs/agent-skill/skill-loader.d.ts +1 -2
  44. package/lib/dts/prompt/skills/afs/list.d.ts +2 -0
  45. package/lib/dts/prompt/skills/afs/read.d.ts +3 -1
  46. package/lib/esm/agents/agent.d.ts +5 -0
  47. package/lib/esm/agents/agent.js +5 -0
  48. package/lib/esm/agents/ai-agent.d.ts +41 -5
  49. package/lib/esm/agents/ai-agent.js +89 -29
  50. package/lib/esm/agents/chat-model.d.ts +3 -0
  51. package/lib/esm/agents/chat-model.js +18 -1
  52. package/lib/esm/agents/image-model.js +1 -1
  53. package/lib/esm/agents/model.d.ts +3 -3
  54. package/lib/esm/agents/model.js +2 -2
  55. package/lib/esm/agents/video-model.js +1 -1
  56. package/lib/esm/aigne/context.js +2 -4
  57. package/lib/esm/prompt/agent-session.d.ts +115 -5
  58. package/lib/esm/prompt/agent-session.js +750 -86
  59. package/lib/esm/prompt/compact/compactor.js +4 -0
  60. package/lib/esm/prompt/compact/session-memory-extractor.d.ts +7 -0
  61. package/lib/esm/prompt/compact/session-memory-extractor.js +139 -0
  62. package/lib/esm/prompt/compact/types.d.ts +257 -0
  63. package/lib/esm/prompt/compact/types.js +34 -0
  64. package/lib/esm/prompt/compact/user-memory-extractor.d.ts +7 -0
  65. package/lib/esm/prompt/compact/user-memory-extractor.js +120 -0
  66. package/lib/esm/prompt/prompt-builder.js +3 -3
  67. package/lib/esm/prompt/skills/afs/agent-skill/agent-skill.d.ts +2 -0
  68. package/lib/esm/prompt/skills/afs/agent-skill/agent-skill.js +6 -0
  69. package/lib/esm/prompt/skills/afs/agent-skill/skill-loader.d.ts +1 -2
  70. package/lib/esm/prompt/skills/afs/agent-skill/skill-loader.js +14 -24
  71. package/lib/esm/prompt/skills/afs/list.d.ts +2 -0
  72. package/lib/esm/prompt/skills/afs/list.js +9 -1
  73. package/lib/esm/prompt/skills/afs/read.d.ts +3 -1
  74. package/lib/esm/prompt/skills/afs/read.js +5 -0
  75. package/lib/esm/utils/mcp-utils.js +1 -1
  76. package/lib/esm/utils/token-estimator.js +1 -1
  77. package/lib/esm/utils/type-utils.js +0 -1
  78. package/package.json +6 -6
@@ -15,6 +15,9 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
15
15
  }) : function(o, v) {
16
16
  o["default"] = v;
17
17
  });
18
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
19
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
20
+ };
18
21
  var __importStar = (this && this.__importStar) || (function () {
19
22
  var ownKeys = function(o) {
20
23
  ownKeys = Object.getOwnPropertyNames || function (o) {
@@ -37,79 +40,213 @@ exports.AgentSession = void 0;
37
40
  const afs_history_1 = require("@aigne/afs-history");
38
41
  const uuid_1 = require("@aigne/uuid");
39
42
  const ufo_1 = require("ufo");
43
+ const yaml_1 = require("yaml");
44
+ const logger_js_1 = require("../utils/logger.js");
40
45
  const token_estimator_js_1 = require("../utils/token-estimator.js");
41
46
  const type_utils_js_1 = require("../utils/type-utils.js");
42
47
  const types_js_1 = require("./compact/types.js");
48
+ __exportStar(require("./compact/types.js"), exports);
43
49
  class AgentSession {
44
50
  sessionId;
45
51
  userId;
46
52
  agentId;
47
53
  afs;
48
54
  historyModulePath;
55
+ mode;
49
56
  compactConfig;
57
+ sessionMemoryConfig;
58
+ userMemoryConfig;
50
59
  runtimeState;
51
60
  initialized;
52
61
  compactionPromise;
62
+ sessionMemoryUpdatePromise;
63
+ userMemoryUpdatePromise;
53
64
  constructor(options) {
54
65
  this.sessionId = options.sessionId;
55
66
  this.userId = options.userId;
56
67
  this.agentId = options.agentId;
57
68
  this.afs = options.afs;
69
+ this.mode = options.mode ?? types_js_1.DEFAULT_SESSION_MODE;
58
70
  this.compactConfig = options.compact ?? {};
71
+ this.sessionMemoryConfig = options.sessionMemory ?? {};
72
+ this.userMemoryConfig = options.userMemory ?? {};
59
73
  this.runtimeState = {
60
74
  historyEntries: [],
61
75
  currentEntry: null,
62
76
  };
63
77
  }
78
+ /**
79
+ * Check if memory extraction is enabled
80
+ * Memory extraction requires mode to be "auto" AND AFS history module to be available
81
+ */
82
+ get isMemoryEnabled() {
83
+ return this.mode === "auto" && !!this.afs && !!this.historyModulePath;
84
+ }
64
85
  async setSystemMessages(...messages) {
65
86
  await this.ensureInitialized();
66
87
  this.runtimeState.systemMessages = messages;
67
88
  }
68
89
  async getMessages() {
69
90
  await this.ensureInitialized();
70
- const { systemMessages, 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
85
- return messages.map((msg) => {
146
+ // Filter out thinking messages and truncate large messages
147
+ return messages
148
+ .map((msg) => {
86
149
  if (!msg.content || typeof msg.content === "string") {
87
150
  return msg;
88
151
  }
89
152
  // Filter out thinking from UnionContent[]
90
153
  const filteredContent = msg.content.filter((c) => !(c.type === "text" && c.isThinking));
91
- return { ...msg, content: filteredContent.length > 0 ? filteredContent : undefined };
92
- });
154
+ if (filteredContent.length === 0)
155
+ return null;
156
+ return { ...msg, content: filteredContent };
157
+ })
158
+ .filter(type_utils_js_1.isNonNullable)
159
+ .map((msg) => this.truncateLargeMessage(msg));
160
+ }
161
+ /**
162
+ * Format user memory facts into a system message
163
+ * Applies token budget limit to ensure memory injection fits within constraints
164
+ */
165
+ formatUserMemory(memoryEntries) {
166
+ const memoryRatio = this.userMemoryConfig.memoryRatio ?? types_js_1.DEFAULT_MEMORY_RATIO;
167
+ const maxTokens = Math.floor((this.compactConfig.maxTokens ?? types_js_1.DEFAULT_MAX_TOKENS) * memoryRatio);
168
+ const header = "[User Memory Facts]";
169
+ let currentTokens = (0, token_estimator_js_1.estimateTokens)(header);
170
+ const facts = [];
171
+ for (const entry of memoryEntries) {
172
+ const fact = entry.content?.fact;
173
+ if (!fact)
174
+ continue;
175
+ const factTokens = (0, token_estimator_js_1.estimateTokens)(fact);
176
+ // Check if adding this fact would exceed token budget
177
+ if (currentTokens + factTokens > maxTokens) {
178
+ break; // Stop adding facts
179
+ }
180
+ facts.push(fact);
181
+ currentTokens += factTokens;
182
+ }
183
+ return {
184
+ role: "system",
185
+ content: this.formatMemoryTemplate({ header, data: facts }),
186
+ };
187
+ }
188
+ /**
189
+ * Format session memory facts into a system message
190
+ * Applies token budget limit to ensure memory injection fits within constraints
191
+ */
192
+ formatSessionMemory(memoryEntries) {
193
+ const memoryRatio = this.sessionMemoryConfig.memoryRatio ?? types_js_1.DEFAULT_MEMORY_RATIO;
194
+ const maxTokens = Math.floor((this.compactConfig.maxTokens ?? types_js_1.DEFAULT_MAX_TOKENS) * memoryRatio);
195
+ const header = "[Session Memory Facts]";
196
+ let currentTokens = (0, token_estimator_js_1.estimateTokens)(header);
197
+ const facts = [];
198
+ for (const entry of memoryEntries) {
199
+ const fact = entry.content?.fact;
200
+ if (!fact)
201
+ continue;
202
+ const factTokens = (0, token_estimator_js_1.estimateTokens)(fact);
203
+ // Check if adding this fact would exceed token budget
204
+ if (currentTokens + factTokens > maxTokens) {
205
+ break; // Stop adding facts
206
+ }
207
+ facts.push(fact);
208
+ currentTokens += factTokens;
209
+ }
210
+ return {
211
+ role: "system",
212
+ content: this.formatMemoryTemplate({ header, data: facts }),
213
+ };
214
+ }
215
+ formatMemoryTemplate({ header, data }) {
216
+ return `\
217
+ ${header}
218
+
219
+ ${"```yaml"}
220
+ ${(0, yaml_1.stringify)(data)}
221
+ ${"```"}
222
+ `;
93
223
  }
94
224
  async startMessage(input, message, options) {
95
225
  await this.ensureInitialized();
96
- await this.maybeAutoCompact(options);
97
- // Always wait for compaction to complete before starting a new message
98
- // This ensures data consistency even in async compact mode
99
- if (this.compactionPromise)
100
- 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;
101
235
  this.runtimeState.currentEntry = { input, messages: [message] };
102
236
  }
103
- async endMessage(output, options) {
237
+ async endMessage(output, message, options) {
104
238
  await this.ensureInitialized();
105
239
  if (!this.runtimeState.currentEntry?.input ||
106
240
  !this.runtimeState.currentEntry.messages?.length) {
107
241
  throw new Error("No current entry to end. Call startMessage() first.");
108
242
  }
243
+ if (message)
244
+ this.runtimeState.currentEntry.messages.push(message);
109
245
  this.runtimeState.currentEntry.output = output;
110
246
  let newEntry;
111
- if (this.afs && this.historyModulePath) {
112
- 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"), {
113
250
  userId: this.userId,
114
251
  sessionId: this.sessionId,
115
252
  agentId: this.agentId,
@@ -117,6 +254,7 @@ class AgentSession {
117
254
  })).data;
118
255
  }
119
256
  else {
257
+ // Create in-memory entry for runtime state
120
258
  const id = (0, uuid_1.v7)();
121
259
  newEntry = {
122
260
  id,
@@ -129,8 +267,16 @@ class AgentSession {
129
267
  }
130
268
  this.runtimeState.historyEntries.push(newEntry);
131
269
  this.runtimeState.currentEntry = null;
132
- // Check if auto-compact should be triggered
133
- 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
+ }
134
280
  }
135
281
  /**
136
282
  * Manually trigger compaction
@@ -145,36 +291,34 @@ class AgentSession {
145
291
  this.compactionPromise = this.doCompact(options).finally(() => {
146
292
  this.compactionPromise = undefined;
147
293
  });
148
- return this.compactionPromise;
294
+ const isAsync = this.compactConfig.async ?? types_js_1.DEFAULT_COMPACT_ASYNC;
295
+ if (!isAsync)
296
+ await this.compactionPromise;
149
297
  }
150
298
  /**
151
299
  * Internal method that performs the actual compaction
152
300
  */
153
301
  async doCompact(options) {
154
- const { compactor, keepRecentRatio } = this.compactConfig ?? {};
302
+ const { compactor } = this.compactConfig ?? {};
155
303
  if (!compactor) {
156
304
  throw new Error("Cannot compact without a compactor agent configured.");
157
305
  }
158
306
  const historyEntries = this.runtimeState.historyEntries;
159
307
  if (historyEntries.length === 0)
160
308
  return;
161
- // Calculate token budget for keeping recent messages
162
- const ratio = keepRecentRatio ?? types_js_1.DEFAULT_KEEP_RECENT_RATIO;
163
- const maxTokens = this.compactConfig?.maxTokens ?? types_js_1.DEFAULT_MAX_TOKENS;
164
- let keepTokenBudget = Math.floor(maxTokens * ratio);
165
- // Calculate tokens for system messages
166
- const systemTokens = (this.runtimeState.systemMessages ?? []).reduce((sum, msg) => {
167
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
168
- return sum + (0, token_estimator_js_1.estimateTokens)(content);
169
- }, 0);
170
- // Calculate tokens for current entry messages
171
- const currentTokens = (this.runtimeState.currentEntry?.messages ?? []).reduce((sum, msg) => {
172
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
173
- return sum + (0, token_estimator_js_1.estimateTokens)(content);
174
- }, 0);
175
- // Subtract system and current tokens from budget
176
- // This ensures total tokens (system + current + kept history) stays within ratio budget
177
- keepTokenBudget = Math.max(0, keepTokenBudget - systemTokens - currentTokens);
309
+ const maxTokens = this.maxTokens;
310
+ // Target to keep only 50% of keepRecentTokens to leave buffer room
311
+ // This avoids triggering compression again shortly after compaction
312
+ // Similar to compactCurrentEntry, we compress more aggressively to leave headroom
313
+ //
314
+ // Note: We don't subtract systemTokens or currentEntry tokens because:
315
+ // 1. keepRecentTokens is already a relative ratio (e.g., 50% of maxTokens)
316
+ // 2. systemTokens overhead is typically small (~1-2k, ~1-2% of maxTokens)
317
+ // 3. currentEntry is still being constructed (not yet added to history)
318
+ // 4. In tool use scenarios, currentEntry can be very large (many tool calls)
319
+ // 5. Subtracting them would complicate logic without significant benefit
320
+ // 6. Total token limit is enforced by maybeAutoCompact trigger condition
321
+ const keepRecentTokens = this.keepRecentTokens * 0.5;
178
322
  // Find split point by iterating backwards from most recent entry
179
323
  // The split point divides history into: [compact] | [keep]
180
324
  let splitIndex = historyEntries.length; // Default: keep all (no compaction)
@@ -185,7 +329,7 @@ class AgentSession {
185
329
  continue;
186
330
  const entryTokens = this.estimateMessagesTokens(entry.content?.messages ?? []);
187
331
  // Check if adding this entry would exceed token budget
188
- if (accumulatedTokens + entryTokens > keepTokenBudget) {
332
+ if (accumulatedTokens + entryTokens > keepRecentTokens) {
189
333
  // Would exceed budget, split here (this entry and earlier ones will be compacted)
190
334
  splitIndex = i + 1;
191
335
  break;
@@ -207,58 +351,179 @@ class AgentSession {
207
351
  // Split into batches to avoid context overflow
208
352
  const batches = this.splitIntoBatches(entriesToCompact, maxTokens);
209
353
  // Process batches incrementally, each summary becomes input for the next
210
- let currentSummary = this.runtimeState.compactSummary;
354
+ let currentSummary = this.runtimeState.historyCompact?.summary;
211
355
  for (const batch of batches) {
356
+ const messages = batch
357
+ .flatMap((e) => e.content?.messages ?? [])
358
+ .filter(type_utils_js_1.isNonNullable)
359
+ .map((msg) => this.truncateLargeMessage(msg));
212
360
  const result = await options.context.invoke(compactor, {
213
361
  previousSummary: [currentSummary].filter(type_utils_js_1.isNonNullable),
214
- messages: batch.flatMap((e) => e.content?.messages ?? []).filter(type_utils_js_1.isNonNullable),
362
+ messages,
215
363
  });
216
364
  currentSummary = result.summary;
217
365
  }
366
+ // Extract last Agent Skill from entries to compact
367
+ let lastAgentSkill = this.findLastAgentSkill(entriesToCompact);
368
+ // If no skill found in entries to compact, inherit from previous compact
369
+ if (!lastAgentSkill && this.runtimeState.historyCompact?.lastAgentSkill) {
370
+ lastAgentSkill = this.runtimeState.historyCompact.lastAgentSkill;
371
+ }
372
+ // Create compact content
373
+ const historyCompact = {
374
+ summary: currentSummary ?? "",
375
+ lastAgentSkill,
376
+ };
218
377
  // Write compact entry to AFS
219
378
  if (this.afs && this.historyModulePath) {
220
379
  await this.afs.write((0, ufo_1.joinURL)(this.historyModulePath, "by-session", this.sessionId, "@metadata/compact/new"), {
221
380
  userId: this.userId,
222
381
  agentId: this.agentId,
223
- content: { summary: currentSummary },
382
+ content: historyCompact,
224
383
  metadata: {
225
384
  latestEntryId: latestCompactedEntry.id,
226
385
  },
227
386
  });
228
387
  }
229
388
  // Update runtime state: keep the summary and recent entries
230
- this.runtimeState.compactSummary = currentSummary;
389
+ this.runtimeState.historyCompact = historyCompact;
231
390
  this.runtimeState.historyEntries = entriesToKeep;
232
391
  }
392
+ async compactCurrentEntry(options) {
393
+ const { compactor } = this.compactConfig ?? {};
394
+ if (!compactor)
395
+ return;
396
+ const currentEntry = this.runtimeState.currentEntry;
397
+ if (!currentEntry?.messages?.length)
398
+ return;
399
+ const alreadyCompressedCount = this.runtimeState.currentEntryCompact?.compressedCount ?? 0;
400
+ const uncompressedMessages = currentEntry.messages.slice(alreadyCompressedCount);
401
+ if (uncompressedMessages.length === 0)
402
+ return;
403
+ // Target to keep only 50% of keepTokenBudget to leave buffer room
404
+ // This avoids frequent small-batch compressions in tool use scenarios
405
+ const keepTokenBudget = this.keepRecentTokens * 0.5;
406
+ let splitIndex = uncompressedMessages.length;
407
+ let accumulatedTokens = 0;
408
+ for (let i = uncompressedMessages.length - 1; i >= 0; i--) {
409
+ const msg = uncompressedMessages[i];
410
+ if (!msg)
411
+ continue;
412
+ const msgTokens = this.estimateMessagesTokens([msg]);
413
+ if (accumulatedTokens + msgTokens > keepTokenBudget) {
414
+ splitIndex = i + 1;
415
+ break;
416
+ }
417
+ accumulatedTokens += msgTokens;
418
+ splitIndex = i;
419
+ }
420
+ const keptMessages = uncompressedMessages.slice(splitIndex);
421
+ const requiredToolCallIds = new Set();
422
+ for (const msg of keptMessages) {
423
+ if (msg.role === "tool" && msg.toolCallId) {
424
+ requiredToolCallIds.add(msg.toolCallId);
425
+ }
426
+ }
427
+ if (requiredToolCallIds.size > 0) {
428
+ for (let i = splitIndex - 1; i >= 0; i--) {
429
+ const msg = uncompressedMessages[i];
430
+ if (!msg?.toolCalls)
431
+ continue;
432
+ for (const toolCall of msg.toolCalls) {
433
+ if (requiredToolCallIds.has(toolCall.id)) {
434
+ splitIndex = i;
435
+ break;
436
+ }
437
+ }
438
+ }
439
+ }
440
+ const messagesToCompact = uncompressedMessages
441
+ .slice(0, splitIndex)
442
+ .map((msg) => this.truncateLargeMessage(msg));
443
+ if (messagesToCompact.length === 0)
444
+ return;
445
+ const result = await options.context.invoke(compactor, {
446
+ previousSummary: this.runtimeState.currentEntryCompact?.summary
447
+ ? [this.runtimeState.currentEntryCompact.summary]
448
+ : undefined,
449
+ messages: messagesToCompact,
450
+ });
451
+ // Find last Agent Skill from messages being compacted
452
+ const lastAgentSkill = this.findLastAgentSkillFromMessages(messagesToCompact) ??
453
+ this.runtimeState.currentEntryCompact?.lastAgentSkill;
454
+ this.runtimeState.currentEntryCompact = {
455
+ summary: result.summary,
456
+ lastAgentSkill,
457
+ compressedCount: alreadyCompressedCount + messagesToCompact.length,
458
+ };
459
+ }
460
+ async maybeCompactCurrentEntry(options) {
461
+ const currentEntry = this.runtimeState.currentEntry;
462
+ if (!currentEntry?.messages?.length)
463
+ return;
464
+ const compressedCount = this.runtimeState.currentEntryCompact?.compressedCount ?? 0;
465
+ const uncompressedMessages = currentEntry.messages.slice(compressedCount);
466
+ const threshold = this.keepRecentTokens;
467
+ const currentTokens = this.estimateMessagesTokens(uncompressedMessages);
468
+ if (currentTokens > threshold) {
469
+ await this.compactCurrentEntry(options);
470
+ }
471
+ }
233
472
  async maybeAutoCompact(options) {
234
473
  if (this.compactionPromise)
235
474
  await this.compactionPromise;
236
- if (!this.compactConfig)
237
- return;
238
- // Check if compaction is disabled
239
475
  const mode = this.compactConfig.mode ?? types_js_1.DEFAULT_COMPACT_MODE;
240
476
  if (mode === "disabled")
241
477
  return;
242
478
  const { compactor } = this.compactConfig;
243
- const maxTokens = this.compactConfig.maxTokens ?? types_js_1.DEFAULT_MAX_TOKENS;
244
479
  if (!compactor)
245
480
  return;
246
- const currentTokens = this.estimateMessagesTokens(await this.getMessages());
481
+ const maxTokens = this.maxTokens;
482
+ const messages = await this.getMessages();
483
+ const currentTokens = this.estimateMessagesTokens(messages);
247
484
  if (currentTokens >= maxTokens) {
248
- this.compact(options);
249
- const isAsync = this.compactConfig.async ?? types_js_1.DEFAULT_COMPACT_ASYNC;
250
- if (!isAsync)
251
- await this.compactionPromise;
485
+ await this.compact(options);
252
486
  }
253
487
  }
254
488
  /**
255
- * Estimate token count for 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
256
492
  */
257
- estimateMessagesTokens(messages) {
258
- return messages.reduce((sum, msg) => {
259
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
260
- return sum + (0, token_estimator_js_1.estimateTokens)(content);
261
- }, 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;
262
527
  }
263
528
  /**
264
529
  * Split entries into batches based on token limit
@@ -287,12 +552,63 @@ class AgentSession {
287
552
  }
288
553
  return batches;
289
554
  }
290
- async appendCurrentMessages(...messages) {
555
+ async appendCurrentMessages(messages, options) {
291
556
  await this.ensureInitialized();
292
557
  if (!this.runtimeState.currentEntry || !this.runtimeState.currentEntry.messages?.length) {
293
558
  throw new Error("No current entry to append messages. Call startMessage() first.");
294
559
  }
295
- this.runtimeState.currentEntry.messages.push(...messages);
560
+ this.runtimeState.currentEntry.messages.push(...[messages].flat());
561
+ await this.maybeCompactCurrentEntry(options);
562
+ }
563
+ /**
564
+ * Truncate text content to fit within target token limit
565
+ * @param text The text to truncate
566
+ * @param currentTokens Current token count of the text
567
+ * @param targetTokens Target token count after truncation
568
+ * @returns Truncated text
569
+ */
570
+ truncateText(text, currentTokens, targetTokens) {
571
+ if (currentTokens <= targetTokens)
572
+ return text;
573
+ const keepRatio = (targetTokens / currentTokens) * 0.9;
574
+ const keepLength = Math.floor(text.length * keepRatio);
575
+ const headLength = Math.floor(keepLength * 0.7);
576
+ const tailLength = Math.floor(keepLength * 0.3);
577
+ return (text.slice(0, headLength) +
578
+ `\n\n[... truncated ${currentTokens - targetTokens} tokens ...]\n\n` +
579
+ text.slice(-tailLength));
580
+ }
581
+ truncateLargeMessage(msg) {
582
+ const singleMessageLimit = this.singleMessageLimit;
583
+ // Handle string content
584
+ if (typeof msg.content === "string") {
585
+ const tokens = (0, token_estimator_js_1.estimateTokens)(msg.content);
586
+ if (tokens <= singleMessageLimit)
587
+ return msg;
588
+ const truncated = this.truncateText(msg.content, tokens, singleMessageLimit);
589
+ return { ...msg, content: truncated };
590
+ }
591
+ // Handle array content (UnionContent[])
592
+ if (Array.isArray(msg.content)) {
593
+ // Truncate each text block individually if it exceeds the limit
594
+ const truncatedContent = msg.content.map((block) => {
595
+ // Keep non-text blocks unchanged
596
+ if (block.type !== "text" || typeof block.text !== "string") {
597
+ return block;
598
+ }
599
+ // Check if this text block needs truncation
600
+ const blockTokens = (0, token_estimator_js_1.estimateTokens)(block.text);
601
+ if (blockTokens <= singleMessageLimit) {
602
+ return block;
603
+ }
604
+ // Truncate this text block independently
605
+ const truncatedText = this.truncateText(block.text, blockTokens, singleMessageLimit);
606
+ return { ...block, text: truncatedText };
607
+ });
608
+ return { ...msg, content: truncatedContent };
609
+ }
610
+ // Unknown content type, return as-is
611
+ return msg;
296
612
  }
297
613
  async ensureInitialized() {
298
614
  this.initialized ??= this.initialize();
@@ -302,40 +618,391 @@ class AgentSession {
302
618
  if (this.initialized)
303
619
  return;
304
620
  await this.initializeDefaultCompactor();
621
+ await this.initializeDefaultSessionMemoryExtractor();
622
+ await this.initializeDefaultUserMemoryExtractor();
305
623
  const historyModule = (await this.afs?.listModules())?.find((m) => m.module instanceof afs_history_1.AFSHistory);
306
624
  this.historyModulePath = historyModule?.path;
307
625
  if (this.afs && this.historyModulePath) {
308
- // Load latest compact entry if exists
309
- const compactPath = (0, ufo_1.joinURL)(this.historyModulePath, "by-session", this.sessionId, "@metadata/compact");
310
- const compactResult = await this.afs.list(compactPath, {
311
- filter: { userId: this.userId, agentId: this.agentId },
312
- orderBy: [["createdAt", "desc"]],
313
- 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);
314
733
  });
315
- const latestCompact = compactResult.data[0];
316
- if (latestCompact?.content?.summary) {
317
- 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
+ }
318
939
  }
319
- // Load history entries (after compact point if exists)
320
- const afsEntries = (await this.afs.list((0, ufo_1.joinURL)(this.historyModulePath, "by-session", this.sessionId), {
321
- filter: {
322
- userId: this.userId,
323
- agentId: this.agentId,
324
- // Only load entries after the latest compact
325
- after: latestCompact?.createdAt?.toISOString(),
326
- },
327
- orderBy: [["createdAt", "desc"]],
328
- // Set a very large limit to load all history entries
329
- // The default limit is 10 which would cause history truncation
330
- limit: 10000,
331
- })).data;
332
- this.runtimeState.historyEntries = afsEntries
333
- .reverse()
334
- .filter((entry) => (0, type_utils_js_1.isNonNullable)(entry.content));
335
940
  }
336
941
  }
942
+ /**
943
+ * Find Agent Skill content from a single message
944
+ * @param msg - Message to search in
945
+ * @returns The skill content text if found, undefined otherwise
946
+ */
947
+ findSkillContentInMessage(msg) {
948
+ if (msg.role === "user" && Array.isArray(msg.content)) {
949
+ const skillBlock = msg.content.find((block) => block.type === "text" && block.isAgentSkill === true);
950
+ if (skillBlock && skillBlock.type === "text") {
951
+ return skillBlock.text;
952
+ }
953
+ }
954
+ return undefined;
955
+ }
956
+ /**
957
+ * Find the last Agent Skill from a list of messages
958
+ * @param messages - Messages to search through
959
+ * @returns The last Agent Skill found, or undefined if none found
960
+ */
961
+ findLastAgentSkillFromMessages(messages) {
962
+ // Search backwards through messages to find the last Agent Skill
963
+ for (let i = messages.length - 1; i >= 0; i--) {
964
+ const msg = messages[i];
965
+ if (!msg)
966
+ continue;
967
+ const skillContent = this.findSkillContentInMessage(msg);
968
+ if (skillContent) {
969
+ return {
970
+ content: skillContent,
971
+ };
972
+ }
973
+ }
974
+ return undefined;
975
+ }
976
+ /**
977
+ * Find the last Agent Skill from a list of history entries
978
+ * @param entries - History entries to search through
979
+ * @returns The last Agent Skill found, or undefined if none found
980
+ */
981
+ findLastAgentSkill(entries) {
982
+ // Flatten all messages from entries
983
+ const allMessages = entries.flatMap((entry) => entry.content?.messages ?? []);
984
+ return this.findLastAgentSkillFromMessages(allMessages);
985
+ }
337
986
  async initializeDefaultCompactor() {
338
987
  this.compactConfig.compactor ??= await Promise.resolve().then(() => __importStar(require("./compact/compactor.js"))).then((m) => new m.AISessionCompactor());
339
988
  }
989
+ async initializeDefaultSessionMemoryExtractor() {
990
+ this.sessionMemoryConfig.extractor ??= await Promise.resolve().then(() => __importStar(require("./compact/session-memory-extractor.js"))).then((m) => new m.AISessionMemoryExtractor());
991
+ }
992
+ async initializeDefaultUserMemoryExtractor() {
993
+ this.userMemoryConfig.extractor ??= await Promise.resolve().then(() => __importStar(require("./compact/user-memory-extractor.js"))).then((m) => new m.AIUserMemoryExtractor());
994
+ }
995
+ get maxTokens() {
996
+ return this.compactConfig?.maxTokens ?? types_js_1.DEFAULT_MAX_TOKENS;
997
+ }
998
+ get keepRecentRatio() {
999
+ return this.compactConfig?.keepRecentRatio ?? types_js_1.DEFAULT_KEEP_RECENT_RATIO;
1000
+ }
1001
+ get keepRecentTokens() {
1002
+ return Math.floor(this.maxTokens * this.keepRecentRatio);
1003
+ }
1004
+ get singleMessageLimit() {
1005
+ return this.keepRecentTokens * 0.5;
1006
+ }
340
1007
  }
341
1008
  exports.AgentSession = AgentSession;