@librechat/agents 3.1.77 → 3.1.78
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/common/enum.cjs +54 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +155 -4
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +291 -0
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -0
- package/dist/cjs/main.cjs +90 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/anthropicToolCache.cjs +102 -0
- package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -0
- package/dist/cjs/messages/prune.cjs +27 -0
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/messages/recency.cjs +99 -0
- package/dist/cjs/messages/recency.cjs.map +1 -0
- package/dist/cjs/run.cjs +30 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +100 -6
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +635 -23
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/local/CompileCheckTool.cjs +227 -0
- package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -0
- package/dist/cjs/tools/local/FileCheckpointer.cjs +90 -0
- package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalCodingTools.cjs +1098 -0
- package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +1042 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalExecutionTools.cjs +122 -0
- package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +453 -0
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/local/attachments.cjs +183 -0
- package/dist/cjs/tools/local/attachments.cjs.map +1 -0
- package/dist/cjs/tools/local/bashAst.cjs +129 -0
- package/dist/cjs/tools/local/bashAst.cjs.map +1 -0
- package/dist/cjs/tools/local/editStrategies.cjs +188 -0
- package/dist/cjs/tools/local/editStrategies.cjs.map +1 -0
- package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +141 -0
- package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -0
- package/dist/cjs/tools/local/syntaxCheck.cjs +182 -0
- package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -0
- package/dist/cjs/tools/local/textEncoding.cjs +30 -0
- package/dist/cjs/tools/local/textEncoding.cjs.map +1 -0
- package/dist/cjs/tools/local/workspaceFS.cjs +51 -0
- package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +31 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/esm/common/enum.mjs +53 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +156 -5
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +289 -0
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -0
- package/dist/esm/main.mjs +17 -2
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/anthropicToolCache.mjs +99 -0
- package/dist/esm/messages/anthropicToolCache.mjs.map +1 -0
- package/dist/esm/messages/prune.mjs +26 -1
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/messages/recency.mjs +97 -0
- package/dist/esm/messages/recency.mjs.map +1 -0
- package/dist/esm/run.mjs +30 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +100 -6
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +635 -23
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/local/CompileCheckTool.mjs +223 -0
- package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -0
- package/dist/esm/tools/local/FileCheckpointer.mjs +87 -0
- package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -0
- package/dist/esm/tools/local/LocalCodingTools.mjs +1075 -0
- package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -0
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +1022 -0
- package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -0
- package/dist/esm/tools/local/LocalExecutionTools.mjs +117 -0
- package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -0
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +448 -0
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/local/attachments.mjs +180 -0
- package/dist/esm/tools/local/attachments.mjs.map +1 -0
- package/dist/esm/tools/local/bashAst.mjs +126 -0
- package/dist/esm/tools/local/bashAst.mjs.map +1 -0
- package/dist/esm/tools/local/editStrategies.mjs +185 -0
- package/dist/esm/tools/local/editStrategies.mjs.map +1 -0
- package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +137 -0
- package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -0
- package/dist/esm/tools/local/syntaxCheck.mjs +179 -0
- package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -0
- package/dist/esm/tools/local/textEncoding.mjs +27 -0
- package/dist/esm/tools/local/textEncoding.mjs.map +1 -0
- package/dist/esm/tools/local/workspaceFS.mjs +49 -0
- package/dist/esm/tools/local/workspaceFS.mjs.map +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +31 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/types/common/enum.d.ts +39 -1
- package/dist/types/graphs/Graph.d.ts +34 -0
- package/dist/types/hooks/createWorkspacePolicyHook.d.ts +95 -0
- package/dist/types/hooks/index.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/messages/anthropicToolCache.d.ts +51 -0
- package/dist/types/messages/index.d.ts +2 -0
- package/dist/types/messages/prune.d.ts +11 -0
- package/dist/types/messages/recency.d.ts +64 -0
- package/dist/types/run.d.ts +21 -0
- package/dist/types/tools/ToolNode.d.ts +145 -2
- package/dist/types/tools/local/CompileCheckTool.d.ts +31 -0
- package/dist/types/tools/local/FileCheckpointer.d.ts +39 -0
- package/dist/types/tools/local/LocalCodingTools.d.ts +57 -0
- package/dist/types/tools/local/LocalExecutionEngine.d.ts +149 -0
- package/dist/types/tools/local/LocalExecutionTools.d.ts +9 -0
- package/dist/types/tools/local/LocalProgrammaticToolCalling.d.ts +21 -0
- package/dist/types/tools/local/attachments.d.ts +84 -0
- package/dist/types/tools/local/bashAst.d.ts +11 -0
- package/dist/types/tools/local/editStrategies.d.ts +28 -0
- package/dist/types/tools/local/index.d.ts +12 -0
- package/dist/types/tools/local/resolveLocalExecutionTools.d.ts +38 -0
- package/dist/types/tools/local/syntaxCheck.d.ts +42 -0
- package/dist/types/tools/local/textEncoding.d.ts +21 -0
- package/dist/types/tools/local/workspaceFS.d.ts +49 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +29 -0
- package/dist/types/types/hitl.d.ts +56 -27
- package/dist/types/types/run.d.ts +8 -1
- package/dist/types/types/summarize.d.ts +30 -0
- package/dist/types/types/tools.d.ts +341 -6
- package/package.json +21 -2
- package/src/common/enum.ts +54 -0
- package/src/graphs/Graph.ts +173 -6
- package/src/hooks/__tests__/compactHooks.test.ts +38 -2
- package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +393 -0
- package/src/hooks/createWorkspacePolicyHook.ts +355 -0
- package/src/hooks/index.ts +6 -0
- package/src/index.ts +1 -0
- package/src/messages/__tests__/anthropicToolCache.test.ts +125 -0
- package/src/messages/__tests__/recency.test.ts +267 -0
- package/src/messages/anthropicToolCache.ts +116 -0
- package/src/messages/index.ts +2 -0
- package/src/messages/prune.ts +27 -1
- package/src/messages/recency.ts +155 -0
- package/src/run.ts +31 -0
- package/src/scripts/compare_pi_vs_ours.ts +840 -0
- package/src/scripts/local_engine.ts +166 -0
- package/src/scripts/local_engine_checkpointer.ts +205 -0
- package/src/scripts/local_engine_compile.ts +263 -0
- package/src/scripts/local_engine_hooks.ts +226 -0
- package/src/scripts/local_engine_image.ts +201 -0
- package/src/scripts/local_engine_ptc.ts +151 -0
- package/src/scripts/local_engine_workspace.ts +258 -0
- package/src/scripts/subagent-configurable-inheritance.ts +252 -0
- package/src/scripts/summarization-recency.ts +462 -0
- package/src/specs/prune.test.ts +39 -0
- package/src/summarization/__tests__/node.test.ts +499 -3
- package/src/summarization/node.ts +124 -7
- package/src/tools/ToolNode.ts +769 -20
- package/src/tools/__tests__/LocalExecutionTools.test.ts +2647 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +175 -0
- package/src/tools/__tests__/SubagentExecutor.test.ts +148 -0
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +114 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +84 -0
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +467 -0
- package/src/tools/__tests__/directToolHooks.test.ts +411 -0
- package/src/tools/__tests__/localToolNames.test.ts +73 -0
- package/src/tools/__tests__/workspaceSeam.test.ts +134 -0
- package/src/tools/local/CompileCheckTool.ts +278 -0
- package/src/tools/local/FileCheckpointer.ts +93 -0
- package/src/tools/local/LocalCodingTools.ts +1342 -0
- package/src/tools/local/LocalExecutionEngine.ts +1329 -0
- package/src/tools/local/LocalExecutionTools.ts +167 -0
- package/src/tools/local/LocalProgrammaticToolCalling.ts +594 -0
- package/src/tools/local/__tests__/FileCheckpointer.test.ts +120 -0
- package/src/tools/local/__tests__/editStrategies.test.ts +134 -0
- package/src/tools/local/attachments.ts +251 -0
- package/src/tools/local/bashAst.ts +151 -0
- package/src/tools/local/editStrategies.ts +188 -0
- package/src/tools/local/index.ts +12 -0
- package/src/tools/local/resolveLocalExecutionTools.ts +208 -0
- package/src/tools/local/syntaxCheck.ts +243 -0
- package/src/tools/local/textEncoding.ts +37 -0
- package/src/tools/local/workspaceFS.ts +89 -0
- package/src/tools/subagent/SubagentExecutor.ts +60 -0
- package/src/types/hitl.ts +56 -27
- package/src/types/run.ts +12 -1
- package/src/types/summarize.ts +31 -0
- package/src/types/tools.ts +359 -7
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AIMessage,
|
|
3
|
+
HumanMessage,
|
|
4
|
+
ToolMessage,
|
|
5
|
+
SystemMessage,
|
|
6
|
+
type BaseMessage,
|
|
7
|
+
} from '@langchain/core/messages';
|
|
8
|
+
import { splitAtRecencyBoundary } from '@/messages/recency';
|
|
9
|
+
|
|
10
|
+
describe('splitAtRecencyBoundary', () => {
|
|
11
|
+
describe('default behavior (turns: 2)', () => {
|
|
12
|
+
it('returns empty head and full tail for an empty array', () => {
|
|
13
|
+
const result = splitAtRecencyBoundary([], { turns: 2 });
|
|
14
|
+
expect(result.head).toEqual([]);
|
|
15
|
+
expect(result.tail).toEqual([]);
|
|
16
|
+
expect(result.tailTurnCount).toBe(0);
|
|
17
|
+
expect(result.tailStartIndex).toBe(0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('always preserves the most recent turn even with one large message', () => {
|
|
21
|
+
const messages = [new HumanMessage('huge first message'.repeat(1000))];
|
|
22
|
+
const result = splitAtRecencyBoundary(messages, { turns: 2 });
|
|
23
|
+
expect(result.head).toEqual([]);
|
|
24
|
+
expect(result.tail).toEqual(messages);
|
|
25
|
+
expect(result.tailTurnCount).toBe(1);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('keeps a complete user-assistant exchange in the tail', () => {
|
|
29
|
+
const messages = [new HumanMessage('hi'), new AIMessage('hello')];
|
|
30
|
+
const result = splitAtRecencyBoundary(messages, { turns: 2 });
|
|
31
|
+
expect(result.head).toEqual([]);
|
|
32
|
+
expect(result.tail).toEqual(messages);
|
|
33
|
+
expect(result.tailTurnCount).toBe(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('places older turns in the head when there are more turns than the cap', () => {
|
|
37
|
+
const messages = [
|
|
38
|
+
new HumanMessage('turn 1'),
|
|
39
|
+
new AIMessage('reply 1'),
|
|
40
|
+
new HumanMessage('turn 2'),
|
|
41
|
+
new AIMessage('reply 2'),
|
|
42
|
+
new HumanMessage('turn 3'),
|
|
43
|
+
new AIMessage('reply 3'),
|
|
44
|
+
];
|
|
45
|
+
const result = splitAtRecencyBoundary(messages, { turns: 2 });
|
|
46
|
+
expect(result.head).toEqual(messages.slice(0, 2));
|
|
47
|
+
expect(result.tail).toEqual(messages.slice(2));
|
|
48
|
+
expect(result.tailTurnCount).toBe(2);
|
|
49
|
+
expect(result.tailStartIndex).toBe(2);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('preserves tool_use / tool_result pairs across the boundary', () => {
|
|
53
|
+
const messages = [
|
|
54
|
+
new HumanMessage('turn 1'),
|
|
55
|
+
new AIMessage({
|
|
56
|
+
content: '',
|
|
57
|
+
tool_calls: [{ id: 'call_a', name: 'search', args: {} }],
|
|
58
|
+
}),
|
|
59
|
+
new ToolMessage({
|
|
60
|
+
content: 'result A',
|
|
61
|
+
tool_call_id: 'call_a',
|
|
62
|
+
name: 'search',
|
|
63
|
+
}),
|
|
64
|
+
new AIMessage('done with turn 1'),
|
|
65
|
+
new HumanMessage('turn 2'),
|
|
66
|
+
new AIMessage({
|
|
67
|
+
content: '',
|
|
68
|
+
tool_calls: [{ id: 'call_b', name: 'search', args: {} }],
|
|
69
|
+
}),
|
|
70
|
+
new ToolMessage({
|
|
71
|
+
content: 'result B',
|
|
72
|
+
tool_call_id: 'call_b',
|
|
73
|
+
name: 'search',
|
|
74
|
+
}),
|
|
75
|
+
new AIMessage('done with turn 2'),
|
|
76
|
+
new HumanMessage('turn 3'),
|
|
77
|
+
new AIMessage('reply 3'),
|
|
78
|
+
];
|
|
79
|
+
const result = splitAtRecencyBoundary(messages, { turns: 2 });
|
|
80
|
+
// Head must contain turn 1's complete tool_use → tool_result pair.
|
|
81
|
+
expect(result.head).toHaveLength(4);
|
|
82
|
+
expect(result.head[0]).toBe(messages[0]);
|
|
83
|
+
expect(result.head[3]).toBe(messages[3]);
|
|
84
|
+
// Tail starts cleanly at turn 2's HumanMessage — never mid-pair.
|
|
85
|
+
expect(result.tail[0]).toBe(messages[4]);
|
|
86
|
+
expect(result.tail).toHaveLength(6);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('disabled (turns: 0)', () => {
|
|
91
|
+
it('puts everything in head when turns is 0', () => {
|
|
92
|
+
const messages = [
|
|
93
|
+
new HumanMessage('one'),
|
|
94
|
+
new AIMessage('two'),
|
|
95
|
+
new HumanMessage('three'),
|
|
96
|
+
];
|
|
97
|
+
const result = splitAtRecencyBoundary(messages, { turns: 0 });
|
|
98
|
+
expect(result.head).toEqual(messages);
|
|
99
|
+
expect(result.tail).toEqual([]);
|
|
100
|
+
expect(result.tailTurnCount).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('treats negative turns as 0', () => {
|
|
104
|
+
const messages = [new HumanMessage('a'), new AIMessage('b')];
|
|
105
|
+
const result = splitAtRecencyBoundary(messages, { turns: -5 });
|
|
106
|
+
expect(result.tail).toEqual([]);
|
|
107
|
+
expect(result.head).toEqual(messages);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('token cap', () => {
|
|
112
|
+
it('honors the token cap when adding older turns', () => {
|
|
113
|
+
const messages = [
|
|
114
|
+
new HumanMessage('turn 1'),
|
|
115
|
+
new AIMessage('reply 1'),
|
|
116
|
+
new HumanMessage('turn 2'),
|
|
117
|
+
new AIMessage('reply 2'),
|
|
118
|
+
new HumanMessage('turn 3'),
|
|
119
|
+
new AIMessage('reply 3'),
|
|
120
|
+
];
|
|
121
|
+
const tokenCounter = (): number => 100;
|
|
122
|
+
const result = splitAtRecencyBoundary(messages, {
|
|
123
|
+
turns: 5,
|
|
124
|
+
tokens: 250,
|
|
125
|
+
tokenCounter,
|
|
126
|
+
});
|
|
127
|
+
// Last turn is always preserved (200 tokens for 2 messages).
|
|
128
|
+
// Adding turn 2 would push to 400, exceeding cap of 250 → stop.
|
|
129
|
+
expect(result.tailTurnCount).toBe(1);
|
|
130
|
+
expect(result.tail).toEqual(messages.slice(4));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('always preserves the most recent turn even when it exceeds the cap', () => {
|
|
134
|
+
const messages = [new HumanMessage('huge'), new AIMessage('also huge')];
|
|
135
|
+
const tokenCounter = (): number => 1_000_000;
|
|
136
|
+
const result = splitAtRecencyBoundary(messages, {
|
|
137
|
+
turns: 2,
|
|
138
|
+
tokens: 10,
|
|
139
|
+
tokenCounter,
|
|
140
|
+
});
|
|
141
|
+
expect(result.head).toEqual([]);
|
|
142
|
+
expect(result.tail).toEqual(messages);
|
|
143
|
+
expect(result.tailTurnCount).toBe(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('ignores the token cap when no tokenCounter is provided', () => {
|
|
147
|
+
const messages = [
|
|
148
|
+
new HumanMessage('a'),
|
|
149
|
+
new AIMessage('b'),
|
|
150
|
+
new HumanMessage('c'),
|
|
151
|
+
new AIMessage('d'),
|
|
152
|
+
];
|
|
153
|
+
const result = splitAtRecencyBoundary(messages, {
|
|
154
|
+
turns: 3,
|
|
155
|
+
tokens: 1, // would force tail to most-recent-only if applied
|
|
156
|
+
});
|
|
157
|
+
// No tokenCounter → fall back to turn-based selection only.
|
|
158
|
+
expect(result.tailTurnCount).toBe(2);
|
|
159
|
+
expect(result.head).toEqual([]);
|
|
160
|
+
expect(result.tail).toEqual(messages);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('linearity', () => {
|
|
165
|
+
it('calls tokenCounter once per message in visited turns (no quadratic recount)', () => {
|
|
166
|
+
// Build a long history: 200 turns × 10 messages = 2,000 messages.
|
|
167
|
+
// If the boundary search were quadratic in the number of turns,
|
|
168
|
+
// the call count would explode (e.g., 200 × 2,000 = 400,000).
|
|
169
|
+
// The disjoint-slice invariant guarantees one call per visited
|
|
170
|
+
// message, bounded by messages.length even with a generous turn
|
|
171
|
+
// budget that visits every turn.
|
|
172
|
+
const messages: BaseMessage[] = [];
|
|
173
|
+
const turnCount = 200;
|
|
174
|
+
const messagesPerTurn = 10;
|
|
175
|
+
for (let t = 0; t < turnCount; t++) {
|
|
176
|
+
messages.push(new HumanMessage(`turn ${t} query`));
|
|
177
|
+
for (let m = 1; m < messagesPerTurn; m++) {
|
|
178
|
+
messages.push(new AIMessage(`turn ${t} reply ${m}`));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let calls = 0;
|
|
183
|
+
const tokenCounter = (): number => {
|
|
184
|
+
calls += 1;
|
|
185
|
+
return 1;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Generous tokens cap so the loop visits every turn.
|
|
189
|
+
// turnsCap also generous so the limit isn't hit early.
|
|
190
|
+
splitAtRecencyBoundary(messages, {
|
|
191
|
+
turns: 1_000_000,
|
|
192
|
+
tokens: 1_000_000,
|
|
193
|
+
tokenCounter,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Strictly bounded by messages.length. No message is counted
|
|
197
|
+
// twice, regardless of how many turns the splitter walks.
|
|
198
|
+
expect(calls).toBeLessThanOrEqual(messages.length);
|
|
199
|
+
expect(calls).toBe(messages.length);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('stops counting once the tokens cap is exceeded (no scan past the boundary)', () => {
|
|
203
|
+
const messages: BaseMessage[] = [];
|
|
204
|
+
for (let t = 0; t < 50; t++) {
|
|
205
|
+
messages.push(new HumanMessage(`turn ${t}`));
|
|
206
|
+
messages.push(new AIMessage(`reply ${t}`));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let calls = 0;
|
|
210
|
+
const tokenCounter = (): number => {
|
|
211
|
+
calls += 1;
|
|
212
|
+
return 1; // 1 token per message → 100 tokens total
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Cap of 10 tokens lets us include the last 5 turns (10 messages)
|
|
216
|
+
// before the next turn's 2 tokens would overflow.
|
|
217
|
+
const result = splitAtRecencyBoundary(messages, {
|
|
218
|
+
turns: 1_000,
|
|
219
|
+
tokens: 10,
|
|
220
|
+
tokenCounter,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Visited at most: 5 included turns × 2 messages + one over-budget
|
|
224
|
+
// turn × 2 messages (counted then rejected) = 12 messages. Far
|
|
225
|
+
// less than the full 100.
|
|
226
|
+
expect(calls).toBeLessThanOrEqual(12);
|
|
227
|
+
expect(result.tailTurnCount).toBe(5);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('degenerate inputs', () => {
|
|
232
|
+
it('puts everything in the head when there is no HumanMessage', () => {
|
|
233
|
+
const messages = [
|
|
234
|
+
new SystemMessage('preamble'),
|
|
235
|
+
new AIMessage('starter'),
|
|
236
|
+
];
|
|
237
|
+
const result = splitAtRecencyBoundary(messages, { turns: 2 });
|
|
238
|
+
expect(result.head).toEqual(messages);
|
|
239
|
+
expect(result.tail).toEqual([]);
|
|
240
|
+
expect(result.tailTurnCount).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('handles a HumanMessage at index 0 with prior non-human messages absent', () => {
|
|
244
|
+
const messages = [new HumanMessage('only')];
|
|
245
|
+
const result = splitAtRecencyBoundary(messages, { turns: 3 });
|
|
246
|
+
expect(result.head).toEqual([]);
|
|
247
|
+
expect(result.tail).toEqual(messages);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('handles tool messages as the very last messages', () => {
|
|
251
|
+
const messages = [
|
|
252
|
+
new HumanMessage('q1'),
|
|
253
|
+
new AIMessage('a1'),
|
|
254
|
+
new HumanMessage('q2'),
|
|
255
|
+
new AIMessage({
|
|
256
|
+
content: '',
|
|
257
|
+
tool_calls: [{ id: 'c1', name: 't', args: {} }],
|
|
258
|
+
}),
|
|
259
|
+
new ToolMessage({ content: 'r', tool_call_id: 'c1', name: 't' }),
|
|
260
|
+
];
|
|
261
|
+
const result = splitAtRecencyBoundary(messages, { turns: 1 });
|
|
262
|
+
// Most recent turn includes the trailing tool result.
|
|
263
|
+
expect(result.tail).toEqual(messages.slice(2));
|
|
264
|
+
expect(result.head).toEqual(messages.slice(0, 2));
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic prompt-caching helper for the `tools[]` request field.
|
|
3
|
+
*
|
|
4
|
+
* Anthropic accepts `cache_control: { type: 'ephemeral' }` on individual
|
|
5
|
+
* tool definitions. Whichever tool carries the marker becomes the end of
|
|
6
|
+
* a cached prefix: `tools[0..N]` (everything up to and including the
|
|
7
|
+
* marked tool) is cached and rebated on subsequent matching requests.
|
|
8
|
+
*
|
|
9
|
+
* For agents that mix static and deferred (lazily-discovered) tools, the
|
|
10
|
+
* winning configuration is:
|
|
11
|
+
*
|
|
12
|
+
* 1. Stable-partition tools so all *static* (non-deferred) tools come
|
|
13
|
+
* first and discovered-deferred tools come last.
|
|
14
|
+
* 2. Stamp `cache_control` on the LAST static tool.
|
|
15
|
+
*
|
|
16
|
+
* That way, the cached prefix covers exactly the static tool inventory.
|
|
17
|
+
* Discovered tools that show up later (or vary turn-to-turn as new ones
|
|
18
|
+
* get discovered) never invalidate the prefix because they sit *after*
|
|
19
|
+
* the breakpoint.
|
|
20
|
+
*
|
|
21
|
+
* LangChain's Anthropic adapter passes the marker through via
|
|
22
|
+
* `tool.extras.cache_control` (`AnthropicToolExtrasSchema`), so we set
|
|
23
|
+
* it as an `extras` field on a fresh wrapper around the tool — never
|
|
24
|
+
* mutating the original tool instance, since callers may share them
|
|
25
|
+
* across runs.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { GraphTools } from '@/types';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns a callable that reports whether a given tool *name* is deferred
|
|
32
|
+
* according to the supplied registry of tool definitions. Tools without
|
|
33
|
+
* a registry entry are treated as non-deferred (i.e. static), matching
|
|
34
|
+
* the host-supplied `graphTools` semantics elsewhere in the SDK.
|
|
35
|
+
*/
|
|
36
|
+
export function makeIsDeferred(
|
|
37
|
+
toolDefinitions:
|
|
38
|
+
| ReadonlyArray<{ name: string; defer_loading?: boolean }>
|
|
39
|
+
| undefined
|
|
40
|
+
): (toolName: string) => boolean {
|
|
41
|
+
if (toolDefinitions == null || toolDefinitions.length === 0) {
|
|
42
|
+
return () => false;
|
|
43
|
+
}
|
|
44
|
+
const deferred = new Set<string>();
|
|
45
|
+
for (const def of toolDefinitions) {
|
|
46
|
+
if (def.defer_loading === true) deferred.add(def.name);
|
|
47
|
+
}
|
|
48
|
+
if (deferred.size === 0) return () => false;
|
|
49
|
+
return (name) => deferred.has(name);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Stable-partition `tools` into [static..., deferred...] and stamp the
|
|
54
|
+
* Anthropic `cache_control: ephemeral` marker on the last static tool.
|
|
55
|
+
*
|
|
56
|
+
* If `tools` is undefined or empty, or no entry has a usable `.name`,
|
|
57
|
+
* returns the input unchanged. If there are no static tools at all,
|
|
58
|
+
* also returns unchanged (nothing to cache).
|
|
59
|
+
*
|
|
60
|
+
* The original tool instances are never mutated. The marked entry is a
|
|
61
|
+
* shallow wrapper that preserves the prototype chain so downstream
|
|
62
|
+
* `instanceof` checks still pass. `extras` is merged so any existing
|
|
63
|
+
* `providerToolDefinition` / other extras the host attached are kept.
|
|
64
|
+
*/
|
|
65
|
+
export function partitionAndMarkAnthropicToolCache(
|
|
66
|
+
tools: GraphTools | undefined,
|
|
67
|
+
isDeferred: (toolName: string) => boolean
|
|
68
|
+
): GraphTools | undefined {
|
|
69
|
+
if (tools == null || tools.length === 0) return tools;
|
|
70
|
+
|
|
71
|
+
// Use unknown[] internally to avoid GraphTools' union-array variance
|
|
72
|
+
// (each member is one of three array types). We cast back to
|
|
73
|
+
// GraphTools when returning.
|
|
74
|
+
const staticTools: unknown[] = [];
|
|
75
|
+
const deferredTools: unknown[] = [];
|
|
76
|
+
|
|
77
|
+
for (const tool of tools) {
|
|
78
|
+
const name = (tool as { name?: unknown }).name;
|
|
79
|
+
if (typeof name === 'string' && isDeferred(name)) {
|
|
80
|
+
deferredTools.push(tool);
|
|
81
|
+
} else {
|
|
82
|
+
staticTools.push(tool);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (staticTools.length === 0) {
|
|
87
|
+
return tools;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const last = staticTools[staticTools.length - 1] as {
|
|
91
|
+
extras?: Record<string, unknown>;
|
|
92
|
+
};
|
|
93
|
+
// Already marked? Don't double-clone.
|
|
94
|
+
if (
|
|
95
|
+
last.extras != null &&
|
|
96
|
+
'cache_control' in last.extras &&
|
|
97
|
+
(last.extras as { cache_control?: unknown }).cache_control != null
|
|
98
|
+
) {
|
|
99
|
+
if (deferredTools.length === 0) return tools;
|
|
100
|
+
return [...staticTools, ...deferredTools] as GraphTools;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const wrapped = Object.assign(
|
|
104
|
+
Object.create(Object.getPrototypeOf(last) ?? Object.prototype),
|
|
105
|
+
last,
|
|
106
|
+
{
|
|
107
|
+
extras: {
|
|
108
|
+
...((last.extras as Record<string, unknown> | undefined) ?? {}),
|
|
109
|
+
cache_control: { type: 'ephemeral' as const },
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
staticTools[staticTools.length - 1] = wrapped;
|
|
115
|
+
return [...staticTools, ...deferredTools] as GraphTools;
|
|
116
|
+
}
|
package/src/messages/index.ts
CHANGED
|
@@ -3,8 +3,10 @@ export * from './ids';
|
|
|
3
3
|
export * from './prune';
|
|
4
4
|
export * from './format';
|
|
5
5
|
export * from './cache';
|
|
6
|
+
export * from './anthropicToolCache';
|
|
6
7
|
export * from './content';
|
|
7
8
|
export * from './tools';
|
|
8
9
|
export * from './contextPruning';
|
|
9
10
|
export * from './contextPruningSettings';
|
|
10
11
|
export * from './reducer';
|
|
12
|
+
export * from './recency';
|
package/src/messages/prune.ts
CHANGED
|
@@ -50,7 +50,33 @@ const PRESSURE_BANDS: [number, number][] = [
|
|
|
50
50
|
const MASKED_RESULT_MAX_CHARS = 300;
|
|
51
51
|
|
|
52
52
|
/** Hard cap for the originalToolContent store (~2 MB estimated from char length). */
|
|
53
|
-
const ORIGINAL_CONTENT_MAX_CHARS = 2_000_000;
|
|
53
|
+
export const ORIGINAL_CONTENT_MAX_CHARS = 2_000_000;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Evicts oldest entries from `map` (in Map-iteration / insertion order) until
|
|
57
|
+
* the cumulative char length of remaining values fits within
|
|
58
|
+
* `ORIGINAL_CONTENT_MAX_CHARS`. Used by the recency-window carry-over merge
|
|
59
|
+
* path in Graph.ts to bound long-running session memory: the pruner enforces
|
|
60
|
+
* the cap inside its own `originalToolContent` map, but a key-wise union with
|
|
61
|
+
* recency carry-over bypasses that cap unless re-applied here.
|
|
62
|
+
*/
|
|
63
|
+
export function enforceOriginalContentCap(map: Map<number, string>): void {
|
|
64
|
+
let total = 0;
|
|
65
|
+
for (const v of map.values()) {
|
|
66
|
+
total += v.length;
|
|
67
|
+
}
|
|
68
|
+
while (total > ORIGINAL_CONTENT_MAX_CHARS && map.size > 0) {
|
|
69
|
+
const oldest = map.keys().next();
|
|
70
|
+
if (oldest.done === true) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
const removed = map.get(oldest.value);
|
|
74
|
+
if (removed != null) {
|
|
75
|
+
total -= removed.length;
|
|
76
|
+
}
|
|
77
|
+
map.delete(oldest.value);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
54
80
|
|
|
55
81
|
/** Minimum cumulative calibration ratio — provider can't count fewer tokens
|
|
56
82
|
* than our raw estimate (within reason). Prevents divide-by-zero edge cases. */
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for splitting a message list into a head (to be summarized)
|
|
5
|
+
* and a tail (to be preserved verbatim).
|
|
6
|
+
*/
|
|
7
|
+
export interface RecencyWindowOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Maximum number of recent user-led turns to keep in the tail. A "turn"
|
|
10
|
+
* begins at a HumanMessage and includes every following AIMessage and
|
|
11
|
+
* ToolMessage up to (but not including) the next HumanMessage. Cutting
|
|
12
|
+
* at turn boundaries guarantees that tool_use / tool_result pairs are
|
|
13
|
+
* never split across the head/tail divide.
|
|
14
|
+
*
|
|
15
|
+
* The most recent turn is always preserved regardless of this value or
|
|
16
|
+
* the token cap, so that a single oversized first message is never
|
|
17
|
+
* destroyed by summarization.
|
|
18
|
+
*
|
|
19
|
+
* Defaults to `2`. A value of `0` disables the recency window (head =
|
|
20
|
+
* everything, tail = empty), restoring the pre-recency-window behavior.
|
|
21
|
+
*/
|
|
22
|
+
turns?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Optional cap on tail size in tokens. When set, additional turns
|
|
25
|
+
* beyond the most recent one are added to the tail only while the
|
|
26
|
+
* cumulative token count stays at or below this cap. Turns are added
|
|
27
|
+
* whole — never partially — so a turn that would exceed the cap is
|
|
28
|
+
* left in the head.
|
|
29
|
+
*
|
|
30
|
+
* The most recent turn is always preserved even if it exceeds the cap.
|
|
31
|
+
*/
|
|
32
|
+
tokens?: number;
|
|
33
|
+
/** Token-counter used to evaluate the optional `tokens` cap. */
|
|
34
|
+
tokenCounter?: (m: BaseMessage) => number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RecencySplit {
|
|
38
|
+
/** Older messages eligible for summarization. Empty when nothing to summarize. */
|
|
39
|
+
head: BaseMessage[];
|
|
40
|
+
/** Recent messages preserved verbatim. Always contains the most recent turn when any HumanMessage exists. */
|
|
41
|
+
tail: BaseMessage[];
|
|
42
|
+
/** Number of user-led turns retained in the tail (0 if no HumanMessage exists). */
|
|
43
|
+
tailTurnCount: number;
|
|
44
|
+
/** Index in the original `messages` array where the tail begins. */
|
|
45
|
+
tailStartIndex: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Splits `messages` into a head (older, to summarize) and a tail (recent,
|
|
50
|
+
* to preserve verbatim) at user-message boundaries. The most recent
|
|
51
|
+
* user-led turn is always included in the tail; additional older turns
|
|
52
|
+
* are added subject to `turns` and `tokens` caps.
|
|
53
|
+
*
|
|
54
|
+
* Cutting strictly at HumanMessage boundaries ensures that:
|
|
55
|
+
* - tool_use ↔ tool_result pairs are never split (they always live within
|
|
56
|
+
* the same turn);
|
|
57
|
+
* - the first user message is never replaced by a summary, addressing
|
|
58
|
+
* the "first turn destruction" failure mode where a single large
|
|
59
|
+
* user-pasted payload would otherwise be replaced by a generic summary.
|
|
60
|
+
*
|
|
61
|
+
* When `messages` contains no HumanMessage (degenerate state — e.g. system
|
|
62
|
+
* + assistant messages from a programmatic preamble), everything is
|
|
63
|
+
* placed in the head and the tail is empty. The summarize node treats
|
|
64
|
+
* an empty tail as "nothing recent to preserve" and falls through to its
|
|
65
|
+
* existing logic.
|
|
66
|
+
*/
|
|
67
|
+
export function splitAtRecencyBoundary(
|
|
68
|
+
messages: BaseMessage[],
|
|
69
|
+
options: RecencyWindowOptions = {}
|
|
70
|
+
): RecencySplit {
|
|
71
|
+
const turnsCap = options.turns ?? 2;
|
|
72
|
+
|
|
73
|
+
if (messages.length === 0 || turnsCap <= 0) {
|
|
74
|
+
return {
|
|
75
|
+
head: messages,
|
|
76
|
+
tail: [],
|
|
77
|
+
tailTurnCount: 0,
|
|
78
|
+
tailStartIndex: messages.length,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const turnStarts: number[] = [];
|
|
83
|
+
for (let i = 0; i < messages.length; i++) {
|
|
84
|
+
if (messages[i].getType() === 'human') {
|
|
85
|
+
turnStarts.push(i);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (turnStarts.length === 0) {
|
|
90
|
+
return {
|
|
91
|
+
head: messages,
|
|
92
|
+
tail: [],
|
|
93
|
+
tailTurnCount: 0,
|
|
94
|
+
tailStartIndex: messages.length,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const lastTurnStart = turnStarts[turnStarts.length - 1] as number;
|
|
99
|
+
let tailStartIndex = lastTurnStart;
|
|
100
|
+
let tailTurnCount = 1;
|
|
101
|
+
|
|
102
|
+
const tokensCap = options.tokens;
|
|
103
|
+
const tokenCounter = options.tokenCounter;
|
|
104
|
+
const trackTokens =
|
|
105
|
+
tokensCap != null && Number.isFinite(tokensCap) && tokenCounter != null;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Token-counting strategy: each candidate turn `t` spans the half-open
|
|
109
|
+
* range `[turnStarts[t], turnStarts[t + 1])` (or `[turnStarts[t], messages.length)`
|
|
110
|
+
* for the most recent turn). Successive iterations of the outer loop
|
|
111
|
+
* walk older turns one at a time and never revisit messages from a
|
|
112
|
+
* later turn — so each message contributes to `tokenCounter` at most
|
|
113
|
+
* once across the entire selection, making the boundary search
|
|
114
|
+
* `O(messages_in_visited_turns)` and bounded by `O(messages.length)`
|
|
115
|
+
* even before the `turnsCap` short-circuit applies. The inner upper
|
|
116
|
+
* bound uses `turnStarts[t + 1]` (a value derived from immutable
|
|
117
|
+
* `turnStarts`) rather than the mutated `tailStartIndex` to make the
|
|
118
|
+
* disjoint-range invariant self-evident.
|
|
119
|
+
*/
|
|
120
|
+
let tailTokens = 0;
|
|
121
|
+
if (trackTokens) {
|
|
122
|
+
for (let i = lastTurnStart; i < messages.length; i++) {
|
|
123
|
+
tailTokens += tokenCounter(messages[i] as BaseMessage);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (let t = turnStarts.length - 2; t >= 0; t--) {
|
|
128
|
+
if (tailTurnCount >= turnsCap) {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
const turnStart = turnStarts[t] as number;
|
|
132
|
+
const turnEnd = turnStarts[t + 1] as number;
|
|
133
|
+
|
|
134
|
+
if (trackTokens) {
|
|
135
|
+
let turnTokens = 0;
|
|
136
|
+
for (let i = turnStart; i < turnEnd; i++) {
|
|
137
|
+
turnTokens += tokenCounter(messages[i] as BaseMessage);
|
|
138
|
+
}
|
|
139
|
+
if (tailTokens + turnTokens > (tokensCap as number)) {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
tailTokens += turnTokens;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
tailStartIndex = turnStart;
|
|
146
|
+
tailTurnCount += 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
head: messages.slice(0, tailStartIndex),
|
|
151
|
+
tail: messages.slice(tailStartIndex),
|
|
152
|
+
tailTurnCount,
|
|
153
|
+
tailStartIndex,
|
|
154
|
+
};
|
|
155
|
+
}
|
package/src/run.ts
CHANGED
|
@@ -54,6 +54,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
54
54
|
private hookRegistry?: HookRegistry;
|
|
55
55
|
private humanInTheLoop?: t.HumanInTheLoopConfig;
|
|
56
56
|
private toolOutputReferences?: t.ToolOutputReferencesConfig;
|
|
57
|
+
private toolExecution?: t.ToolExecutionConfig;
|
|
57
58
|
private indexTokenCountMap?: Record<string, number>;
|
|
58
59
|
calibrationRatio: number = 1;
|
|
59
60
|
graphRunnable?: t.CompiledStateWorkflow;
|
|
@@ -98,6 +99,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
98
99
|
this.hookRegistry = config.hooks;
|
|
99
100
|
this.humanInTheLoop = config.humanInTheLoop;
|
|
100
101
|
this.toolOutputReferences = config.toolOutputReferences;
|
|
102
|
+
this.toolExecution = config.toolExecution;
|
|
101
103
|
|
|
102
104
|
if (!config.graphConfig) {
|
|
103
105
|
throw new Error('Graph config not provided');
|
|
@@ -178,6 +180,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
178
180
|
standardGraph.hookRegistry = this.hookRegistry;
|
|
179
181
|
standardGraph.humanInTheLoop = this.humanInTheLoop;
|
|
180
182
|
standardGraph.toolOutputReferences = this.toolOutputReferences;
|
|
183
|
+
standardGraph.toolExecution = this.toolExecution;
|
|
181
184
|
this.Graph = standardGraph;
|
|
182
185
|
return standardGraph.createWorkflow();
|
|
183
186
|
}
|
|
@@ -202,6 +205,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
202
205
|
multiAgentGraph.hookRegistry = this.hookRegistry;
|
|
203
206
|
multiAgentGraph.humanInTheLoop = this.humanInTheLoop;
|
|
204
207
|
multiAgentGraph.toolOutputReferences = this.toolOutputReferences;
|
|
208
|
+
multiAgentGraph.toolExecution = this.toolExecution;
|
|
205
209
|
this.Graph = multiAgentGraph;
|
|
206
210
|
return multiAgentGraph.createWorkflow();
|
|
207
211
|
}
|
|
@@ -898,6 +902,33 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
898
902
|
* graph state from the checkpoint and re-enters the interrupted node
|
|
899
903
|
* from the start.
|
|
900
904
|
*/
|
|
905
|
+
/**
|
|
906
|
+
* Returns the per-Run file checkpointer when
|
|
907
|
+
* `toolExecution.local.fileCheckpointing === true` was set on the
|
|
908
|
+
* RunConfig. Hosts can capture extra paths or call `rewind()`
|
|
909
|
+
* directly. Returns undefined when checkpointing is disabled.
|
|
910
|
+
*
|
|
911
|
+
* Construction-time invariant: the checkpointer is shared across
|
|
912
|
+
* every ToolNode the graph compiles (single-agent and multi-agent),
|
|
913
|
+
* so a `rewind()` call here unwinds writes made by ANY agent in the
|
|
914
|
+
* run.
|
|
915
|
+
*/
|
|
916
|
+
getFileCheckpointer(): t.LocalFileCheckpointer | undefined {
|
|
917
|
+
return this.Graph?.getOrCreateFileCheckpointer();
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Convenience wrapper that calls `rewind()` on the per-Run file
|
|
922
|
+
* checkpointer. Restores every file the local engine snapshotted
|
|
923
|
+
* during this Run to its pre-write content (and deletes any path
|
|
924
|
+
* that didn't exist before being created). Returns the count of
|
|
925
|
+
* paths processed; returns 0 when checkpointing is disabled.
|
|
926
|
+
*/
|
|
927
|
+
async rewindFiles(): Promise<number> {
|
|
928
|
+
const cp = this.getFileCheckpointer();
|
|
929
|
+
return cp == null ? 0 : cp.rewind();
|
|
930
|
+
}
|
|
931
|
+
|
|
901
932
|
async resume<TResume = t.ToolApprovalDecision[] | t.ToolApprovalDecisionMap>(
|
|
902
933
|
resumeValue: TResume,
|
|
903
934
|
callerConfig: Partial<RunnableConfig> & {
|