@librechat/agents 3.7.0 → 3.7.1
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/dist/cjs/agents/AgentContext.cjs +11 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +7 -2
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +56 -9
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/main.cjs +11 -1
- package/dist/cjs/messages/content.cjs +8 -5
- package/dist/cjs/messages/content.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +15 -5
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/index.cjs +2 -1
- package/dist/cjs/messages/projectionInvariant.cjs +74 -0
- package/dist/cjs/messages/projectionInvariant.cjs.map +1 -0
- package/dist/cjs/messages/provenance.cjs +30 -8
- package/dist/cjs/messages/provenance.cjs.map +1 -1
- package/dist/cjs/messages/recency.cjs +202 -7
- package/dist/cjs/messages/recency.cjs.map +1 -1
- package/dist/cjs/messages/toolResultTypes.cjs +1 -0
- package/dist/cjs/session/AgentSession.cjs +4 -23
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/session/deriveMessages.cjs +25 -0
- package/dist/cjs/session/deriveMessages.cjs.map +1 -0
- package/dist/cjs/session/index.cjs +1 -0
- package/dist/cjs/summarization/node.cjs +13 -6
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/subagent/InMemorySubagentTaskStore.cjs +65 -8
- package/dist/cjs/tools/subagent/InMemorySubagentTaskStore.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +11 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +8 -3
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +56 -9
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/main.mjs +6 -4
- package/dist/esm/messages/content.mjs +8 -6
- package/dist/esm/messages/content.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +16 -6
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/index.mjs +2 -1
- package/dist/esm/messages/projectionInvariant.mjs +72 -0
- package/dist/esm/messages/projectionInvariant.mjs.map +1 -0
- package/dist/esm/messages/provenance.mjs +30 -9
- package/dist/esm/messages/provenance.mjs.map +1 -1
- package/dist/esm/messages/recency.mjs +201 -8
- package/dist/esm/messages/recency.mjs.map +1 -1
- package/dist/esm/messages/toolResultTypes.mjs +1 -1
- package/dist/esm/session/AgentSession.mjs +4 -23
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/session/deriveMessages.mjs +25 -0
- package/dist/esm/session/deriveMessages.mjs.map +1 -0
- package/dist/esm/session/index.mjs +1 -0
- package/dist/esm/summarization/node.mjs +14 -7
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/subagent/InMemorySubagentTaskStore.mjs +65 -8
- package/dist/esm/tools/subagent/InMemorySubagentTaskStore.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +6 -1
- package/dist/types/messages/content.d.ts +4 -1
- package/dist/types/messages/format.d.ts +6 -0
- package/dist/types/messages/index.d.ts +1 -0
- package/dist/types/messages/projectionInvariant.d.ts +25 -0
- package/dist/types/messages/provenance.d.ts +10 -0
- package/dist/types/messages/recency.d.ts +30 -18
- package/dist/types/session/deriveMessages.d.ts +11 -0
- package/dist/types/session/index.d.ts +2 -0
- package/dist/types/tools/subagent/InMemorySubagentTaskStore.d.ts +13 -1
- package/dist/types/types/graph.d.ts +1 -1
- package/dist/types/types/subagentTasks.d.ts +22 -0
- package/dist/types/types/summarize.d.ts +15 -13
- package/package.json +3 -1
- package/src/agents/AgentContext.ts +19 -5
- package/src/graphs/Graph.ts +9 -1
- package/src/llm/invoke.ts +102 -23
- package/src/messages/content.ts +20 -10
- package/src/messages/format.ts +29 -5
- package/src/messages/index.ts +1 -0
- package/src/messages/projectionInvariant.ts +134 -0
- package/src/messages/provenance.ts +60 -18
- package/src/messages/recency.ts +429 -27
- package/src/session/AgentSession.ts +4 -30
- package/src/session/deriveMessages.ts +37 -0
- package/src/session/index.ts +2 -0
- package/src/summarization/node.ts +37 -15
- package/src/tools/subagent/InMemorySubagentTaskStore.ts +139 -6
- package/src/types/graph.ts +1 -0
- package/src/types/subagentTasks.ts +28 -0
- package/src/types/summarize.ts +15 -13
package/src/messages/recency.ts
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AIMessage,
|
|
3
|
+
BaseMessage,
|
|
4
|
+
ToolMessage,
|
|
5
|
+
} from '@langchain/core/messages';
|
|
6
|
+
import type {
|
|
7
|
+
ProviderToolCallIndex,
|
|
8
|
+
ProviderToolCallPartDescriptor,
|
|
9
|
+
ProviderToolResultPartDescriptor,
|
|
10
|
+
} from './toolResultTypes';
|
|
11
|
+
import { getProviderSourceMessageIds } from './provenance';
|
|
12
|
+
import {
|
|
13
|
+
appendProviderToolCallDescriptor,
|
|
14
|
+
consumeProviderToolResultPair,
|
|
15
|
+
getBoundedProviderPairingArray,
|
|
16
|
+
getBoundedProviderPairingArrayProperty,
|
|
17
|
+
getProviderAIMessageToolCallDescriptor,
|
|
18
|
+
getProviderToolCallPartDescriptor,
|
|
19
|
+
getProviderToolResultPartDescriptor,
|
|
20
|
+
PROVIDER_TOOL_PAIRING_MAX_IDENTIFIER_CHARS,
|
|
21
|
+
} from './toolResultTypes';
|
|
2
22
|
|
|
3
23
|
export const DEFAULT_RETAIN_RECENT_TURNS = 2;
|
|
24
|
+
export const DEFAULT_INTRA_TURN_RETAIN_RATIO = 0.16;
|
|
4
25
|
|
|
5
26
|
/**
|
|
6
27
|
* Configuration for splitting a message list into a head (to be summarized)
|
|
@@ -8,15 +29,15 @@ export const DEFAULT_RETAIN_RECENT_TURNS = 2;
|
|
|
8
29
|
*/
|
|
9
30
|
export interface RecencyWindowOptions {
|
|
10
31
|
/**
|
|
11
|
-
* Maximum number of recent user-led turns to keep in the tail.
|
|
12
|
-
* begins at a HumanMessage and includes every following
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
32
|
+
* Maximum number of recent user-led turns to keep in the tail. A "turn"
|
|
33
|
+
* begins at a user-authored HumanMessage and includes every following
|
|
34
|
+
* AIMessage and tool result up to the next user-authored HumanMessage.
|
|
35
|
+
* Provider-native HumanMessages containing only tool results remain in the
|
|
36
|
+
* current turn, so the boundary cannot split them from their calls.
|
|
16
37
|
*
|
|
17
|
-
* The most recent turn is
|
|
18
|
-
*
|
|
19
|
-
*
|
|
38
|
+
* The most recent turn is preserved unless `intraTurnTokens` enables the
|
|
39
|
+
* pairing-balanced fallback for a tool-heavy history. A lone oversized user
|
|
40
|
+
* message is never eligible for that fallback.
|
|
20
41
|
*
|
|
21
42
|
* Defaults to `2`. A value of `0` disables the recency window (head =
|
|
22
43
|
* everything, tail = empty), restoring the pre-recency-window behavior.
|
|
@@ -34,31 +55,378 @@ export interface RecencyWindowOptions {
|
|
|
34
55
|
tokens?: number;
|
|
35
56
|
/** Token-counter used to evaluate the optional `tokens` cap. */
|
|
36
57
|
tokenCounter?: (m: BaseMessage) => number;
|
|
58
|
+
/**
|
|
59
|
+
* Minimum token budget to retain when the turn window would otherwise make
|
|
60
|
+
* the whole history indivisible. When set with `tokenCounter`, older closed
|
|
61
|
+
* tool-call/result units may be summarized from within the earliest retained
|
|
62
|
+
* turn. A lone user payload and open tool units remain indivisible.
|
|
63
|
+
*/
|
|
64
|
+
intraTurnTokens?: number;
|
|
37
65
|
}
|
|
38
66
|
|
|
39
67
|
export interface RecencySplit {
|
|
40
68
|
/** Older messages eligible for summarization. Empty when nothing to summarize. */
|
|
41
69
|
head: BaseMessage[];
|
|
42
|
-
/** Recent messages preserved verbatim
|
|
70
|
+
/** Recent messages preserved verbatim, beginning at a pairing-balanced boundary. */
|
|
43
71
|
tail: BaseMessage[];
|
|
44
72
|
/** Number of user-led turns retained in the tail (0 if no HumanMessage exists). */
|
|
45
73
|
tailTurnCount: number;
|
|
46
74
|
/** Index in the original `messages` array where the tail begins. */
|
|
47
75
|
tailStartIndex: number;
|
|
76
|
+
/** True when the pairing-balanced fallback selected the boundary. */
|
|
77
|
+
usedIntraTurnFallback: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveIntraTurnRetainTokens({
|
|
81
|
+
tokens,
|
|
82
|
+
maxContextTokens,
|
|
83
|
+
}: {
|
|
84
|
+
tokens?: number;
|
|
85
|
+
maxContextTokens?: number;
|
|
86
|
+
}): number | undefined {
|
|
87
|
+
if (tokens != null) {
|
|
88
|
+
return Number.isFinite(tokens) && tokens > 0
|
|
89
|
+
? Math.floor(tokens)
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
maxContextTokens == null ||
|
|
94
|
+
!Number.isFinite(maxContextTokens) ||
|
|
95
|
+
maxContextTokens <= 0
|
|
96
|
+
) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return Math.max(
|
|
100
|
+
1,
|
|
101
|
+
Math.floor(maxContextTokens * DEFAULT_INTRA_TURN_RETAIN_RATIO)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readOwnString(value: unknown, key: string): string | undefined {
|
|
106
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
111
|
+
return descriptor != null &&
|
|
112
|
+
'value' in descriptor &&
|
|
113
|
+
typeof descriptor.value === 'string' &&
|
|
114
|
+
descriptor.value !== '' &&
|
|
115
|
+
descriptor.value.length <= PROVIDER_TOOL_PAIRING_MAX_IDENTIFIER_CHARS
|
|
116
|
+
? descriptor.value
|
|
117
|
+
: undefined;
|
|
118
|
+
} catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readOwnValue(value: unknown, key: string): unknown {
|
|
124
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
129
|
+
return descriptor != null && 'value' in descriptor
|
|
130
|
+
? descriptor.value
|
|
131
|
+
: undefined;
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const LEGACY_FUNCTION_CALL_PREFIX = 'legacy_function:';
|
|
138
|
+
|
|
139
|
+
function getLegacyFunctionCallId(name: string): string | undefined {
|
|
140
|
+
const callId = `${LEGACY_FUNCTION_CALL_PREFIX}${name}`;
|
|
141
|
+
return callId.length <= PROVIDER_TOOL_PAIRING_MAX_IDENTIFIER_CHARS
|
|
142
|
+
? callId
|
|
143
|
+
: undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getRawToolCallDescriptor(
|
|
147
|
+
toolCall: unknown
|
|
148
|
+
): ProviderToolCallPartDescriptor | undefined {
|
|
149
|
+
const callId = readOwnString(toolCall, 'id');
|
|
150
|
+
if (callId == null) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
let name: string | undefined;
|
|
154
|
+
if (toolCall != null && typeof toolCall === 'object') {
|
|
155
|
+
try {
|
|
156
|
+
const property = Object.getOwnPropertyDescriptor(toolCall, 'function');
|
|
157
|
+
name =
|
|
158
|
+
property != null && 'value' in property
|
|
159
|
+
? readOwnString(property.value, 'name')
|
|
160
|
+
: undefined;
|
|
161
|
+
} catch {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
callId,
|
|
167
|
+
kind: 'tool',
|
|
168
|
+
sourceType: 'raw_tool_calls',
|
|
169
|
+
...(name == null ? {} : { name }),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function appendMessageToolCalls(
|
|
174
|
+
message: BaseMessage,
|
|
175
|
+
calls: ProviderToolCallIndex
|
|
176
|
+
): void {
|
|
177
|
+
const messageRole = (message as BaseMessage & { role?: unknown }).role;
|
|
178
|
+
if (message.getType() !== 'ai' && messageRole !== 'assistant') {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const toolCall of (message as AIMessage).tool_calls ?? []) {
|
|
183
|
+
const descriptor = getProviderAIMessageToolCallDescriptor(toolCall);
|
|
184
|
+
if (descriptor != null) {
|
|
185
|
+
appendProviderToolCallDescriptor(calls, descriptor);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const rawToolCalls = getBoundedProviderPairingArrayProperty(
|
|
190
|
+
message.additional_kwargs,
|
|
191
|
+
'tool_calls'
|
|
192
|
+
);
|
|
193
|
+
if (rawToolCalls != null) {
|
|
194
|
+
for (const toolCall of rawToolCalls) {
|
|
195
|
+
const descriptor = getRawToolCallDescriptor(toolCall);
|
|
196
|
+
if (descriptor != null) {
|
|
197
|
+
appendProviderToolCallDescriptor(calls, descriptor);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const legacyFunctionCall = readOwnValue(
|
|
203
|
+
message.additional_kwargs,
|
|
204
|
+
'function_call'
|
|
205
|
+
);
|
|
206
|
+
const legacyFunctionName = readOwnString(legacyFunctionCall, 'name');
|
|
207
|
+
const legacyFunctionCallId =
|
|
208
|
+
legacyFunctionName == null
|
|
209
|
+
? undefined
|
|
210
|
+
: getLegacyFunctionCallId(legacyFunctionName);
|
|
211
|
+
if (legacyFunctionCallId != null) {
|
|
212
|
+
appendProviderToolCallDescriptor(calls, {
|
|
213
|
+
callId: legacyFunctionCallId,
|
|
214
|
+
kind: 'tool',
|
|
215
|
+
name: legacyFunctionName,
|
|
216
|
+
sourceType: 'legacy_function_call',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getToolMessageResultDescriptor(
|
|
222
|
+
message: BaseMessage
|
|
223
|
+
): ProviderToolResultPartDescriptor | undefined {
|
|
224
|
+
if (message.getType() === 'function' && message.name != null) {
|
|
225
|
+
const toolCallId = getLegacyFunctionCallId(message.name);
|
|
226
|
+
return toolCallId == null
|
|
227
|
+
? undefined
|
|
228
|
+
: {
|
|
229
|
+
type: 'function_message',
|
|
230
|
+
toolCallId,
|
|
231
|
+
compatibleCallKinds: ['tool'],
|
|
232
|
+
expectedToolNames: [message.name],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (message.getType() !== 'tool') {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
const toolMessage = message as ToolMessage & { toolCallId?: unknown };
|
|
239
|
+
const toolCallId =
|
|
240
|
+
typeof toolMessage.tool_call_id === 'string'
|
|
241
|
+
? toolMessage.tool_call_id
|
|
242
|
+
: toolMessage.toolCallId;
|
|
243
|
+
return typeof toolCallId === 'string' &&
|
|
244
|
+
toolCallId !== '' &&
|
|
245
|
+
toolCallId.length <= PROVIDER_TOOL_PAIRING_MAX_IDENTIFIER_CHARS
|
|
246
|
+
? {
|
|
247
|
+
type: 'tool_message',
|
|
248
|
+
toolCallId,
|
|
249
|
+
compatibleCallKinds: ['tool'],
|
|
250
|
+
}
|
|
251
|
+
: undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interface MessagePairingResult {
|
|
255
|
+
completedToolCalls: number;
|
|
256
|
+
trustedHumanToolResult: boolean;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function inspectMessagePairing(
|
|
260
|
+
message: BaseMessage,
|
|
261
|
+
calls: ProviderToolCallIndex
|
|
262
|
+
): MessagePairingResult {
|
|
263
|
+
const isHuman = message.getType() === 'human';
|
|
264
|
+
const candidateCalls = isHuman ? new Map(calls) : calls;
|
|
265
|
+
if (!isHuman) {
|
|
266
|
+
appendMessageToolCalls(message, candidateCalls);
|
|
267
|
+
}
|
|
268
|
+
const content = getBoundedProviderPairingArray(message.content);
|
|
269
|
+
let completedToolCalls = 0;
|
|
270
|
+
let resultPartCount = 0;
|
|
271
|
+
let everyPartIsTrustedHumanResult = isHuman && content != null;
|
|
272
|
+
if (content != null) {
|
|
273
|
+
let previousPart: unknown;
|
|
274
|
+
for (const part of content) {
|
|
275
|
+
const callDescriptor = getProviderToolCallPartDescriptor(part);
|
|
276
|
+
if (callDescriptor != null && !isHuman) {
|
|
277
|
+
appendProviderToolCallDescriptor(candidateCalls, callDescriptor);
|
|
278
|
+
}
|
|
279
|
+
const resultDescriptor = getProviderToolResultPartDescriptor(part);
|
|
280
|
+
if (resultDescriptor == null) {
|
|
281
|
+
everyPartIsTrustedHumanResult = false;
|
|
282
|
+
} else {
|
|
283
|
+
resultPartCount += 1;
|
|
284
|
+
const canPairHumanResult =
|
|
285
|
+
!isHuman || resultDescriptor.allowHumanMessagePairing === true;
|
|
286
|
+
if (
|
|
287
|
+
canPairHumanResult &&
|
|
288
|
+
consumeProviderToolResultPair(
|
|
289
|
+
resultDescriptor,
|
|
290
|
+
candidateCalls,
|
|
291
|
+
previousPart
|
|
292
|
+
)
|
|
293
|
+
) {
|
|
294
|
+
completedToolCalls += 1;
|
|
295
|
+
} else {
|
|
296
|
+
everyPartIsTrustedHumanResult = false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
previousPart = part;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const toolMessageResult = getToolMessageResultDescriptor(message);
|
|
304
|
+
if (
|
|
305
|
+
toolMessageResult != null &&
|
|
306
|
+
consumeProviderToolResultPair(toolMessageResult, candidateCalls)
|
|
307
|
+
) {
|
|
308
|
+
completedToolCalls += 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const trustedHumanToolResult =
|
|
312
|
+
everyPartIsTrustedHumanResult &&
|
|
313
|
+
resultPartCount > 0 &&
|
|
314
|
+
resultPartCount === content?.length;
|
|
315
|
+
if (trustedHumanToolResult) {
|
|
316
|
+
calls.clear();
|
|
317
|
+
for (const [callId, entry] of candidateCalls) {
|
|
318
|
+
calls.set(callId, entry);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
completedToolCalls: isHuman && !trustedHumanToolResult
|
|
323
|
+
? 0
|
|
324
|
+
: completedToolCalls,
|
|
325
|
+
trustedHumanToolResult,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function findTurnStarts(messages: BaseMessage[]): number[] {
|
|
330
|
+
const turnStarts: number[] = [];
|
|
331
|
+
const calls: ProviderToolCallIndex = new Map();
|
|
332
|
+
for (let i = 0; i < messages.length; i++) {
|
|
333
|
+
const message = messages[i] as BaseMessage;
|
|
334
|
+
const pairing = inspectMessagePairing(message, calls);
|
|
335
|
+
if (message.getType() !== 'human' || pairing.trustedHumanToolResult) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
turnStarts.push(i);
|
|
339
|
+
calls.clear();
|
|
340
|
+
}
|
|
341
|
+
return turnStarts;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function getCoverageSourceIds(message: BaseMessage): string[] {
|
|
345
|
+
const sourceIds = getProviderSourceMessageIds(message);
|
|
346
|
+
if (sourceIds.length > 0) {
|
|
347
|
+
return sourceIds;
|
|
348
|
+
}
|
|
349
|
+
const messageId = message.id?.trim();
|
|
350
|
+
return messageId != null && messageId !== '' ? [messageId] : [];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function findIntraTurnBoundary(
|
|
354
|
+
messages: BaseMessage[],
|
|
355
|
+
countMessageAt: (index: number) => number,
|
|
356
|
+
retainTokens: number,
|
|
357
|
+
earliestRetainedTurnEndIndex: number
|
|
358
|
+
): number | undefined {
|
|
359
|
+
let remainingTokens = 0;
|
|
360
|
+
for (let i = 0; i < messages.length; i++) {
|
|
361
|
+
remainingTokens += countMessageAt(i);
|
|
362
|
+
}
|
|
363
|
+
if (remainingTokens <= retainTokens) {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const sourceIdsByIndex = new Array<string[]>(messages.length);
|
|
368
|
+
const lastIndexBySourceId = new Map<string, number>();
|
|
369
|
+
for (let i = 0; i < messages.length; i++) {
|
|
370
|
+
const sourceIds = getCoverageSourceIds(messages[i] as BaseMessage);
|
|
371
|
+
sourceIdsByIndex[i] = sourceIds;
|
|
372
|
+
for (const sourceId of sourceIds) {
|
|
373
|
+
lastIndexBySourceId.set(sourceId, i);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const pendingToolCalls: ProviderToolCallIndex = new Map();
|
|
378
|
+
const straddlingSourceIds = new Set<string>();
|
|
379
|
+
let completedToolCalls = 0;
|
|
380
|
+
let boundary: number | undefined;
|
|
381
|
+
|
|
382
|
+
const boundarySearchEnd = Math.min(
|
|
383
|
+
messages.length - 1,
|
|
384
|
+
earliestRetainedTurnEndIndex - 1
|
|
385
|
+
);
|
|
386
|
+
for (let i = 0; i <= boundarySearchEnd; i++) {
|
|
387
|
+
const message = messages[i] as BaseMessage;
|
|
388
|
+
remainingTokens -= countMessageAt(i);
|
|
389
|
+
|
|
390
|
+
const pairing = inspectMessagePairing(message, pendingToolCalls);
|
|
391
|
+
completedToolCalls += pairing.completedToolCalls;
|
|
392
|
+
if (message.getType() === 'human' && !pairing.trustedHumanToolResult) {
|
|
393
|
+
pendingToolCalls.clear();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
for (const sourceId of sourceIdsByIndex[i] as string[]) {
|
|
397
|
+
if (lastIndexBySourceId.get(sourceId) === i) {
|
|
398
|
+
straddlingSourceIds.delete(sourceId);
|
|
399
|
+
} else {
|
|
400
|
+
straddlingSourceIds.add(sourceId);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (remainingTokens < retainTokens) {
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
if (
|
|
408
|
+
completedToolCalls > 0 &&
|
|
409
|
+
pendingToolCalls.size === 0 &&
|
|
410
|
+
straddlingSourceIds.size === 0 &&
|
|
411
|
+
(message.getType() !== 'human' || pairing.trustedHumanToolResult)
|
|
412
|
+
) {
|
|
413
|
+
boundary = i + 1;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return boundary;
|
|
48
418
|
}
|
|
49
419
|
|
|
50
420
|
/**
|
|
51
421
|
* Splits `messages` into a head (older, to summarize) and a tail (recent,
|
|
52
|
-
* to preserve verbatim)
|
|
53
|
-
* user-led turn is
|
|
54
|
-
*
|
|
422
|
+
* to preserve verbatim), preferring user-message boundaries. The most recent
|
|
423
|
+
* user-led turn is normally included in the tail; additional older turns are
|
|
424
|
+
* added subject to `turns` and `tokens` caps.
|
|
55
425
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* the "first turn destruction" failure mode where a single large
|
|
61
|
-
* user-pasted payload would otherwise be replaced by a generic summary.
|
|
426
|
+
* When that policy exposes no compactable head, `intraTurnTokens` may select
|
|
427
|
+
* a boundary after older closed tool-call/result units. This keeps runaway
|
|
428
|
+
* first-turn tool loops compactable without splitting parallel calls from
|
|
429
|
+
* their results. A user payload without a completed tool unit stays intact.
|
|
62
430
|
*
|
|
63
431
|
* When `messages` contains no HumanMessage (degenerate state — e.g. system
|
|
64
432
|
* + assistant messages from a programmatic preamble), everything is
|
|
@@ -78,15 +446,11 @@ export function splitAtRecencyBoundary(
|
|
|
78
446
|
tail: [],
|
|
79
447
|
tailTurnCount: 0,
|
|
80
448
|
tailStartIndex: messages.length,
|
|
449
|
+
usedIntraTurnFallback: false,
|
|
81
450
|
};
|
|
82
451
|
}
|
|
83
452
|
|
|
84
|
-
const turnStarts
|
|
85
|
-
for (let i = 0; i < messages.length; i++) {
|
|
86
|
-
if (messages[i].getType() === 'human') {
|
|
87
|
-
turnStarts.push(i);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
453
|
+
const turnStarts = findTurnStarts(messages);
|
|
90
454
|
|
|
91
455
|
if (turnStarts.length === 0) {
|
|
92
456
|
return {
|
|
@@ -94,6 +458,7 @@ export function splitAtRecencyBoundary(
|
|
|
94
458
|
tail: [],
|
|
95
459
|
tailTurnCount: 0,
|
|
96
460
|
tailStartIndex: messages.length,
|
|
461
|
+
usedIntraTurnFallback: false,
|
|
97
462
|
};
|
|
98
463
|
}
|
|
99
464
|
|
|
@@ -105,6 +470,16 @@ export function splitAtRecencyBoundary(
|
|
|
105
470
|
const tokenCounter = options.tokenCounter;
|
|
106
471
|
const trackTokens =
|
|
107
472
|
tokensCap != null && Number.isFinite(tokensCap) && tokenCounter != null;
|
|
473
|
+
const messageTokens: Array<number | undefined> = new Array(messages.length);
|
|
474
|
+
const countMessageAt = (index: number): number => {
|
|
475
|
+
const cached = messageTokens[index];
|
|
476
|
+
if (cached != null) {
|
|
477
|
+
return cached;
|
|
478
|
+
}
|
|
479
|
+
const count = tokenCounter?.(messages[index] as BaseMessage) ?? 0;
|
|
480
|
+
messageTokens[index] = count;
|
|
481
|
+
return count;
|
|
482
|
+
};
|
|
108
483
|
|
|
109
484
|
/**
|
|
110
485
|
* Token-counting strategy: each candidate turn `t` spans the half-open
|
|
@@ -122,7 +497,7 @@ export function splitAtRecencyBoundary(
|
|
|
122
497
|
let tailTokens = 0;
|
|
123
498
|
if (trackTokens) {
|
|
124
499
|
for (let i = lastTurnStart; i < messages.length; i++) {
|
|
125
|
-
tailTokens +=
|
|
500
|
+
tailTokens += countMessageAt(i);
|
|
126
501
|
}
|
|
127
502
|
}
|
|
128
503
|
|
|
@@ -136,7 +511,7 @@ export function splitAtRecencyBoundary(
|
|
|
136
511
|
if (trackTokens) {
|
|
137
512
|
let turnTokens = 0;
|
|
138
513
|
for (let i = turnStart; i < turnEnd; i++) {
|
|
139
|
-
turnTokens +=
|
|
514
|
+
turnTokens += countMessageAt(i);
|
|
140
515
|
}
|
|
141
516
|
if (tailTokens + turnTokens > (tokensCap as number)) {
|
|
142
517
|
break;
|
|
@@ -148,10 +523,37 @@ export function splitAtRecencyBoundary(
|
|
|
148
523
|
tailTurnCount += 1;
|
|
149
524
|
}
|
|
150
525
|
|
|
526
|
+
if (tailStartIndex === turnStarts[0]) {
|
|
527
|
+
const intraTurnTokens = options.intraTurnTokens;
|
|
528
|
+
if (
|
|
529
|
+
intraTurnTokens != null &&
|
|
530
|
+
Number.isFinite(intraTurnTokens) &&
|
|
531
|
+
intraTurnTokens > 0 &&
|
|
532
|
+
tokenCounter != null
|
|
533
|
+
) {
|
|
534
|
+
const intraTurnBoundary = findIntraTurnBoundary(
|
|
535
|
+
messages,
|
|
536
|
+
countMessageAt,
|
|
537
|
+
intraTurnTokens,
|
|
538
|
+
turnStarts[1] ?? messages.length
|
|
539
|
+
);
|
|
540
|
+
if (intraTurnBoundary != null) {
|
|
541
|
+
return {
|
|
542
|
+
head: messages.slice(0, intraTurnBoundary),
|
|
543
|
+
tail: messages.slice(intraTurnBoundary),
|
|
544
|
+
tailTurnCount,
|
|
545
|
+
tailStartIndex: intraTurnBoundary,
|
|
546
|
+
usedIntraTurnFallback: true,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
151
552
|
return {
|
|
152
553
|
head: messages.slice(0, tailStartIndex),
|
|
153
554
|
tail: messages.slice(tailStartIndex),
|
|
154
555
|
tailTurnCount,
|
|
155
556
|
tailStartIndex,
|
|
557
|
+
usedIntraTurnFallback: false,
|
|
156
558
|
};
|
|
157
559
|
}
|
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
} from './types';
|
|
24
24
|
import type { HookRegistry } from '@/hooks';
|
|
25
25
|
import type * as t from '@/types';
|
|
26
|
-
import {
|
|
26
|
+
import { deriveMessages } from './deriveMessages';
|
|
27
27
|
import { createSummarizeNode } from '@/summarization/node';
|
|
28
28
|
import { resolveStreamLimits } from '@/llm/streamLimits';
|
|
29
29
|
import { JsonlSessionStore } from './JsonlSessionStore';
|
|
@@ -493,32 +493,6 @@ function createCheckpointReference(params: {
|
|
|
493
493
|
};
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
-
function createSessionRunState(entries: SessionEntry[]): {
|
|
497
|
-
messages: BaseMessage[];
|
|
498
|
-
initialSummary?: InitialSummary;
|
|
499
|
-
} {
|
|
500
|
-
const messages: BaseMessage[] = [];
|
|
501
|
-
let initialSummary: InitialSummary | undefined;
|
|
502
|
-
for (const entry of entries) {
|
|
503
|
-
if (entry.type === 'summary') {
|
|
504
|
-
initialSummary = {
|
|
505
|
-
text: entry.data.text,
|
|
506
|
-
tokenCount:
|
|
507
|
-
typeof entry.data.tokenCount === 'number' &&
|
|
508
|
-
Number.isFinite(entry.data.tokenCount)
|
|
509
|
-
? entry.data.tokenCount
|
|
510
|
-
: 0,
|
|
511
|
-
};
|
|
512
|
-
messages.length = 0;
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
if (entry.type === 'message') {
|
|
516
|
-
messages.push(deserializeMessage(entry.data.message));
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
return { messages, initialSummary };
|
|
520
|
-
}
|
|
521
|
-
|
|
522
496
|
function isMessageEntry(
|
|
523
497
|
entry: SessionEntry
|
|
524
498
|
): entry is Extract<SessionEntry, { type: 'message' }> {
|
|
@@ -1103,7 +1077,7 @@ export class AgentSession {
|
|
|
1103
1077
|
handlerResult.events.push(streamEvent);
|
|
1104
1078
|
onEvent?.(streamEvent);
|
|
1105
1079
|
};
|
|
1106
|
-
const sessionState =
|
|
1080
|
+
const sessionState = deriveMessages(
|
|
1107
1081
|
isSessionThread ? (this.store?.getPath() ?? []) : []
|
|
1108
1082
|
);
|
|
1109
1083
|
let run: Run<t.IState> | undefined;
|
|
@@ -1349,7 +1323,7 @@ export class AgentSession {
|
|
|
1349
1323
|
throw new Error('Cannot compact an ephemeral session');
|
|
1350
1324
|
}
|
|
1351
1325
|
const activePath = path ?? store.getPath();
|
|
1352
|
-
const sessionState =
|
|
1326
|
+
const sessionState = deriveMessages(activePath);
|
|
1353
1327
|
const messageEntries = activePath.filter(isMessageEntry);
|
|
1354
1328
|
if (sessionState.messages.length === 0) {
|
|
1355
1329
|
return undefined;
|
|
@@ -1474,7 +1448,7 @@ export class AgentSession {
|
|
|
1474
1448
|
threadId,
|
|
1475
1449
|
userHandlers: this.runConfig.customHandlers,
|
|
1476
1450
|
});
|
|
1477
|
-
const sessionState =
|
|
1451
|
+
const sessionState = deriveMessages(
|
|
1478
1452
|
isSessionThread ? (this.store?.getPath() ?? []) : []
|
|
1479
1453
|
);
|
|
1480
1454
|
let run: Run<t.IState> | undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
2
|
+
import type { SessionEntry } from './types';
|
|
3
|
+
import { deserializeMessage } from './messageSerialization';
|
|
4
|
+
|
|
5
|
+
export interface DerivedSessionMessages {
|
|
6
|
+
initialSummary?: {
|
|
7
|
+
text: string;
|
|
8
|
+
tokenCount: number;
|
|
9
|
+
};
|
|
10
|
+
messages: BaseMessage[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Derives the active model context from one append-only session-log path. */
|
|
14
|
+
export function deriveMessages(
|
|
15
|
+
entries: readonly SessionEntry[]
|
|
16
|
+
): DerivedSessionMessages {
|
|
17
|
+
const messages: BaseMessage[] = [];
|
|
18
|
+
let initialSummary: DerivedSessionMessages['initialSummary'];
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (entry.type === 'summary') {
|
|
21
|
+
initialSummary = {
|
|
22
|
+
text: entry.data.text,
|
|
23
|
+
tokenCount:
|
|
24
|
+
typeof entry.data.tokenCount === 'number' &&
|
|
25
|
+
Number.isFinite(entry.data.tokenCount)
|
|
26
|
+
? entry.data.tokenCount
|
|
27
|
+
: 0,
|
|
28
|
+
};
|
|
29
|
+
messages.length = 0;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (entry.type === 'message') {
|
|
33
|
+
messages.push(deserializeMessage(entry.data.message));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { messages, ...(initialSummary != null && { initialSummary }) };
|
|
37
|
+
}
|
package/src/session/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { AgentSession, createAgentSession } from './AgentSession';
|
|
2
|
+
export { deriveMessages } from './deriveMessages';
|
|
2
3
|
export { JsonlSessionStore, SessionManager } from './JsonlSessionStore';
|
|
3
4
|
export { createRunHandlers } from './handlers';
|
|
4
5
|
export {
|
|
@@ -42,3 +43,4 @@ export type {
|
|
|
42
43
|
SessionSummaryEntry,
|
|
43
44
|
SessionTreeNode,
|
|
44
45
|
} from './types';
|
|
46
|
+
export type { DerivedSessionMessages } from './deriveMessages';
|