@librechat/agents 3.1.85 → 3.1.87
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/README.md +69 -0
- package/dist/cjs/agents/AgentContext.cjs +7 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/events.cjs +23 -0
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +133 -18
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +251 -53
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +1 -5
- package/dist/cjs/llm/init.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +113 -24
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +3 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +18 -5
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/openai/index.cjs +253 -0
- package/dist/cjs/openai/index.cjs.map +1 -0
- package/dist/cjs/responses/index.cjs +448 -0
- package/dist/cjs/responses/index.cjs.map +1 -0
- package/dist/cjs/run.cjs +108 -7
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +1057 -0
- package/dist/cjs/session/AgentSession.cjs.map +1 -0
- package/dist/cjs/session/JsonlSessionStore.cjs +425 -0
- package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -0
- package/dist/cjs/session/handlers.cjs +221 -0
- package/dist/cjs/session/handlers.cjs.map +1 -0
- package/dist/cjs/session/ids.cjs +22 -0
- package/dist/cjs/session/ids.cjs.map +1 -0
- package/dist/cjs/session/messageSerialization.cjs +179 -0
- package/dist/cjs/session/messageSerialization.cjs.map +1 -0
- package/dist/cjs/stream.cjs +472 -11
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +1 -1
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +177 -59
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/eagerEventExecution.cjs +113 -0
- package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -0
- package/dist/cjs/tools/handlers.cjs +1 -1
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/cjs/tools/streamedToolCallSeals.cjs +42 -0
- package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +7 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/events.mjs +23 -1
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +133 -18
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +251 -53
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -5
- package/dist/esm/llm/init.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +113 -25
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +4 -2
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/main.mjs +5 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/openai/index.mjs +246 -0
- package/dist/esm/openai/index.mjs.map +1 -0
- package/dist/esm/responses/index.mjs +440 -0
- package/dist/esm/responses/index.mjs.map +1 -0
- package/dist/esm/run.mjs +108 -7
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +1054 -0
- package/dist/esm/session/AgentSession.mjs.map +1 -0
- package/dist/esm/session/JsonlSessionStore.mjs +422 -0
- package/dist/esm/session/JsonlSessionStore.mjs.map +1 -0
- package/dist/esm/session/handlers.mjs +219 -0
- package/dist/esm/session/handlers.mjs.map +1 -0
- package/dist/esm/session/ids.mjs +17 -0
- package/dist/esm/session/ids.mjs.map +1 -0
- package/dist/esm/session/messageSerialization.mjs +173 -0
- package/dist/esm/session/messageSerialization.mjs.map +1 -0
- package/dist/esm/stream.mjs +473 -12
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +1 -1
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +177 -59
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/eagerEventExecution.mjs +107 -0
- package/dist/esm/tools/eagerEventExecution.mjs.map +1 -0
- package/dist/esm/tools/handlers.mjs +1 -1
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/esm/tools/streamedToolCallSeals.mjs +36 -0
- package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -0
- package/dist/types/events.d.ts +1 -0
- package/dist/types/graphs/Graph.d.ts +24 -9
- package/dist/types/index.d.ts +1 -0
- package/dist/types/llm/openai/index.d.ts +1 -0
- package/dist/types/openai/index.d.ts +75 -0
- package/dist/types/responses/index.d.ts +97 -0
- package/dist/types/run.d.ts +2 -0
- package/dist/types/session/AgentSession.d.ts +32 -0
- package/dist/types/session/JsonlSessionStore.d.ts +67 -0
- package/dist/types/session/handlers.d.ts +8 -0
- package/dist/types/session/ids.d.ts +4 -0
- package/dist/types/session/index.d.ts +5 -0
- package/dist/types/session/messageSerialization.d.ts +7 -0
- package/dist/types/session/types.d.ts +191 -0
- package/dist/types/tools/ToolNode.d.ts +12 -1
- package/dist/types/tools/eagerEventExecution.d.ts +23 -0
- package/dist/types/tools/streamedToolCallSeals.d.ts +13 -0
- package/dist/types/types/hitl.d.ts +4 -0
- package/dist/types/types/run.d.ts +11 -1
- package/dist/types/types/tools.d.ts +36 -0
- package/package.json +19 -2
- package/src/__tests__/stream.eagerEventExecution.test.ts +2458 -0
- package/src/agents/AgentContext.ts +7 -2
- package/src/agents/__tests__/AgentContext.test.ts +254 -5
- package/src/events.ts +29 -0
- package/src/graphs/Graph.ts +224 -50
- package/src/graphs/MultiAgentGraph.ts +1 -1
- package/src/graphs/__tests__/composition.smoke.test.ts +30 -0
- package/src/index.ts +3 -0
- package/src/llm/anthropic/index.ts +356 -84
- package/src/llm/anthropic/llm.spec.ts +64 -0
- package/src/llm/custom-chat-models.smoke.test.ts +175 -4
- package/src/llm/openai/contentBlocks.test.ts +35 -0
- package/src/llm/openai/deepseek.test.ts +201 -2
- package/src/llm/openai/index.ts +171 -26
- package/src/llm/openai/utils/index.ts +22 -0
- package/src/llm/openrouter/index.ts +4 -2
- package/src/openai/__tests__/openai.test.ts +337 -0
- package/src/openai/index.ts +404 -0
- package/src/responses/__tests__/responses.test.ts +652 -0
- package/src/responses/index.ts +677 -0
- package/src/run.ts +158 -8
- package/src/scripts/compare_pi_vs_ours.ts +592 -173
- package/src/scripts/session_live.ts +548 -0
- package/src/session/AgentSession.ts +1432 -0
- package/src/session/JsonlSessionStore.ts +572 -0
- package/src/session/__tests__/JsonlSessionStore.test.ts +1410 -0
- package/src/session/__tests__/handlers.test.ts +161 -0
- package/src/session/handlers.ts +272 -0
- package/src/session/ids.ts +17 -0
- package/src/session/index.ts +44 -0
- package/src/session/messageSerialization.ts +207 -0
- package/src/session/types.ts +275 -0
- package/src/specs/custom-event-await.test.ts +89 -0
- package/src/specs/summarization.test.ts +1 -1
- package/src/stream.ts +755 -48
- package/src/summarization/node.ts +1 -1
- package/src/tools/ToolNode.ts +299 -126
- package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +373 -0
- package/src/tools/__tests__/handlers.test.ts +2 -1
- package/src/tools/__tests__/hitl.test.ts +206 -110
- package/src/tools/eagerEventExecution.ts +153 -0
- package/src/tools/handlers.ts +8 -4
- package/src/tools/streamedToolCallSeals.ts +57 -0
- package/src/types/hitl.ts +4 -0
- package/src/types/run.ts +11 -0
- package/src/types/tools.ts +36 -0
- package/dist/cjs/llm/text.cjs +0 -69
- package/dist/cjs/llm/text.cjs.map +0 -1
- package/dist/esm/llm/text.mjs +0 -67
- package/dist/esm/llm/text.mjs.map +0 -1
|
@@ -0,0 +1,1410 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, writeFile } from 'fs/promises';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import {
|
|
5
|
+
AIMessage,
|
|
6
|
+
HumanMessage,
|
|
7
|
+
RemoveMessage,
|
|
8
|
+
} from '@langchain/core/messages';
|
|
9
|
+
import { MemorySaver } from '@langchain/langgraph';
|
|
10
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
11
|
+
import type { Checkpoint, CheckpointMetadata } from '@langchain/langgraph';
|
|
12
|
+
import { JsonlSessionStore, createAgentSession } from '@/session';
|
|
13
|
+
import { toJsonValue } from '@/session/messageSerialization';
|
|
14
|
+
import * as providers from '@/llm/providers';
|
|
15
|
+
import { GraphEvents } from '@/common';
|
|
16
|
+
import type * as t from '@/types';
|
|
17
|
+
import { Run } from '@/run';
|
|
18
|
+
|
|
19
|
+
type MockRun = {
|
|
20
|
+
processStream: jest.MockedFunction<Run<t.IState>['processStream']>;
|
|
21
|
+
resume: jest.MockedFunction<Run<t.IState>['resume']>;
|
|
22
|
+
getRunMessages: jest.MockedFunction<Run<t.IState>['getRunMessages']>;
|
|
23
|
+
getCalibrationRatio: jest.MockedFunction<
|
|
24
|
+
Run<t.IState>['getCalibrationRatio']
|
|
25
|
+
>;
|
|
26
|
+
getInterrupt: jest.MockedFunction<Run<t.IState>['getInterrupt']>;
|
|
27
|
+
getHaltReason: jest.MockedFunction<Run<t.IState>['getHaltReason']>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function createMockRun(outputText = 'ok'): MockRun {
|
|
31
|
+
return {
|
|
32
|
+
processStream: jest
|
|
33
|
+
.fn<
|
|
34
|
+
ReturnType<Run<t.IState>['processStream']>,
|
|
35
|
+
Parameters<Run<t.IState>['processStream']>
|
|
36
|
+
>()
|
|
37
|
+
.mockResolvedValue([{ type: 'text', text: outputText }]),
|
|
38
|
+
resume: jest
|
|
39
|
+
.fn<
|
|
40
|
+
ReturnType<Run<t.IState>['resume']>,
|
|
41
|
+
Parameters<Run<t.IState>['resume']>
|
|
42
|
+
>()
|
|
43
|
+
.mockResolvedValue([{ type: 'text', text: outputText }]),
|
|
44
|
+
getRunMessages: jest.fn(() => [new AIMessage(outputText)]),
|
|
45
|
+
getCalibrationRatio: jest.fn(() => 1),
|
|
46
|
+
getInterrupt: jest.fn(() => undefined),
|
|
47
|
+
getHaltReason: jest.fn(() => undefined),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mockRunCreate(mockRun: MockRun): t.RunConfig[] {
|
|
52
|
+
const capturedConfigs: t.RunConfig[] = [];
|
|
53
|
+
jest.spyOn(Run, 'create').mockImplementation((async <
|
|
54
|
+
T extends t.BaseGraphState,
|
|
55
|
+
>(
|
|
56
|
+
config: t.RunConfig
|
|
57
|
+
): Promise<Run<T>> => {
|
|
58
|
+
capturedConfigs.push(config);
|
|
59
|
+
return mockRun as unknown as Run<T>;
|
|
60
|
+
}) as never);
|
|
61
|
+
return capturedConfigs;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getProcessedState(mockRun: MockRun): t.IState {
|
|
65
|
+
expect(mockRun.processStream).toHaveBeenCalled();
|
|
66
|
+
const input = mockRun.processStream.mock.calls[0][0];
|
|
67
|
+
if (!('messages' in input)) {
|
|
68
|
+
throw new Error('Expected processStream to receive message state');
|
|
69
|
+
}
|
|
70
|
+
return input;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getProcessedMessages(mockRun: MockRun): BaseMessage[] {
|
|
74
|
+
return getProcessedState(mockRun).messages;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function putCheckpoint(params: {
|
|
78
|
+
checkpointer: MemorySaver;
|
|
79
|
+
threadId: string;
|
|
80
|
+
id: string;
|
|
81
|
+
checkpointNs?: string;
|
|
82
|
+
}): Promise<void> {
|
|
83
|
+
const checkpoint: Checkpoint = {
|
|
84
|
+
v: 4,
|
|
85
|
+
id: params.id,
|
|
86
|
+
ts: new Date().toISOString(),
|
|
87
|
+
channel_values: {},
|
|
88
|
+
channel_versions: {},
|
|
89
|
+
versions_seen: {},
|
|
90
|
+
};
|
|
91
|
+
const metadata: CheckpointMetadata = {
|
|
92
|
+
source: 'loop',
|
|
93
|
+
step: 0,
|
|
94
|
+
parents: {},
|
|
95
|
+
};
|
|
96
|
+
await params.checkpointer.put(
|
|
97
|
+
{
|
|
98
|
+
configurable: {
|
|
99
|
+
thread_id: params.threadId,
|
|
100
|
+
checkpoint_ns: params.checkpointNs ?? '',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
checkpoint,
|
|
104
|
+
metadata
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function mockSummarizer(response: string): void {
|
|
109
|
+
jest.spyOn(providers, 'getChatModelClass').mockReturnValue(
|
|
110
|
+
class {
|
|
111
|
+
constructor() {
|
|
112
|
+
return {
|
|
113
|
+
invoke: jest.fn().mockResolvedValue({ content: response }),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
} as never
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe('JsonlSessionStore', () => {
|
|
121
|
+
let dir: string;
|
|
122
|
+
|
|
123
|
+
beforeEach(async () => {
|
|
124
|
+
dir = await mkdtemp(join(tmpdir(), 'lc-agent-session-'));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
afterEach(async () => {
|
|
128
|
+
jest.restoreAllMocks();
|
|
129
|
+
await rm(dir, { recursive: true, force: true });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('stores messages as an append-only tree and restores the active path', async () => {
|
|
133
|
+
const path = join(dir, 'session.jsonl');
|
|
134
|
+
const store = await JsonlSessionStore.create({
|
|
135
|
+
path,
|
|
136
|
+
cwd: dir,
|
|
137
|
+
sessionId: 'session-a',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const user = await store.appendMessage(new HumanMessage('hello'));
|
|
141
|
+
const assistant = await store.appendMessage(new AIMessage('hi'));
|
|
142
|
+
|
|
143
|
+
const reopened = await JsonlSessionStore.open(path);
|
|
144
|
+
|
|
145
|
+
expect(reopened.header.id).toBe('session-a');
|
|
146
|
+
expect(reopened.getLeafEntry()?.id).toBe(assistant.id);
|
|
147
|
+
expect(reopened.getPath().map((entry) => entry.id)).toEqual([
|
|
148
|
+
user.id,
|
|
149
|
+
assistant.id,
|
|
150
|
+
]);
|
|
151
|
+
expect(reopened.getMessages().map((message) => message.content)).toEqual([
|
|
152
|
+
'hello',
|
|
153
|
+
'hi',
|
|
154
|
+
]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('round-trips remove messages in persisted sessions', async () => {
|
|
158
|
+
const path = join(dir, 'remove.jsonl');
|
|
159
|
+
const store = await JsonlSessionStore.create({ path, cwd: dir });
|
|
160
|
+
|
|
161
|
+
await store.appendMessage(
|
|
162
|
+
new HumanMessage({ id: 'message-a', content: 'a' })
|
|
163
|
+
);
|
|
164
|
+
await store.appendMessage(new RemoveMessage({ id: 'message-a' }));
|
|
165
|
+
|
|
166
|
+
const reopened = await JsonlSessionStore.open(path);
|
|
167
|
+
const messages = reopened.getMessages();
|
|
168
|
+
|
|
169
|
+
expect(messages.map((message) => message._getType())).toEqual([
|
|
170
|
+
'human',
|
|
171
|
+
'remove',
|
|
172
|
+
]);
|
|
173
|
+
expect((messages[1] as RemoveMessage).id).toBe('message-a');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('fails when creating a session file that already exists', async () => {
|
|
177
|
+
const path = join(dir, 'existing.jsonl');
|
|
178
|
+
await JsonlSessionStore.create({
|
|
179
|
+
path,
|
|
180
|
+
cwd: dir,
|
|
181
|
+
sessionId: 'session-a',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await expect(
|
|
185
|
+
JsonlSessionStore.create({
|
|
186
|
+
path,
|
|
187
|
+
cwd: dir,
|
|
188
|
+
sessionId: 'session-b',
|
|
189
|
+
})
|
|
190
|
+
).rejects.toMatchObject({ code: 'EEXIST' });
|
|
191
|
+
|
|
192
|
+
const raw = await readFile(path, 'utf8');
|
|
193
|
+
expect(raw.match(/"type":"session"/g)).toHaveLength(1);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('keeps default session roots distinct for similar cwd strings', async () => {
|
|
197
|
+
const cwdA = join(dir, 'foo/bar');
|
|
198
|
+
const cwdB = join(dir, 'foo-bar');
|
|
199
|
+
const storeA = await JsonlSessionStore.create({
|
|
200
|
+
cwd: cwdA,
|
|
201
|
+
sessionId: 'cwd-a',
|
|
202
|
+
});
|
|
203
|
+
const storeB = await JsonlSessionStore.create({
|
|
204
|
+
cwd: cwdB,
|
|
205
|
+
sessionId: 'cwd-b',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const [itemsA, itemsB] = await Promise.all([
|
|
210
|
+
JsonlSessionStore.list(cwdA),
|
|
211
|
+
JsonlSessionStore.list(cwdB),
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
expect(dirname(storeA.path)).not.toBe(dirname(storeB.path));
|
|
215
|
+
expect(itemsA.map((item) => item.id)).toContain('cwd-a');
|
|
216
|
+
expect(itemsA.map((item) => item.id)).not.toContain('cwd-b');
|
|
217
|
+
expect(itemsB.map((item) => item.id)).toContain('cwd-b');
|
|
218
|
+
expect(itemsB.map((item) => item.id)).not.toContain('cwd-a');
|
|
219
|
+
} finally {
|
|
220
|
+
await Promise.all([
|
|
221
|
+
rm(storeA.path, { force: true }),
|
|
222
|
+
rm(storeB.path, { force: true }),
|
|
223
|
+
]);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('branches in place without deleting abandoned children', async () => {
|
|
228
|
+
const store = await JsonlSessionStore.create({
|
|
229
|
+
path: join(dir, 'branch.jsonl'),
|
|
230
|
+
cwd: dir,
|
|
231
|
+
});
|
|
232
|
+
const first = await store.appendMessage(new HumanMessage('one'));
|
|
233
|
+
const abandoned = await store.appendMessage(new AIMessage('abandoned'));
|
|
234
|
+
|
|
235
|
+
await store.branch(first.id);
|
|
236
|
+
const alternate = await store.appendMessage(new AIMessage('alternate'));
|
|
237
|
+
|
|
238
|
+
expect(
|
|
239
|
+
store
|
|
240
|
+
.getChildren(first.id)
|
|
241
|
+
.filter((entry) => entry.type === 'message')
|
|
242
|
+
.map((entry) => entry.id)
|
|
243
|
+
.sort()
|
|
244
|
+
).toEqual([abandoned.id, alternate.id].sort());
|
|
245
|
+
expect(store.getPath().map((entry) => entry.id)).toEqual([
|
|
246
|
+
first.id,
|
|
247
|
+
alternate.id,
|
|
248
|
+
]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('clones and forks active paths into new session files', async () => {
|
|
252
|
+
const store = await JsonlSessionStore.create({
|
|
253
|
+
path: join(dir, 'source.jsonl'),
|
|
254
|
+
cwd: dir,
|
|
255
|
+
});
|
|
256
|
+
const first = await store.appendMessage(new HumanMessage('first'));
|
|
257
|
+
const second = await store.appendMessage(new AIMessage('second'));
|
|
258
|
+
|
|
259
|
+
const clone = await store.clone({ cwd: dir });
|
|
260
|
+
const fork = await store.fork(second.id, { cwd: dir, position: 'before' });
|
|
261
|
+
|
|
262
|
+
expect(clone.header.parentSession).toBe(store.path);
|
|
263
|
+
expect(clone.getPath().map((entry) => entry.id)).toEqual([
|
|
264
|
+
first.id,
|
|
265
|
+
second.id,
|
|
266
|
+
]);
|
|
267
|
+
expect(fork.getPath().map((entry) => entry.id)).toEqual([first.id]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('tracks labels and compaction entries', async () => {
|
|
271
|
+
const store = await JsonlSessionStore.create({
|
|
272
|
+
path: join(dir, 'labels.jsonl'),
|
|
273
|
+
cwd: dir,
|
|
274
|
+
});
|
|
275
|
+
const message = await store.appendMessage(new HumanMessage('hello'));
|
|
276
|
+
|
|
277
|
+
await store.setLabel(message.id, 'checkpoint');
|
|
278
|
+
const summary = await store.appendEntryForCompaction({
|
|
279
|
+
text: 'summary',
|
|
280
|
+
retainedEntryIds: [message.id],
|
|
281
|
+
summarizedEntryIds: [],
|
|
282
|
+
});
|
|
283
|
+
const compaction = await store.appendCompactionEntry({
|
|
284
|
+
summaryEntryId: summary.id,
|
|
285
|
+
retainedEntryIds: [message.id],
|
|
286
|
+
summarizedEntryIds: [],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(store.getLabel(message.id)).toBe('checkpoint');
|
|
290
|
+
expect(summary.data.text).toBe('summary');
|
|
291
|
+
expect(compaction.data.summaryEntryId).toBe(summary.id);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('records LangGraph checkpoint references without moving the active leaf', async () => {
|
|
295
|
+
const store = await JsonlSessionStore.create({
|
|
296
|
+
path: join(dir, 'checkpoints.jsonl'),
|
|
297
|
+
cwd: dir,
|
|
298
|
+
});
|
|
299
|
+
const message = await store.appendMessage(new HumanMessage('hello'));
|
|
300
|
+
|
|
301
|
+
const checkpoint = await store.appendCheckpoint({
|
|
302
|
+
source: 'run',
|
|
303
|
+
threadId: store.header.id,
|
|
304
|
+
runId: 'run_checkpoint',
|
|
305
|
+
checkpointId: 'checkpoint_1',
|
|
306
|
+
checkpointNs: '',
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
expect(checkpoint.data.provider).toBe('langgraph');
|
|
310
|
+
expect(store.getLeafEntry()?.id).toBe(message.id);
|
|
311
|
+
expect(store.getLatestCheckpoint(store.header.id)?.id).toBe(checkpoint.id);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('treats reset checkpoints as latest checkpoint barriers', async () => {
|
|
315
|
+
const store = await JsonlSessionStore.create({
|
|
316
|
+
path: join(dir, 'checkpoint-reset.jsonl'),
|
|
317
|
+
cwd: dir,
|
|
318
|
+
});
|
|
319
|
+
await store.appendCheckpoint({
|
|
320
|
+
source: 'run',
|
|
321
|
+
threadId: store.header.id,
|
|
322
|
+
runId: 'run_before_reset',
|
|
323
|
+
checkpointId: 'checkpoint_before_reset',
|
|
324
|
+
});
|
|
325
|
+
await store.appendCheckpoint({
|
|
326
|
+
source: 'reset',
|
|
327
|
+
threadId: store.header.id,
|
|
328
|
+
reason: 'branch',
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
expect(store.getLatestCheckpoint(store.header.id)).toBeUndefined();
|
|
332
|
+
|
|
333
|
+
const checkpoint = await store.appendCheckpoint({
|
|
334
|
+
source: 'run',
|
|
335
|
+
threadId: store.header.id,
|
|
336
|
+
runId: 'run_after_reset',
|
|
337
|
+
checkpointId: 'checkpoint_after_reset',
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(store.getLatestCheckpoint(store.header.id)?.id).toBe(checkpoint.id);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('preserves Error details in JSONL payloads', () => {
|
|
344
|
+
const error = new Error('resume failed');
|
|
345
|
+
const payload = toJsonValue(error);
|
|
346
|
+
|
|
347
|
+
expect(payload).toMatchObject({
|
|
348
|
+
name: 'Error',
|
|
349
|
+
message: 'resume failed',
|
|
350
|
+
});
|
|
351
|
+
expect(
|
|
352
|
+
typeof payload === 'object' &&
|
|
353
|
+
payload != null &&
|
|
354
|
+
!Array.isArray(payload) &&
|
|
355
|
+
typeof payload.stack === 'string'
|
|
356
|
+
).toBe(true);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('replaces circular object references in JSONL payloads', () => {
|
|
360
|
+
interface CircularPayload {
|
|
361
|
+
label: string;
|
|
362
|
+
self?: CircularPayload;
|
|
363
|
+
child?: { parent?: CircularPayload };
|
|
364
|
+
}
|
|
365
|
+
const circular: CircularPayload = { label: 'root' };
|
|
366
|
+
circular.self = circular;
|
|
367
|
+
circular.child = { parent: circular };
|
|
368
|
+
|
|
369
|
+
expect(toJsonValue(circular)).toMatchObject({
|
|
370
|
+
label: 'root',
|
|
371
|
+
self: '[Circular]',
|
|
372
|
+
child: { parent: '[Circular]' },
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('replaces circular Error causes in JSONL payloads', () => {
|
|
377
|
+
const error = new Error('request failed');
|
|
378
|
+
Object.defineProperty(error, 'cause', {
|
|
379
|
+
value: error,
|
|
380
|
+
configurable: true,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(toJsonValue(error)).toMatchObject({
|
|
384
|
+
name: 'Error',
|
|
385
|
+
message: 'request failed',
|
|
386
|
+
cause: '[Circular]',
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('creates high-level sessions with a JSONL store by default', async () => {
|
|
391
|
+
const session = await createAgentSession({
|
|
392
|
+
cwd: dir,
|
|
393
|
+
runId: 'template-run',
|
|
394
|
+
graphConfig: {
|
|
395
|
+
type: 'standard',
|
|
396
|
+
llmConfig: {
|
|
397
|
+
provider: 'openAI' as never,
|
|
398
|
+
model: 'test-model',
|
|
399
|
+
},
|
|
400
|
+
instructions: 'test',
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
expect(session.getSessionStore()?.header.cwd).toBe(dir);
|
|
405
|
+
expect(session.sessionPath).toContain('.jsonl');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('surfaces invalid explicit session files instead of replacing them', async () => {
|
|
409
|
+
const sessionPath = join(dir, 'invalid.jsonl');
|
|
410
|
+
await writeFile(sessionPath, 'not jsonl\n', 'utf8');
|
|
411
|
+
|
|
412
|
+
await expect(
|
|
413
|
+
createAgentSession({
|
|
414
|
+
cwd: dir,
|
|
415
|
+
sessionPath,
|
|
416
|
+
runId: 'template-run',
|
|
417
|
+
graphConfig: {
|
|
418
|
+
type: 'standard',
|
|
419
|
+
llmConfig: {
|
|
420
|
+
provider: 'openAI' as never,
|
|
421
|
+
model: 'test-model',
|
|
422
|
+
},
|
|
423
|
+
instructions: 'test',
|
|
424
|
+
},
|
|
425
|
+
})
|
|
426
|
+
).rejects.toThrow('Invalid session file');
|
|
427
|
+
|
|
428
|
+
expect(await readFile(sessionPath, 'utf8')).toBe('not jsonl\n');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('creates an explicit session path without fuzzy matching existing sessions', async () => {
|
|
432
|
+
const existing = await JsonlSessionStore.create({
|
|
433
|
+
path: join(dir, 'matching-existing.jsonl'),
|
|
434
|
+
cwd: dir,
|
|
435
|
+
sessionId: 'explicit-target-existing',
|
|
436
|
+
});
|
|
437
|
+
await existing.appendMessage(new HumanMessage('existing history'));
|
|
438
|
+
const sessionPath = join(dir, 'explicit-target.jsonl');
|
|
439
|
+
|
|
440
|
+
const session = await createAgentSession({
|
|
441
|
+
cwd: dir,
|
|
442
|
+
sessionPath,
|
|
443
|
+
runId: 'template-run',
|
|
444
|
+
graphConfig: {
|
|
445
|
+
type: 'standard',
|
|
446
|
+
llmConfig: {
|
|
447
|
+
provider: 'openAI' as never,
|
|
448
|
+
model: 'test-model',
|
|
449
|
+
},
|
|
450
|
+
instructions: 'test',
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
expect(session.sessionPath).toBe(sessionPath);
|
|
455
|
+
expect(session.getSessionStore()?.header.id).not.toBe(existing.header.id);
|
|
456
|
+
expect(session.getSessionStore()?.getMessages()).toEqual([]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('preserves non-message state while applying session history', async () => {
|
|
460
|
+
const mockRun = createMockRun('stateful output');
|
|
461
|
+
mockRunCreate(mockRun);
|
|
462
|
+
const session = await createAgentSession({
|
|
463
|
+
cwd: dir,
|
|
464
|
+
runId: 'template-run',
|
|
465
|
+
graphConfig: {
|
|
466
|
+
type: 'standard',
|
|
467
|
+
llmConfig: {
|
|
468
|
+
provider: 'openAI' as never,
|
|
469
|
+
model: 'test-model',
|
|
470
|
+
},
|
|
471
|
+
instructions: 'test',
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
await session.getSessionStore()?.appendMessage(new HumanMessage('history'));
|
|
475
|
+
const input: t.IState & { selectedAgent: string } = {
|
|
476
|
+
messages: [new HumanMessage('fresh')],
|
|
477
|
+
selectedAgent: 'subagent-a',
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
await session.run(input);
|
|
481
|
+
|
|
482
|
+
const processedState = getProcessedState(mockRun) as t.IState & {
|
|
483
|
+
selectedAgent?: string;
|
|
484
|
+
};
|
|
485
|
+
expect(processedState.selectedAgent).toBe('subagent-a');
|
|
486
|
+
expect(processedState.messages.map((message) => message.content)).toEqual([
|
|
487
|
+
'history',
|
|
488
|
+
'fresh',
|
|
489
|
+
]);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('restores persisted summary token counts into run config', async () => {
|
|
493
|
+
const mockRun = createMockRun('with summary');
|
|
494
|
+
const capturedConfigs = mockRunCreate(mockRun);
|
|
495
|
+
const session = await createAgentSession({
|
|
496
|
+
cwd: dir,
|
|
497
|
+
runId: 'template-run',
|
|
498
|
+
graphConfig: {
|
|
499
|
+
type: 'standard',
|
|
500
|
+
llmConfig: {
|
|
501
|
+
provider: 'openAI' as never,
|
|
502
|
+
model: 'test-model',
|
|
503
|
+
},
|
|
504
|
+
instructions: 'test',
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
await session.getSessionStore()?.appendEntryForCompaction({
|
|
508
|
+
text: 'stored summary',
|
|
509
|
+
tokenCount: 123,
|
|
510
|
+
retainedEntryIds: [],
|
|
511
|
+
summarizedEntryIds: [],
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
await session.run('fresh');
|
|
515
|
+
|
|
516
|
+
const { graphConfig } = capturedConfigs[0];
|
|
517
|
+
const initialSummary =
|
|
518
|
+
'initialSummary' in graphConfig ? graphConfig.initialSummary : undefined;
|
|
519
|
+
expect(initialSummary).toEqual({
|
|
520
|
+
text: 'stored summary',
|
|
521
|
+
tokenCount: 123,
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('preserves custom handlers outside the session event adapter set', async () => {
|
|
526
|
+
const mockRun = createMockRun('handled');
|
|
527
|
+
const capturedConfigs = mockRunCreate(mockRun);
|
|
528
|
+
const agentLogHandler: t.EventHandler = { handle: jest.fn() };
|
|
529
|
+
const messageDeltaHandler: t.EventHandler = { handle: jest.fn() };
|
|
530
|
+
const session = await createAgentSession({
|
|
531
|
+
cwd: dir,
|
|
532
|
+
runId: 'template-run',
|
|
533
|
+
graphConfig: {
|
|
534
|
+
type: 'standard',
|
|
535
|
+
llmConfig: {
|
|
536
|
+
provider: 'openAI' as never,
|
|
537
|
+
model: 'test-model',
|
|
538
|
+
},
|
|
539
|
+
instructions: 'test',
|
|
540
|
+
},
|
|
541
|
+
customHandlers: {
|
|
542
|
+
[GraphEvents.ON_AGENT_LOG]: agentLogHandler,
|
|
543
|
+
[GraphEvents.ON_MESSAGE_DELTA]: messageDeltaHandler,
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
await session.run('start');
|
|
548
|
+
|
|
549
|
+
expect(capturedConfigs[0].customHandlers?.[GraphEvents.ON_AGENT_LOG]).toBe(
|
|
550
|
+
agentLogHandler
|
|
551
|
+
);
|
|
552
|
+
expect(
|
|
553
|
+
capturedConfigs[0].customHandlers?.[GraphEvents.ON_MESSAGE_DELTA]
|
|
554
|
+
).not.toBe(messageDeltaHandler);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('shares a session-level LangGraph checkpointer for HITL resume', async () => {
|
|
558
|
+
const session = await createAgentSession({
|
|
559
|
+
cwd: dir,
|
|
560
|
+
runId: 'template-run',
|
|
561
|
+
humanInTheLoop: { enabled: true },
|
|
562
|
+
graphConfig: {
|
|
563
|
+
type: 'standard',
|
|
564
|
+
llmConfig: {
|
|
565
|
+
provider: 'openAI' as never,
|
|
566
|
+
model: 'test-model',
|
|
567
|
+
},
|
|
568
|
+
instructions: 'test',
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
expect(session.getCheckpointer()).toBeInstanceOf(MemorySaver);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('keeps stores and checkpointing optional for high-level sessions', async () => {
|
|
576
|
+
const session = await createAgentSession({
|
|
577
|
+
cwd: dir,
|
|
578
|
+
runId: 'template-run',
|
|
579
|
+
ephemeral: true,
|
|
580
|
+
checkpointing: false,
|
|
581
|
+
humanInTheLoop: { enabled: true },
|
|
582
|
+
graphConfig: {
|
|
583
|
+
type: 'standard',
|
|
584
|
+
llmConfig: {
|
|
585
|
+
provider: 'openAI' as never,
|
|
586
|
+
model: 'test-model',
|
|
587
|
+
},
|
|
588
|
+
instructions: 'test',
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
expect(session.getSessionStore()).toBeUndefined();
|
|
593
|
+
expect(session.getCheckpointer()).toBeUndefined();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('removes graph checkpointers when checkpointing is disabled', async () => {
|
|
597
|
+
const graphCheckpointer = new MemorySaver();
|
|
598
|
+
const mockRun = createMockRun('disabled');
|
|
599
|
+
const capturedConfigs = mockRunCreate(mockRun);
|
|
600
|
+
const session = await createAgentSession({
|
|
601
|
+
cwd: dir,
|
|
602
|
+
runId: 'template-run',
|
|
603
|
+
checkpointing: false,
|
|
604
|
+
graphConfig: {
|
|
605
|
+
type: 'standard',
|
|
606
|
+
llmConfig: {
|
|
607
|
+
provider: 'openAI' as never,
|
|
608
|
+
model: 'test-model',
|
|
609
|
+
},
|
|
610
|
+
instructions: 'test',
|
|
611
|
+
compileOptions: { checkpointer: graphCheckpointer },
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
await session.run('start');
|
|
616
|
+
|
|
617
|
+
expect(session.getCheckpointer()).toBeUndefined();
|
|
618
|
+
expect(
|
|
619
|
+
capturedConfigs[0].graphConfig.compileOptions?.checkpointer
|
|
620
|
+
).toBeUndefined();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('reuses the session-level checkpointer across HITL resume', async () => {
|
|
624
|
+
const mockRun = createMockRun('resumed');
|
|
625
|
+
const capturedConfigs = mockRunCreate(mockRun);
|
|
626
|
+
const session = await createAgentSession({
|
|
627
|
+
cwd: dir,
|
|
628
|
+
runId: 'template-run',
|
|
629
|
+
humanInTheLoop: { enabled: true },
|
|
630
|
+
graphConfig: {
|
|
631
|
+
type: 'standard',
|
|
632
|
+
llmConfig: {
|
|
633
|
+
provider: 'openAI' as never,
|
|
634
|
+
model: 'test-model',
|
|
635
|
+
},
|
|
636
|
+
instructions: 'test',
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
await session.run('start');
|
|
641
|
+
await session.resumeInterrupt([]);
|
|
642
|
+
|
|
643
|
+
const checkpointer =
|
|
644
|
+
capturedConfigs[0].graphConfig.compileOptions?.checkpointer;
|
|
645
|
+
expect(checkpointer).toBeInstanceOf(MemorySaver);
|
|
646
|
+
expect(session.getCheckpointer()).toBe(checkpointer);
|
|
647
|
+
expect(capturedConfigs[1].graphConfig.compileOptions?.checkpointer).toBe(
|
|
648
|
+
checkpointer
|
|
649
|
+
);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('preserves a caller-supplied session checkpointer', async () => {
|
|
653
|
+
const checkpointer = new MemorySaver();
|
|
654
|
+
const session = await createAgentSession({
|
|
655
|
+
cwd: dir,
|
|
656
|
+
runId: 'template-run',
|
|
657
|
+
checkpointing: { checkpointer },
|
|
658
|
+
graphConfig: {
|
|
659
|
+
type: 'standard',
|
|
660
|
+
llmConfig: {
|
|
661
|
+
provider: 'openAI' as never,
|
|
662
|
+
model: 'test-model',
|
|
663
|
+
},
|
|
664
|
+
instructions: 'test',
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
expect(session.getCheckpointer()).toBe(checkpointer);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('injects the session checkpointer and replays JSONL history before checkpoints exist', async () => {
|
|
672
|
+
const checkpointer = new MemorySaver();
|
|
673
|
+
const mockRun = createMockRun('first output');
|
|
674
|
+
const capturedConfigs = mockRunCreate(mockRun);
|
|
675
|
+
const session = await createAgentSession({
|
|
676
|
+
cwd: dir,
|
|
677
|
+
runId: 'template-run',
|
|
678
|
+
checkpointing: { checkpointer },
|
|
679
|
+
graphConfig: {
|
|
680
|
+
type: 'standard',
|
|
681
|
+
llmConfig: {
|
|
682
|
+
provider: 'openAI' as never,
|
|
683
|
+
model: 'test-model',
|
|
684
|
+
},
|
|
685
|
+
instructions: 'test',
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
await session.getSessionStore()?.appendMessage(new HumanMessage('history'));
|
|
689
|
+
|
|
690
|
+
await session.run('next');
|
|
691
|
+
|
|
692
|
+
expect(capturedConfigs[0].graphConfig.compileOptions?.checkpointer).toBe(
|
|
693
|
+
checkpointer
|
|
694
|
+
);
|
|
695
|
+
expect(
|
|
696
|
+
getProcessedMessages(mockRun).map((message) => message.content)
|
|
697
|
+
).toEqual(['history', 'next']);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('does not replay session history when overriding thread id without checkpoint state', async () => {
|
|
701
|
+
const checkpointer = new MemorySaver();
|
|
702
|
+
const mockRun = createMockRun('override output');
|
|
703
|
+
mockRunCreate(mockRun);
|
|
704
|
+
const session = await createAgentSession({
|
|
705
|
+
cwd: dir,
|
|
706
|
+
runId: 'template-run',
|
|
707
|
+
checkpointing: { checkpointer },
|
|
708
|
+
graphConfig: {
|
|
709
|
+
type: 'standard',
|
|
710
|
+
llmConfig: {
|
|
711
|
+
provider: 'openAI' as never,
|
|
712
|
+
model: 'test-model',
|
|
713
|
+
},
|
|
714
|
+
instructions: 'test',
|
|
715
|
+
},
|
|
716
|
+
});
|
|
717
|
+
await session.getSessionStore()?.appendMessage(new HumanMessage('history'));
|
|
718
|
+
|
|
719
|
+
await session.run('fresh turn', { threadId: 'thread_override' });
|
|
720
|
+
|
|
721
|
+
expect(
|
|
722
|
+
getProcessedMessages(mockRun).map((message) => message.content)
|
|
723
|
+
).toEqual(['fresh turn']);
|
|
724
|
+
expect(
|
|
725
|
+
session
|
|
726
|
+
.getSessionStore()
|
|
727
|
+
?.getPath()
|
|
728
|
+
.filter((entry) => entry.type === 'message')
|
|
729
|
+
.map((entry) => entry.data.message.content)
|
|
730
|
+
).toEqual(['history']);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('does not persist resumed override thread messages into the session path', async () => {
|
|
734
|
+
const checkpointer = new MemorySaver();
|
|
735
|
+
const mockRun = createMockRun('override resumed');
|
|
736
|
+
mockRunCreate(mockRun);
|
|
737
|
+
const session = await createAgentSession({
|
|
738
|
+
cwd: dir,
|
|
739
|
+
runId: 'template-run',
|
|
740
|
+
checkpointing: { checkpointer },
|
|
741
|
+
graphConfig: {
|
|
742
|
+
type: 'standard',
|
|
743
|
+
llmConfig: {
|
|
744
|
+
provider: 'openAI' as never,
|
|
745
|
+
model: 'test-model',
|
|
746
|
+
},
|
|
747
|
+
instructions: 'test',
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
await session.getSessionStore()?.appendMessage(new HumanMessage('history'));
|
|
751
|
+
|
|
752
|
+
await session.resumeInterrupt([], { threadId: 'thread_override' });
|
|
753
|
+
|
|
754
|
+
expect(mockRun.resume).toHaveBeenCalledWith(
|
|
755
|
+
[],
|
|
756
|
+
expect.objectContaining({
|
|
757
|
+
configurable: expect.objectContaining({
|
|
758
|
+
thread_id: 'thread_override',
|
|
759
|
+
}),
|
|
760
|
+
})
|
|
761
|
+
);
|
|
762
|
+
expect(
|
|
763
|
+
session
|
|
764
|
+
.getSessionStore()
|
|
765
|
+
?.getPath()
|
|
766
|
+
.filter((entry) => entry.type === 'message')
|
|
767
|
+
.map((entry) => entry.data.message.content)
|
|
768
|
+
).toEqual(['history']);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it('resumes isolated Run instances from the stored interrupted checkpoint', async () => {
|
|
772
|
+
const checkpointer = new MemorySaver();
|
|
773
|
+
const mockRun = createMockRun('resumed');
|
|
774
|
+
mockRunCreate(mockRun);
|
|
775
|
+
const session = await createAgentSession({
|
|
776
|
+
cwd: dir,
|
|
777
|
+
runId: 'template-run',
|
|
778
|
+
checkpointing: { checkpointer },
|
|
779
|
+
graphConfig: {
|
|
780
|
+
type: 'standard',
|
|
781
|
+
llmConfig: {
|
|
782
|
+
provider: 'openAI' as never,
|
|
783
|
+
model: 'test-model',
|
|
784
|
+
},
|
|
785
|
+
instructions: 'test',
|
|
786
|
+
},
|
|
787
|
+
});
|
|
788
|
+
await putCheckpoint({
|
|
789
|
+
checkpointer,
|
|
790
|
+
threadId: session.threadId,
|
|
791
|
+
id: 'checkpoint_interrupted',
|
|
792
|
+
});
|
|
793
|
+
await session.getSessionStore()?.appendCheckpoint({
|
|
794
|
+
source: 'run',
|
|
795
|
+
threadId: session.threadId,
|
|
796
|
+
runId: 'run_interrupted',
|
|
797
|
+
checkpointId: 'checkpoint_interrupted',
|
|
798
|
+
checkpointNs: '',
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
await session.resumeInterrupt([{ type: 'approve' }]);
|
|
802
|
+
|
|
803
|
+
expect(mockRun.resume).toHaveBeenCalledWith(
|
|
804
|
+
[{ type: 'approve' }],
|
|
805
|
+
expect.objectContaining({
|
|
806
|
+
configurable: expect.objectContaining({
|
|
807
|
+
thread_id: session.threadId,
|
|
808
|
+
checkpoint_id: 'checkpoint_interrupted',
|
|
809
|
+
checkpoint_ns: '',
|
|
810
|
+
}),
|
|
811
|
+
})
|
|
812
|
+
);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('records a new interrupt checkpoint after resuming from an older checkpoint', async () => {
|
|
816
|
+
const checkpointer = new MemorySaver();
|
|
817
|
+
const mockRun = createMockRun('interrupted again');
|
|
818
|
+
mockRun.getInterrupt.mockReturnValue({
|
|
819
|
+
interruptId: 'interrupt_again',
|
|
820
|
+
threadId: 'thread_new_interrupt',
|
|
821
|
+
checkpointId: 'checkpoint_new_interrupt',
|
|
822
|
+
checkpointNs: '',
|
|
823
|
+
payload: {
|
|
824
|
+
type: 'tool_approval',
|
|
825
|
+
action_requests: [],
|
|
826
|
+
review_configs: [],
|
|
827
|
+
},
|
|
828
|
+
});
|
|
829
|
+
mockRunCreate(mockRun);
|
|
830
|
+
const session = await createAgentSession({
|
|
831
|
+
cwd: dir,
|
|
832
|
+
runId: 'template-run',
|
|
833
|
+
checkpointing: { checkpointer },
|
|
834
|
+
graphConfig: {
|
|
835
|
+
type: 'standard',
|
|
836
|
+
llmConfig: {
|
|
837
|
+
provider: 'openAI' as never,
|
|
838
|
+
model: 'test-model',
|
|
839
|
+
},
|
|
840
|
+
instructions: 'test',
|
|
841
|
+
},
|
|
842
|
+
});
|
|
843
|
+
await putCheckpoint({
|
|
844
|
+
checkpointer,
|
|
845
|
+
threadId: session.threadId,
|
|
846
|
+
id: 'checkpoint_resume_source',
|
|
847
|
+
});
|
|
848
|
+
await putCheckpoint({
|
|
849
|
+
checkpointer,
|
|
850
|
+
threadId: session.threadId,
|
|
851
|
+
id: 'checkpoint_new_interrupt',
|
|
852
|
+
});
|
|
853
|
+
await session.getSessionStore()?.appendCheckpoint({
|
|
854
|
+
source: 'run',
|
|
855
|
+
threadId: session.threadId,
|
|
856
|
+
runId: 'run_interrupted',
|
|
857
|
+
checkpointId: 'checkpoint_resume_source',
|
|
858
|
+
checkpointNs: '',
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
await session.resumeInterrupt([{ type: 'approve' }]);
|
|
862
|
+
|
|
863
|
+
expect(
|
|
864
|
+
session.getSessionStore()?.getLatestCheckpoint(session.threadId)?.data
|
|
865
|
+
).toMatchObject({
|
|
866
|
+
source: 'resume',
|
|
867
|
+
checkpointId: 'checkpoint_new_interrupt',
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
it('resumes isolated Run instances from the requested checkpoint namespace', async () => {
|
|
872
|
+
const checkpointer = new MemorySaver();
|
|
873
|
+
const mockRun = createMockRun('resumed requested namespace');
|
|
874
|
+
mockRunCreate(mockRun);
|
|
875
|
+
const session = await createAgentSession({
|
|
876
|
+
cwd: dir,
|
|
877
|
+
runId: 'template-run',
|
|
878
|
+
checkpointing: { checkpointer },
|
|
879
|
+
graphConfig: {
|
|
880
|
+
type: 'standard',
|
|
881
|
+
llmConfig: {
|
|
882
|
+
provider: 'openAI' as never,
|
|
883
|
+
model: 'test-model',
|
|
884
|
+
},
|
|
885
|
+
instructions: 'test',
|
|
886
|
+
},
|
|
887
|
+
});
|
|
888
|
+
await session.getSessionStore()?.appendCheckpoint({
|
|
889
|
+
source: 'run',
|
|
890
|
+
threadId: session.threadId,
|
|
891
|
+
runId: 'run_default',
|
|
892
|
+
checkpointId: 'checkpoint_default_latest',
|
|
893
|
+
checkpointNs: '',
|
|
894
|
+
});
|
|
895
|
+
await session.getSessionStore()?.appendCheckpoint({
|
|
896
|
+
source: 'run',
|
|
897
|
+
threadId: session.threadId,
|
|
898
|
+
runId: 'run_requested',
|
|
899
|
+
checkpointId: 'checkpoint_requested_latest',
|
|
900
|
+
checkpointNs: 'requested',
|
|
901
|
+
});
|
|
902
|
+
await session.getSessionStore()?.appendCheckpoint({
|
|
903
|
+
source: 'run',
|
|
904
|
+
threadId: session.threadId,
|
|
905
|
+
runId: 'run_default_again',
|
|
906
|
+
checkpointId: 'checkpoint_default_newer',
|
|
907
|
+
checkpointNs: '',
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
await session.resumeInterrupt([{ type: 'approve' }], {
|
|
911
|
+
config: {
|
|
912
|
+
configurable: {
|
|
913
|
+
checkpoint_ns: 'requested',
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
expect(mockRun.resume).toHaveBeenCalledWith(
|
|
919
|
+
[{ type: 'approve' }],
|
|
920
|
+
expect.objectContaining({
|
|
921
|
+
configurable: expect.objectContaining({
|
|
922
|
+
thread_id: session.threadId,
|
|
923
|
+
checkpoint_id: 'checkpoint_requested_latest',
|
|
924
|
+
checkpoint_ns: 'requested',
|
|
925
|
+
}),
|
|
926
|
+
})
|
|
927
|
+
);
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
it('resumes isolated Run instances from the explicitly requested default namespace', async () => {
|
|
931
|
+
const checkpointer = new MemorySaver();
|
|
932
|
+
const mockRun = createMockRun('resumed default namespace');
|
|
933
|
+
mockRunCreate(mockRun);
|
|
934
|
+
const session = await createAgentSession({
|
|
935
|
+
cwd: dir,
|
|
936
|
+
runId: 'template-run',
|
|
937
|
+
checkpointing: { checkpointer },
|
|
938
|
+
graphConfig: {
|
|
939
|
+
type: 'standard',
|
|
940
|
+
llmConfig: {
|
|
941
|
+
provider: 'openAI' as never,
|
|
942
|
+
model: 'test-model',
|
|
943
|
+
},
|
|
944
|
+
instructions: 'test',
|
|
945
|
+
},
|
|
946
|
+
});
|
|
947
|
+
await session.getSessionStore()?.appendCheckpoint({
|
|
948
|
+
source: 'run',
|
|
949
|
+
threadId: session.threadId,
|
|
950
|
+
runId: 'run_default',
|
|
951
|
+
checkpointId: 'checkpoint_default_latest',
|
|
952
|
+
checkpointNs: '',
|
|
953
|
+
});
|
|
954
|
+
await session.getSessionStore()?.appendCheckpoint({
|
|
955
|
+
source: 'run',
|
|
956
|
+
threadId: session.threadId,
|
|
957
|
+
runId: 'run_requested_newer',
|
|
958
|
+
checkpointId: 'checkpoint_requested_newer',
|
|
959
|
+
checkpointNs: 'requested',
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
await session.resumeInterrupt([{ type: 'approve' }], {
|
|
963
|
+
config: {
|
|
964
|
+
configurable: {
|
|
965
|
+
checkpoint_ns: '',
|
|
966
|
+
},
|
|
967
|
+
},
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
expect(mockRun.resume).toHaveBeenCalledWith(
|
|
971
|
+
[{ type: 'approve' }],
|
|
972
|
+
expect.objectContaining({
|
|
973
|
+
configurable: expect.objectContaining({
|
|
974
|
+
thread_id: session.threadId,
|
|
975
|
+
checkpoint_id: 'checkpoint_default_latest',
|
|
976
|
+
checkpoint_ns: '',
|
|
977
|
+
}),
|
|
978
|
+
})
|
|
979
|
+
);
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
it('uses only new input when LangGraph checkpoint state already exists', async () => {
|
|
983
|
+
const checkpointer = new MemorySaver();
|
|
984
|
+
const mockRun = createMockRun('checkpointed output');
|
|
985
|
+
mockRunCreate(mockRun);
|
|
986
|
+
const session = await createAgentSession({
|
|
987
|
+
cwd: dir,
|
|
988
|
+
runId: 'template-run',
|
|
989
|
+
checkpointing: { checkpointer },
|
|
990
|
+
graphConfig: {
|
|
991
|
+
type: 'standard',
|
|
992
|
+
llmConfig: {
|
|
993
|
+
provider: 'openAI' as never,
|
|
994
|
+
model: 'test-model',
|
|
995
|
+
},
|
|
996
|
+
instructions: 'test',
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
await session.getSessionStore()?.appendMessage(new HumanMessage('history'));
|
|
1000
|
+
await putCheckpoint({
|
|
1001
|
+
checkpointer,
|
|
1002
|
+
threadId: session.threadId,
|
|
1003
|
+
id: 'checkpoint_existing',
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
await session.run('fresh turn', { runId: 'run_checkpointed' });
|
|
1007
|
+
|
|
1008
|
+
const checkpoints = session
|
|
1009
|
+
.getSessionStore()
|
|
1010
|
+
?.getCheckpoints(session.threadId);
|
|
1011
|
+
expect(
|
|
1012
|
+
getProcessedMessages(mockRun).map((message) => message.content)
|
|
1013
|
+
).toEqual(['fresh turn']);
|
|
1014
|
+
expect(checkpoints?.at(-1)?.data).toMatchObject({
|
|
1015
|
+
source: 'run',
|
|
1016
|
+
runId: 'run_checkpointed',
|
|
1017
|
+
checkpointId: 'checkpoint_existing',
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
it('replays JSONL history when the requested checkpoint namespace has no state', async () => {
|
|
1022
|
+
const checkpointer = new MemorySaver();
|
|
1023
|
+
const mockRun = createMockRun('namespace output');
|
|
1024
|
+
mockRunCreate(mockRun);
|
|
1025
|
+
const session = await createAgentSession({
|
|
1026
|
+
cwd: dir,
|
|
1027
|
+
runId: 'template-run',
|
|
1028
|
+
checkpointing: { checkpointer },
|
|
1029
|
+
graphConfig: {
|
|
1030
|
+
type: 'standard',
|
|
1031
|
+
llmConfig: {
|
|
1032
|
+
provider: 'openAI' as never,
|
|
1033
|
+
model: 'test-model',
|
|
1034
|
+
},
|
|
1035
|
+
instructions: 'test',
|
|
1036
|
+
},
|
|
1037
|
+
});
|
|
1038
|
+
await session.getSessionStore()?.appendMessage(new HumanMessage('history'));
|
|
1039
|
+
await putCheckpoint({
|
|
1040
|
+
checkpointer,
|
|
1041
|
+
threadId: session.threadId,
|
|
1042
|
+
id: 'checkpoint_other_namespace',
|
|
1043
|
+
checkpointNs: 'other',
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
await session.run('fresh turn', {
|
|
1047
|
+
config: { configurable: { checkpoint_ns: 'requested' } },
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
expect(
|
|
1051
|
+
getProcessedMessages(mockRun).map((message) => message.content)
|
|
1052
|
+
).toEqual(['history', 'fresh turn']);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
it('looks up the latest checkpoint in the requested namespace', async () => {
|
|
1056
|
+
const checkpointer = new MemorySaver();
|
|
1057
|
+
const session = await createAgentSession({
|
|
1058
|
+
cwd: dir,
|
|
1059
|
+
runId: 'template-run',
|
|
1060
|
+
checkpointing: { checkpointer },
|
|
1061
|
+
graphConfig: {
|
|
1062
|
+
type: 'standard',
|
|
1063
|
+
llmConfig: {
|
|
1064
|
+
provider: 'openAI' as never,
|
|
1065
|
+
model: 'test-model',
|
|
1066
|
+
},
|
|
1067
|
+
instructions: 'test',
|
|
1068
|
+
},
|
|
1069
|
+
});
|
|
1070
|
+
await putCheckpoint({
|
|
1071
|
+
checkpointer,
|
|
1072
|
+
threadId: session.threadId,
|
|
1073
|
+
id: 'checkpoint_requested_namespace',
|
|
1074
|
+
checkpointNs: 'requested',
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
await expect(session.getLatestCheckpoint()).resolves.toBeUndefined();
|
|
1078
|
+
await expect(
|
|
1079
|
+
session.getLatestCheckpoint({ checkpointNs: 'requested' })
|
|
1080
|
+
).resolves.toMatchObject({
|
|
1081
|
+
checkpointId: 'checkpoint_requested_namespace',
|
|
1082
|
+
checkpointNs: 'requested',
|
|
1083
|
+
});
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
it('resets stale checkpoint state when branching changes the active JSONL path', async () => {
|
|
1087
|
+
const checkpointer = new MemorySaver();
|
|
1088
|
+
const session = await createAgentSession({
|
|
1089
|
+
cwd: dir,
|
|
1090
|
+
runId: 'template-run',
|
|
1091
|
+
checkpointing: { checkpointer },
|
|
1092
|
+
graphConfig: {
|
|
1093
|
+
type: 'standard',
|
|
1094
|
+
llmConfig: {
|
|
1095
|
+
provider: 'openAI' as never,
|
|
1096
|
+
model: 'test-model',
|
|
1097
|
+
},
|
|
1098
|
+
instructions: 'test',
|
|
1099
|
+
},
|
|
1100
|
+
});
|
|
1101
|
+
const store = session.getSessionStore();
|
|
1102
|
+
const first = await store?.appendMessage(new HumanMessage('first'));
|
|
1103
|
+
await store?.appendMessage(new AIMessage('second'));
|
|
1104
|
+
await putCheckpoint({
|
|
1105
|
+
checkpointer,
|
|
1106
|
+
threadId: session.threadId,
|
|
1107
|
+
id: 'checkpoint_to_reset',
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
await session.branch(first?.id ?? '', { position: 'at' });
|
|
1111
|
+
|
|
1112
|
+
const tuple = await checkpointer.getTuple({
|
|
1113
|
+
configurable: { thread_id: session.threadId },
|
|
1114
|
+
});
|
|
1115
|
+
expect(tuple).toBeUndefined();
|
|
1116
|
+
expect(store?.getCheckpoints(session.threadId).at(-1)?.data).toMatchObject({
|
|
1117
|
+
source: 'reset',
|
|
1118
|
+
reason: 'branch',
|
|
1119
|
+
});
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
it('keeps checkpoint state when branching to the active JSONL leaf', async () => {
|
|
1123
|
+
const checkpointer = new MemorySaver();
|
|
1124
|
+
const session = await createAgentSession({
|
|
1125
|
+
cwd: dir,
|
|
1126
|
+
runId: 'template-run',
|
|
1127
|
+
checkpointing: { checkpointer },
|
|
1128
|
+
graphConfig: {
|
|
1129
|
+
type: 'standard',
|
|
1130
|
+
llmConfig: {
|
|
1131
|
+
provider: 'openAI' as never,
|
|
1132
|
+
model: 'test-model',
|
|
1133
|
+
},
|
|
1134
|
+
instructions: 'test',
|
|
1135
|
+
},
|
|
1136
|
+
});
|
|
1137
|
+
const store = session.getSessionStore();
|
|
1138
|
+
const active = await store?.appendMessage(new HumanMessage('current'));
|
|
1139
|
+
await putCheckpoint({
|
|
1140
|
+
checkpointer,
|
|
1141
|
+
threadId: session.threadId,
|
|
1142
|
+
id: 'checkpoint_to_keep',
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
await session.branch(active?.id ?? '', { position: 'at' });
|
|
1146
|
+
|
|
1147
|
+
const tuple = await checkpointer.getTuple({
|
|
1148
|
+
configurable: { thread_id: session.threadId },
|
|
1149
|
+
});
|
|
1150
|
+
expect(tuple?.checkpoint.id).toBe('checkpoint_to_keep');
|
|
1151
|
+
expect(
|
|
1152
|
+
store
|
|
1153
|
+
?.getCheckpoints(session.threadId)
|
|
1154
|
+
.some((checkpoint) => checkpoint.data.source === 'reset')
|
|
1155
|
+
).toBe(false);
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
it('resets overridden thread checkpoints when branching changes the active path', async () => {
|
|
1159
|
+
const checkpointer = new MemorySaver();
|
|
1160
|
+
const session = await createAgentSession({
|
|
1161
|
+
cwd: dir,
|
|
1162
|
+
runId: 'template-run',
|
|
1163
|
+
checkpointing: { checkpointer },
|
|
1164
|
+
graphConfig: {
|
|
1165
|
+
type: 'standard',
|
|
1166
|
+
llmConfig: {
|
|
1167
|
+
provider: 'openAI' as never,
|
|
1168
|
+
model: 'test-model',
|
|
1169
|
+
},
|
|
1170
|
+
instructions: 'test',
|
|
1171
|
+
},
|
|
1172
|
+
});
|
|
1173
|
+
const store = session.getSessionStore();
|
|
1174
|
+
const first = await store?.appendMessage(new HumanMessage('first'));
|
|
1175
|
+
await store?.appendMessage(new AIMessage('second'));
|
|
1176
|
+
await putCheckpoint({
|
|
1177
|
+
checkpointer,
|
|
1178
|
+
threadId: 'thread_override',
|
|
1179
|
+
id: 'checkpoint_override',
|
|
1180
|
+
});
|
|
1181
|
+
await store?.appendCheckpoint({
|
|
1182
|
+
source: 'run',
|
|
1183
|
+
threadId: 'thread_override',
|
|
1184
|
+
runId: 'run_override',
|
|
1185
|
+
checkpointId: 'checkpoint_override',
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
await session.branch(first?.id ?? '', { position: 'at' });
|
|
1189
|
+
|
|
1190
|
+
const tuple = await checkpointer.getTuple({
|
|
1191
|
+
configurable: { thread_id: 'thread_override' },
|
|
1192
|
+
});
|
|
1193
|
+
const reset = store
|
|
1194
|
+
?.getCheckpoints('thread_override')
|
|
1195
|
+
.find((checkpoint) => checkpoint.data.source === 'reset');
|
|
1196
|
+
expect(tuple).toBeUndefined();
|
|
1197
|
+
expect(reset?.data.reason).toBe('branch');
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
it('records run.failed when resumeInterrupt throws', async () => {
|
|
1201
|
+
const mockRun = createMockRun('unused');
|
|
1202
|
+
mockRun.resume.mockRejectedValue(new Error('resume failed'));
|
|
1203
|
+
mockRunCreate(mockRun);
|
|
1204
|
+
const session = await createAgentSession({
|
|
1205
|
+
cwd: dir,
|
|
1206
|
+
runId: 'template-run',
|
|
1207
|
+
humanInTheLoop: { enabled: true },
|
|
1208
|
+
graphConfig: {
|
|
1209
|
+
type: 'standard',
|
|
1210
|
+
llmConfig: {
|
|
1211
|
+
provider: 'openAI' as never,
|
|
1212
|
+
model: 'test-model',
|
|
1213
|
+
},
|
|
1214
|
+
instructions: 'test',
|
|
1215
|
+
},
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
await expect(
|
|
1219
|
+
session.resumeInterrupt([{ type: 'approve' }], {
|
|
1220
|
+
runId: 'run_resume_failure',
|
|
1221
|
+
})
|
|
1222
|
+
).rejects.toThrow('resume failed');
|
|
1223
|
+
|
|
1224
|
+
const events = session
|
|
1225
|
+
.getSessionStore()
|
|
1226
|
+
?.getEntries()
|
|
1227
|
+
.filter((entry) => entry.type === 'run_event')
|
|
1228
|
+
.map((entry) => entry.data.event);
|
|
1229
|
+
expect(events).toEqual(['run.started', 'run.failed']);
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
it('compacts into a summary plus retained active path', async () => {
|
|
1233
|
+
mockSummarizer('summary of old work');
|
|
1234
|
+
const tokenCounter: t.TokenCounter = () => 7;
|
|
1235
|
+
const session = await createAgentSession({
|
|
1236
|
+
cwd: dir,
|
|
1237
|
+
runId: 'template-run',
|
|
1238
|
+
tokenCounter,
|
|
1239
|
+
graphConfig: {
|
|
1240
|
+
type: 'standard',
|
|
1241
|
+
llmConfig: {
|
|
1242
|
+
provider: 'openAI' as never,
|
|
1243
|
+
model: 'test-model',
|
|
1244
|
+
},
|
|
1245
|
+
instructions: 'test',
|
|
1246
|
+
},
|
|
1247
|
+
});
|
|
1248
|
+
const store = session.getSessionStore();
|
|
1249
|
+
await store?.appendMessage(new HumanMessage('old'));
|
|
1250
|
+
await store?.appendMessage(new AIMessage('old answer'));
|
|
1251
|
+
await store?.appendMessage(new HumanMessage('recent'));
|
|
1252
|
+
|
|
1253
|
+
await session.compact({
|
|
1254
|
+
instructions: 'summary of old work',
|
|
1255
|
+
retainRecentTurns: 1,
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
expect(store?.getMessages().map((message) => message.content)).toEqual([
|
|
1259
|
+
'summary of old work',
|
|
1260
|
+
'recent',
|
|
1261
|
+
]);
|
|
1262
|
+
const summary = store
|
|
1263
|
+
?.getEntries()
|
|
1264
|
+
.find((entry) => entry.type === 'summary');
|
|
1265
|
+
expect(summary?.data.tokenCount).toBeGreaterThan(0);
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
it('records no retained ids when compaction retains zero messages', async () => {
|
|
1269
|
+
mockSummarizer('summary of everything');
|
|
1270
|
+
const session = await createAgentSession({
|
|
1271
|
+
cwd: dir,
|
|
1272
|
+
runId: 'template-run',
|
|
1273
|
+
graphConfig: {
|
|
1274
|
+
type: 'standard',
|
|
1275
|
+
llmConfig: {
|
|
1276
|
+
provider: 'openAI' as never,
|
|
1277
|
+
model: 'test-model',
|
|
1278
|
+
},
|
|
1279
|
+
instructions: 'test',
|
|
1280
|
+
},
|
|
1281
|
+
});
|
|
1282
|
+
const store = session.getSessionStore();
|
|
1283
|
+
const user = await store?.appendMessage(new HumanMessage('old'));
|
|
1284
|
+
const assistant = await store?.appendMessage(new AIMessage('old answer'));
|
|
1285
|
+
|
|
1286
|
+
await session.compact({ retainRecentTurns: 0 });
|
|
1287
|
+
|
|
1288
|
+
const summary = store
|
|
1289
|
+
?.getEntries()
|
|
1290
|
+
.find((entry) => entry.type === 'summary');
|
|
1291
|
+
const compaction = store
|
|
1292
|
+
?.getEntries()
|
|
1293
|
+
.find((entry) => entry.type === 'compaction');
|
|
1294
|
+
expect(summary?.data.retainedEntryIds).toEqual([]);
|
|
1295
|
+
expect(summary?.data.summarizedEntryIds).toEqual([user?.id, assistant?.id]);
|
|
1296
|
+
expect(compaction?.data.retainedEntryIds).toEqual([]);
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
it('carries calibration ratio forward after resumeInterrupt', async () => {
|
|
1300
|
+
const mockRun = createMockRun('resumed');
|
|
1301
|
+
mockRun.getCalibrationRatio.mockReturnValue(2);
|
|
1302
|
+
const capturedConfigs = mockRunCreate(mockRun);
|
|
1303
|
+
const session = await createAgentSession({
|
|
1304
|
+
cwd: dir,
|
|
1305
|
+
runId: 'template-run',
|
|
1306
|
+
graphConfig: {
|
|
1307
|
+
type: 'standard',
|
|
1308
|
+
llmConfig: {
|
|
1309
|
+
provider: 'openAI' as never,
|
|
1310
|
+
model: 'test-model',
|
|
1311
|
+
},
|
|
1312
|
+
instructions: 'test',
|
|
1313
|
+
},
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
await session.resumeInterrupt([]);
|
|
1317
|
+
await session.run('after resume');
|
|
1318
|
+
|
|
1319
|
+
expect(capturedConfigs[1].calibrationRatio).toBe(2);
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
it('summarizes an abandoned branch before switching in place', async () => {
|
|
1323
|
+
mockSummarizer('summary of abandoned branch');
|
|
1324
|
+
const session = await createAgentSession({
|
|
1325
|
+
cwd: dir,
|
|
1326
|
+
runId: 'template-run',
|
|
1327
|
+
graphConfig: {
|
|
1328
|
+
type: 'standard',
|
|
1329
|
+
llmConfig: {
|
|
1330
|
+
provider: 'openAI' as never,
|
|
1331
|
+
model: 'test-model',
|
|
1332
|
+
},
|
|
1333
|
+
instructions: 'test',
|
|
1334
|
+
},
|
|
1335
|
+
});
|
|
1336
|
+
const store = session.getSessionStore();
|
|
1337
|
+
const first = await store?.appendMessage(new HumanMessage('first'));
|
|
1338
|
+
const abandoned = await store?.appendMessage(
|
|
1339
|
+
new AIMessage('abandoned answer')
|
|
1340
|
+
);
|
|
1341
|
+
|
|
1342
|
+
await session.branch(first?.id ?? '', {
|
|
1343
|
+
position: 'at',
|
|
1344
|
+
summarizeAbandoned: {
|
|
1345
|
+
instructions: 'summarize abandoned branch',
|
|
1346
|
+
},
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
const activePath = store?.getPath();
|
|
1350
|
+
const summary = activePath?.at(-1);
|
|
1351
|
+
expect(activePath?.map((entry) => entry.id)).toEqual([
|
|
1352
|
+
first?.id,
|
|
1353
|
+
summary?.id,
|
|
1354
|
+
]);
|
|
1355
|
+
expect(summary).toMatchObject({
|
|
1356
|
+
type: 'summary',
|
|
1357
|
+
parentId: first?.id,
|
|
1358
|
+
data: {
|
|
1359
|
+
text: 'summary of abandoned branch',
|
|
1360
|
+
summarizedEntryIds: [abandoned?.id],
|
|
1361
|
+
instructions: 'summarize abandoned branch',
|
|
1362
|
+
},
|
|
1363
|
+
});
|
|
1364
|
+
expect(
|
|
1365
|
+
store?.getEntries().some((entry) => entry.type === 'compaction')
|
|
1366
|
+
).toBe(true);
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
it('summarizes a sibling branch before switching branches', async () => {
|
|
1370
|
+
mockSummarizer('summary of sibling branch');
|
|
1371
|
+
const session = await createAgentSession({
|
|
1372
|
+
cwd: dir,
|
|
1373
|
+
runId: 'template-run',
|
|
1374
|
+
graphConfig: {
|
|
1375
|
+
type: 'standard',
|
|
1376
|
+
llmConfig: {
|
|
1377
|
+
provider: 'openAI' as never,
|
|
1378
|
+
model: 'test-model',
|
|
1379
|
+
},
|
|
1380
|
+
instructions: 'test',
|
|
1381
|
+
},
|
|
1382
|
+
});
|
|
1383
|
+
const store = session.getSessionStore();
|
|
1384
|
+
const first = await store?.appendMessage(new HumanMessage('first'));
|
|
1385
|
+
const inactive = await store?.appendMessage(new AIMessage('inactive'));
|
|
1386
|
+
await store?.branch(first?.id ?? '');
|
|
1387
|
+
const activeSibling = await store?.appendMessage(new AIMessage('active'));
|
|
1388
|
+
|
|
1389
|
+
await session.branch(inactive?.id ?? '', {
|
|
1390
|
+
position: 'at',
|
|
1391
|
+
summarizeAbandoned: true,
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
const activePath = store?.getPath();
|
|
1395
|
+
const summary = activePath?.at(-1);
|
|
1396
|
+
expect(activePath?.map((entry) => entry.id)).toEqual([
|
|
1397
|
+
first?.id,
|
|
1398
|
+
inactive?.id,
|
|
1399
|
+
summary?.id,
|
|
1400
|
+
]);
|
|
1401
|
+
expect(summary).toMatchObject({
|
|
1402
|
+
type: 'summary',
|
|
1403
|
+
parentId: inactive?.id,
|
|
1404
|
+
data: {
|
|
1405
|
+
text: 'summary of sibling branch',
|
|
1406
|
+
summarizedEntryIds: [activeSibling?.id],
|
|
1407
|
+
},
|
|
1408
|
+
});
|
|
1409
|
+
});
|
|
1410
|
+
});
|