@librechat/agents 3.1.86 → 3.1.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +69 -0
  2. package/dist/cjs/events.cjs +23 -0
  3. package/dist/cjs/events.cjs.map +1 -1
  4. package/dist/cjs/graphs/Graph.cjs +133 -18
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  8. package/dist/cjs/llm/anthropic/index.cjs +251 -53
  9. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  10. package/dist/cjs/llm/init.cjs +1 -5
  11. package/dist/cjs/llm/init.cjs.map +1 -1
  12. package/dist/cjs/llm/openai/index.cjs +113 -24
  13. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  14. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  15. package/dist/cjs/llm/openrouter/index.cjs +3 -1
  16. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  17. package/dist/cjs/main.cjs +18 -5
  18. package/dist/cjs/main.cjs.map +1 -1
  19. package/dist/cjs/openai/index.cjs +253 -0
  20. package/dist/cjs/openai/index.cjs.map +1 -0
  21. package/dist/cjs/responses/index.cjs +448 -0
  22. package/dist/cjs/responses/index.cjs.map +1 -0
  23. package/dist/cjs/run.cjs +108 -7
  24. package/dist/cjs/run.cjs.map +1 -1
  25. package/dist/cjs/session/AgentSession.cjs +1057 -0
  26. package/dist/cjs/session/AgentSession.cjs.map +1 -0
  27. package/dist/cjs/session/JsonlSessionStore.cjs +425 -0
  28. package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -0
  29. package/dist/cjs/session/handlers.cjs +221 -0
  30. package/dist/cjs/session/handlers.cjs.map +1 -0
  31. package/dist/cjs/session/ids.cjs +22 -0
  32. package/dist/cjs/session/ids.cjs.map +1 -0
  33. package/dist/cjs/session/messageSerialization.cjs +179 -0
  34. package/dist/cjs/session/messageSerialization.cjs.map +1 -0
  35. package/dist/cjs/stream.cjs +475 -11
  36. package/dist/cjs/stream.cjs.map +1 -1
  37. package/dist/cjs/summarization/node.cjs +1 -1
  38. package/dist/cjs/summarization/node.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolNode.cjs +177 -59
  40. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  41. package/dist/cjs/tools/eagerEventExecution.cjs +113 -0
  42. package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -0
  43. package/dist/cjs/tools/handlers.cjs +1 -1
  44. package/dist/cjs/tools/handlers.cjs.map +1 -1
  45. package/dist/cjs/tools/streamedToolCallSeals.cjs +42 -0
  46. package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -0
  47. package/dist/esm/events.mjs +23 -1
  48. package/dist/esm/events.mjs.map +1 -1
  49. package/dist/esm/graphs/Graph.mjs +133 -18
  50. package/dist/esm/graphs/Graph.mjs.map +1 -1
  51. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  52. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  53. package/dist/esm/llm/anthropic/index.mjs +251 -53
  54. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  55. package/dist/esm/llm/init.mjs +1 -5
  56. package/dist/esm/llm/init.mjs.map +1 -1
  57. package/dist/esm/llm/openai/index.mjs +113 -25
  58. package/dist/esm/llm/openai/index.mjs.map +1 -1
  59. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  60. package/dist/esm/llm/openrouter/index.mjs +4 -2
  61. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  62. package/dist/esm/main.mjs +5 -1
  63. package/dist/esm/main.mjs.map +1 -1
  64. package/dist/esm/openai/index.mjs +246 -0
  65. package/dist/esm/openai/index.mjs.map +1 -0
  66. package/dist/esm/responses/index.mjs +440 -0
  67. package/dist/esm/responses/index.mjs.map +1 -0
  68. package/dist/esm/run.mjs +108 -7
  69. package/dist/esm/run.mjs.map +1 -1
  70. package/dist/esm/session/AgentSession.mjs +1054 -0
  71. package/dist/esm/session/AgentSession.mjs.map +1 -0
  72. package/dist/esm/session/JsonlSessionStore.mjs +422 -0
  73. package/dist/esm/session/JsonlSessionStore.mjs.map +1 -0
  74. package/dist/esm/session/handlers.mjs +219 -0
  75. package/dist/esm/session/handlers.mjs.map +1 -0
  76. package/dist/esm/session/ids.mjs +17 -0
  77. package/dist/esm/session/ids.mjs.map +1 -0
  78. package/dist/esm/session/messageSerialization.mjs +173 -0
  79. package/dist/esm/session/messageSerialization.mjs.map +1 -0
  80. package/dist/esm/stream.mjs +476 -12
  81. package/dist/esm/stream.mjs.map +1 -1
  82. package/dist/esm/summarization/node.mjs +1 -1
  83. package/dist/esm/summarization/node.mjs.map +1 -1
  84. package/dist/esm/tools/ToolNode.mjs +177 -59
  85. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  86. package/dist/esm/tools/eagerEventExecution.mjs +107 -0
  87. package/dist/esm/tools/eagerEventExecution.mjs.map +1 -0
  88. package/dist/esm/tools/handlers.mjs +1 -1
  89. package/dist/esm/tools/handlers.mjs.map +1 -1
  90. package/dist/esm/tools/streamedToolCallSeals.mjs +36 -0
  91. package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -0
  92. package/dist/types/events.d.ts +1 -0
  93. package/dist/types/graphs/Graph.d.ts +24 -9
  94. package/dist/types/index.d.ts +1 -0
  95. package/dist/types/llm/openai/index.d.ts +1 -0
  96. package/dist/types/openai/index.d.ts +75 -0
  97. package/dist/types/responses/index.d.ts +97 -0
  98. package/dist/types/run.d.ts +2 -0
  99. package/dist/types/session/AgentSession.d.ts +32 -0
  100. package/dist/types/session/JsonlSessionStore.d.ts +67 -0
  101. package/dist/types/session/handlers.d.ts +8 -0
  102. package/dist/types/session/ids.d.ts +4 -0
  103. package/dist/types/session/index.d.ts +5 -0
  104. package/dist/types/session/messageSerialization.d.ts +7 -0
  105. package/dist/types/session/types.d.ts +191 -0
  106. package/dist/types/tools/ToolNode.d.ts +12 -1
  107. package/dist/types/tools/eagerEventExecution.d.ts +23 -0
  108. package/dist/types/tools/streamedToolCallSeals.d.ts +13 -0
  109. package/dist/types/types/hitl.d.ts +4 -0
  110. package/dist/types/types/run.d.ts +11 -1
  111. package/dist/types/types/tools.d.ts +36 -0
  112. package/package.json +19 -2
  113. package/src/__tests__/stream.eagerEventExecution.test.ts +2571 -0
  114. package/src/events.ts +29 -0
  115. package/src/graphs/Graph.ts +224 -50
  116. package/src/graphs/MultiAgentGraph.ts +1 -1
  117. package/src/graphs/__tests__/composition.smoke.test.ts +30 -0
  118. package/src/index.ts +3 -0
  119. package/src/llm/anthropic/index.ts +356 -84
  120. package/src/llm/anthropic/llm.spec.ts +64 -0
  121. package/src/llm/custom-chat-models.smoke.test.ts +175 -4
  122. package/src/llm/openai/contentBlocks.test.ts +35 -0
  123. package/src/llm/openai/deepseek.test.ts +201 -2
  124. package/src/llm/openai/index.ts +171 -26
  125. package/src/llm/openai/utils/index.ts +22 -0
  126. package/src/llm/openrouter/index.ts +4 -2
  127. package/src/openai/__tests__/openai.test.ts +337 -0
  128. package/src/openai/index.ts +404 -0
  129. package/src/responses/__tests__/responses.test.ts +652 -0
  130. package/src/responses/index.ts +677 -0
  131. package/src/run.ts +158 -8
  132. package/src/scripts/compare_pi_vs_ours.ts +592 -173
  133. package/src/scripts/session_live.ts +548 -0
  134. package/src/session/AgentSession.ts +1432 -0
  135. package/src/session/JsonlSessionStore.ts +572 -0
  136. package/src/session/__tests__/JsonlSessionStore.test.ts +1410 -0
  137. package/src/session/__tests__/handlers.test.ts +161 -0
  138. package/src/session/handlers.ts +272 -0
  139. package/src/session/ids.ts +17 -0
  140. package/src/session/index.ts +44 -0
  141. package/src/session/messageSerialization.ts +207 -0
  142. package/src/session/types.ts +275 -0
  143. package/src/specs/custom-event-await.test.ts +89 -0
  144. package/src/specs/summarization.test.ts +1 -1
  145. package/src/stream.ts +756 -48
  146. package/src/summarization/node.ts +1 -1
  147. package/src/tools/ToolNode.ts +299 -126
  148. package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +373 -0
  149. package/src/tools/__tests__/handlers.test.ts +2 -1
  150. package/src/tools/__tests__/hitl.test.ts +206 -110
  151. package/src/tools/eagerEventExecution.ts +153 -0
  152. package/src/tools/handlers.ts +8 -4
  153. package/src/tools/streamedToolCallSeals.ts +57 -0
  154. package/src/types/hitl.ts +4 -0
  155. package/src/types/run.ts +11 -0
  156. package/src/types/tools.ts +36 -0
  157. package/dist/cjs/llm/text.cjs +0 -69
  158. package/dist/cjs/llm/text.cjs.map +0 -1
  159. package/dist/esm/llm/text.mjs +0 -67
  160. 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
+ });