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