@librechat/agents 3.2.52 → 3.2.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/run.cjs +16 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/run.mjs +16 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/types/run.d.ts +9 -9
- package/dist/types/session/types.d.ts +1 -0
- package/dist/types/types/run.d.ts +17 -0
- package/package.json +5 -2
- package/src/llm/anthropic/utils/cross-provider-reasoning.test.ts +66 -0
- package/src/llm/anthropic/utils/message_inputs.ts +9 -1
- package/src/run.ts +24 -22
- package/src/session/types.ts +4 -1
- package/src/specs/durability-checkpoint.integration.test.ts +243 -0
- package/src/tools/__tests__/hitl.test.ts +167 -0
- package/src/types/run.ts +19 -0
package/src/run.ts
CHANGED
|
@@ -135,6 +135,13 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
135
135
|
Graph: StandardGraph | MultiAgentGraph | undefined;
|
|
136
136
|
returnContent: boolean = false;
|
|
137
137
|
private skipCleanup: boolean = false;
|
|
138
|
+
/**
|
|
139
|
+
* Whether the compiled graph was built with a checkpointer (host-supplied
|
|
140
|
+
* or the HITL `MemorySaver` fallback). Captured at graph creation because
|
|
141
|
+
* the constructor can later overwrite `Graph.compileOptions` with the raw
|
|
142
|
+
* caller options, dropping the fallback checkpointer from that metadata.
|
|
143
|
+
*/
|
|
144
|
+
private hasCheckpointer: boolean = false;
|
|
138
145
|
private _streamResult: t.MessageContentComplex[] | undefined;
|
|
139
146
|
/**
|
|
140
147
|
* Captured interrupt payload typed as `unknown` because the SDK
|
|
@@ -256,6 +263,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
256
263
|
standardGraph.compileOptions = this.applyHITLCheckpointerFallback(
|
|
257
264
|
config.compileOptions
|
|
258
265
|
);
|
|
266
|
+
this.hasCheckpointer = standardGraph.compileOptions?.checkpointer != null;
|
|
259
267
|
standardGraph.hookRegistry = this.hookRegistry;
|
|
260
268
|
standardGraph.humanInTheLoop = this.humanInTheLoop;
|
|
261
269
|
standardGraph.toolOutputReferences = this.toolOutputReferences;
|
|
@@ -283,6 +291,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
283
291
|
|
|
284
292
|
multiAgentGraph.compileOptions =
|
|
285
293
|
this.applyHITLCheckpointerFallback(compileOptions);
|
|
294
|
+
this.hasCheckpointer = multiAgentGraph.compileOptions?.checkpointer != null;
|
|
286
295
|
|
|
287
296
|
multiAgentGraph.hookRegistry = this.hookRegistry;
|
|
288
297
|
multiAgentGraph.humanInTheLoop = this.humanInTheLoop;
|
|
@@ -647,10 +656,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
647
656
|
|
|
648
657
|
async processStream(
|
|
649
658
|
inputs: t.IState | Command,
|
|
650
|
-
callerConfig:
|
|
651
|
-
version: 'v1' | 'v2';
|
|
652
|
-
run_id?: string;
|
|
653
|
-
},
|
|
659
|
+
callerConfig: t.RunStreamConfig,
|
|
654
660
|
streamOptions?: t.EventStreamOptions
|
|
655
661
|
): Promise<MessageContentComplex[] | undefined> {
|
|
656
662
|
if (this.graphRunnable == null) {
|
|
@@ -676,10 +682,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
676
682
|
const isResume = inputs instanceof Command;
|
|
677
683
|
const stateInputs = isResume ? undefined : (inputs as t.IState);
|
|
678
684
|
|
|
679
|
-
const config:
|
|
680
|
-
version: 'v1' | 'v2';
|
|
681
|
-
run_id?: string;
|
|
682
|
-
} = {
|
|
685
|
+
const config: t.RunStreamConfig = {
|
|
683
686
|
recursionLimit: 50,
|
|
684
687
|
...callerConfig,
|
|
685
688
|
configurable: { ...callerConfig.configurable },
|
|
@@ -763,6 +766,16 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
763
766
|
run_id: this.id,
|
|
764
767
|
});
|
|
765
768
|
|
|
769
|
+
/**
|
|
770
|
+
* Default `durability: 'exit'` whenever a checkpointer is active so
|
|
771
|
+
* runs skip per-superstep checkpoint writes and persist only at the
|
|
772
|
+
* exit/interrupt boundary (all HITL/resume needs). An explicit caller
|
|
773
|
+
* value wins; no checkpointer leaves it unset (langgraph default).
|
|
774
|
+
*/
|
|
775
|
+
if (config.durability == null && this.hasCheckpointer) {
|
|
776
|
+
config.durability = 'exit';
|
|
777
|
+
}
|
|
778
|
+
|
|
766
779
|
const threadId = config.configurable.thread_id as string | undefined;
|
|
767
780
|
|
|
768
781
|
if (this.hookRegistry != null && stateInputs != null) {
|
|
@@ -1155,10 +1168,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1155
1168
|
*/
|
|
1156
1169
|
async resume<TResume = t.ToolApprovalDecision[] | t.ToolApprovalDecisionMap>(
|
|
1157
1170
|
resumeValue: TResume,
|
|
1158
|
-
callerConfig:
|
|
1159
|
-
version: 'v1' | 'v2';
|
|
1160
|
-
run_id?: string;
|
|
1161
|
-
},
|
|
1171
|
+
callerConfig: t.RunStreamConfig,
|
|
1162
1172
|
streamOptions?: t.EventStreamOptions,
|
|
1163
1173
|
commandOptions?: Pick<
|
|
1164
1174
|
ConstructorParameters<typeof Command>[0],
|
|
@@ -1191,16 +1201,8 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1191
1201
|
}
|
|
1192
1202
|
|
|
1193
1203
|
private async resolveInterruptResumeConfig(
|
|
1194
|
-
callerConfig:
|
|
1195
|
-
|
|
1196
|
-
run_id?: string;
|
|
1197
|
-
}
|
|
1198
|
-
): Promise<
|
|
1199
|
-
Partial<RunnableConfig> & {
|
|
1200
|
-
version: 'v1' | 'v2';
|
|
1201
|
-
run_id?: string;
|
|
1202
|
-
}
|
|
1203
|
-
> {
|
|
1204
|
+
callerConfig: t.RunStreamConfig
|
|
1205
|
+
): Promise<t.RunStreamConfig> {
|
|
1204
1206
|
const interrupt = this._interrupt;
|
|
1205
1207
|
const interruptId = interrupt?.interruptId;
|
|
1206
1208
|
const workflow = this.graphRunnable as
|
package/src/session/types.ts
CHANGED
|
@@ -209,7 +209,10 @@ export type AgentSessionInput = string | BaseMessage | BaseMessage[] | t.IState;
|
|
|
209
209
|
export interface AgentSessionRunOptions {
|
|
210
210
|
runId?: string;
|
|
211
211
|
threadId?: string;
|
|
212
|
-
config?: Partial<RunnableConfig> & {
|
|
212
|
+
config?: Partial<RunnableConfig> & {
|
|
213
|
+
version?: 'v1' | 'v2';
|
|
214
|
+
durability?: t.Durability;
|
|
215
|
+
};
|
|
213
216
|
streamOptions?: t.EventStreamOptions;
|
|
214
217
|
}
|
|
215
218
|
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { MongoClient } from 'mongodb';
|
|
2
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
3
|
+
import { MongoMemoryServer } from 'mongodb-memory-server-core';
|
|
4
|
+
import { MongoDBSaver } from '@langchain/langgraph-checkpoint-mongodb';
|
|
5
|
+
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
|
|
6
|
+
import {
|
|
7
|
+
END,
|
|
8
|
+
START,
|
|
9
|
+
Command,
|
|
10
|
+
Annotation,
|
|
11
|
+
StateGraph,
|
|
12
|
+
interrupt,
|
|
13
|
+
} from '@langchain/langgraph';
|
|
14
|
+
import type * as t from '@/types';
|
|
15
|
+
import { Providers } from '@/common';
|
|
16
|
+
import { Run } from '@/run';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* End-to-end proof against a real Mongo store that the durability default is
|
|
20
|
+
* the storage optimization it claims to be, not just a config flag. A real
|
|
21
|
+
* `Run` with a fake streaming model (no provider API hit) writes through the
|
|
22
|
+
* official MongoDBSaver, and a langgraph interrupt graph exercises the HITL
|
|
23
|
+
* pause/resume boundary. `exit` persists only at the exit/interrupt boundary
|
|
24
|
+
* with no per-superstep writes; `async` checkpoints every superstep.
|
|
25
|
+
*/
|
|
26
|
+
describe('durability default — real Mongo checkpointer', () => {
|
|
27
|
+
let mongod: MongoMemoryServer;
|
|
28
|
+
let client: MongoClient;
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
// First CI run downloads a mongod binary — allow time beyond the 60s default.
|
|
32
|
+
mongod = await MongoMemoryServer.create();
|
|
33
|
+
client = new MongoClient(mongod.getUri());
|
|
34
|
+
await client.connect();
|
|
35
|
+
}, 120000);
|
|
36
|
+
|
|
37
|
+
afterAll(async () => {
|
|
38
|
+
await client.close();
|
|
39
|
+
await mongod.stop();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const counts = async (
|
|
43
|
+
dbName: string
|
|
44
|
+
): Promise<{ checkpoints: number; writes: number }> => {
|
|
45
|
+
const db = client.db(dbName);
|
|
46
|
+
const [checkpoints, writes] = await Promise.all([
|
|
47
|
+
db.collection('checkpoints').countDocuments({}),
|
|
48
|
+
db.collection('checkpoint_writes').countDocuments({}),
|
|
49
|
+
]);
|
|
50
|
+
return { checkpoints, writes };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
describe('normal run (real Run + fake model)', () => {
|
|
54
|
+
const runOnce = async (
|
|
55
|
+
dbName: string,
|
|
56
|
+
durability?: t.Durability
|
|
57
|
+
): Promise<void> => {
|
|
58
|
+
const run = await Run.create<t.IState>({
|
|
59
|
+
runId: `durability-${dbName}`,
|
|
60
|
+
graphConfig: {
|
|
61
|
+
type: 'standard',
|
|
62
|
+
agents: [
|
|
63
|
+
{
|
|
64
|
+
agentId: 'a',
|
|
65
|
+
provider: Providers.OPENAI,
|
|
66
|
+
clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
67
|
+
instructions: 'noop',
|
|
68
|
+
maxContextTokens: 8000,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
compileOptions: {
|
|
72
|
+
checkpointer: new MongoDBSaver({ client, dbName }),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
// Fake streaming model: one assistant turn, no tool calls -> agent -> END.
|
|
77
|
+
run.Graph?.overrideTestModel(['done'], 1);
|
|
78
|
+
await run.processStream(
|
|
79
|
+
{ messages: [new HumanMessage('hi')] },
|
|
80
|
+
{
|
|
81
|
+
version: 'v2',
|
|
82
|
+
...(durability != null ? { durability } : {}),
|
|
83
|
+
configurable: { thread_id: dbName },
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
it('exit default writes one checkpoint and no per-superstep writes; async writes more of both', async () => {
|
|
89
|
+
await runOnce('normal_exit');
|
|
90
|
+
await runOnce('normal_async', 'async');
|
|
91
|
+
|
|
92
|
+
const exitCounts = await counts('normal_exit');
|
|
93
|
+
const asyncCounts = await counts('normal_async');
|
|
94
|
+
|
|
95
|
+
// exit: only the final checkpoint, zero per-superstep pending writes.
|
|
96
|
+
expect(exitCounts).toEqual({ checkpoints: 1, writes: 0 });
|
|
97
|
+
// async: a checkpoint plus pending writes for every superstep.
|
|
98
|
+
expect(asyncCounts.checkpoints).toBeGreaterThan(exitCounts.checkpoints);
|
|
99
|
+
expect(asyncCounts.writes).toBeGreaterThan(exitCounts.writes);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('HITL interrupt (langgraph graph + real saver)', () => {
|
|
104
|
+
const State = Annotation.Root({
|
|
105
|
+
steps: Annotation<string[]>({
|
|
106
|
+
reducer: (a, b) => a.concat(b),
|
|
107
|
+
default: () => [],
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const drain = async (stream: AsyncIterable<unknown>): Promise<void> => {
|
|
112
|
+
for await (const _event of stream) {
|
|
113
|
+
// Consume the event stream exactly like Run.processStream does.
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const runInterruptGraph = async (
|
|
118
|
+
dbName: string,
|
|
119
|
+
durability: t.Durability
|
|
120
|
+
): Promise<{
|
|
121
|
+
atPause: { checkpoints: number; writes: number };
|
|
122
|
+
done: { checkpoints: number; writes: number };
|
|
123
|
+
}> => {
|
|
124
|
+
const saver = new MongoDBSaver({ client, dbName });
|
|
125
|
+
const graph = new StateGraph(State)
|
|
126
|
+
.addNode('a', () => ({ steps: ['a'] }))
|
|
127
|
+
.addNode('b', () => {
|
|
128
|
+
interrupt({ ask: 'approve?' });
|
|
129
|
+
return { steps: ['b'] };
|
|
130
|
+
})
|
|
131
|
+
.addNode('c', () => ({ steps: ['c'] }))
|
|
132
|
+
.addEdge(START, 'a')
|
|
133
|
+
.addEdge('a', 'b')
|
|
134
|
+
.addEdge('b', 'c')
|
|
135
|
+
.addEdge('c', END)
|
|
136
|
+
.compile({ checkpointer: saver });
|
|
137
|
+
|
|
138
|
+
// durability rides in the 2nd-arg config, exactly as run.ts threads it.
|
|
139
|
+
const cfg = {
|
|
140
|
+
configurable: { thread_id: dbName },
|
|
141
|
+
durability,
|
|
142
|
+
version: 'v2' as const,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
await drain(graph.streamEvents({ steps: [] }, cfg, { raiseError: true }));
|
|
146
|
+
const atPause = await counts(dbName);
|
|
147
|
+
|
|
148
|
+
await drain(
|
|
149
|
+
graph.streamEvents(new Command({ resume: 'ok' }), cfg, {
|
|
150
|
+
raiseError: true,
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
const done = await counts(dbName);
|
|
154
|
+
|
|
155
|
+
return { atPause, done };
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
it('exit checkpoints only at the interrupt boundary and final exit; async checkpoints every superstep', async () => {
|
|
159
|
+
const exitRun = await runInterruptGraph('hitl_exit', 'exit');
|
|
160
|
+
const asyncRun = await runInterruptGraph('hitl_async', 'async');
|
|
161
|
+
|
|
162
|
+
// exit: one checkpoint at the pause (the interrupt), two once resumed
|
|
163
|
+
// (interrupt boundary + final exit).
|
|
164
|
+
expect(exitRun.atPause.checkpoints).toBe(1);
|
|
165
|
+
expect(exitRun.done.checkpoints).toBe(2);
|
|
166
|
+
|
|
167
|
+
// async: a checkpoint for every superstep, strictly more at each stage.
|
|
168
|
+
expect(asyncRun.atPause.checkpoints).toBeGreaterThan(
|
|
169
|
+
exitRun.atPause.checkpoints
|
|
170
|
+
);
|
|
171
|
+
expect(asyncRun.done.checkpoints).toBeGreaterThan(
|
|
172
|
+
exitRun.done.checkpoints
|
|
173
|
+
);
|
|
174
|
+
expect(asyncRun.done.writes).toBeGreaterThan(exitRun.done.writes);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('multiple interrupts (sequential ask-user-questions)', () => {
|
|
179
|
+
const State = Annotation.Root({
|
|
180
|
+
qa: Annotation<string[]>({
|
|
181
|
+
reducer: (a, b) => a.concat(b),
|
|
182
|
+
default: () => [],
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const drain = async (stream: AsyncIterable<unknown>): Promise<void> => {
|
|
187
|
+
for await (const _event of stream) {
|
|
188
|
+
// Consume the event stream.
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
it('checkpoints each interrupt boundary, chains them, and discards none across resumes', async () => {
|
|
193
|
+
const dbName = 'multi_exit';
|
|
194
|
+
const saver = new MongoDBSaver({ client, dbName });
|
|
195
|
+
const graph = new StateGraph(State)
|
|
196
|
+
.addNode('ask1', () => ({ qa: [`Q1=${interrupt({ q: 'Q1' })}`] }))
|
|
197
|
+
.addNode('ask2', () => ({ qa: [`Q2=${interrupt({ q: 'Q2' })}`] }))
|
|
198
|
+
.addNode('done', () => ({ qa: ['done'] }))
|
|
199
|
+
.addEdge(START, 'ask1')
|
|
200
|
+
.addEdge('ask1', 'ask2')
|
|
201
|
+
.addEdge('ask2', 'done')
|
|
202
|
+
.addEdge('done', END)
|
|
203
|
+
.compile({ checkpointer: saver });
|
|
204
|
+
|
|
205
|
+
const cfg = {
|
|
206
|
+
configurable: { thread_id: dbName },
|
|
207
|
+
durability: 'exit' as const,
|
|
208
|
+
version: 'v2' as const,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// First question -> pauses at ask1, one checkpoint at that boundary.
|
|
212
|
+
await drain(graph.streamEvents({ qa: [] }, cfg, { raiseError: true }));
|
|
213
|
+
let state = await graph.getState(cfg);
|
|
214
|
+
expect(state.next).toEqual(['ask1']);
|
|
215
|
+
expect((await counts(dbName)).checkpoints).toBe(1);
|
|
216
|
+
|
|
217
|
+
// Answer 1 -> the run continues and the LLM asks a second question.
|
|
218
|
+
// The chain grows to 2: the first interrupt checkpoint is retained as
|
|
219
|
+
// the parent, not discarded or overwritten.
|
|
220
|
+
await drain(
|
|
221
|
+
graph.streamEvents(new Command({ resume: 'A1' }), cfg, {
|
|
222
|
+
raiseError: true,
|
|
223
|
+
})
|
|
224
|
+
);
|
|
225
|
+
state = await graph.getState(cfg);
|
|
226
|
+
expect(state.next).toEqual(['ask2']);
|
|
227
|
+
expect(state.values.qa).toEqual(['Q1=A1']);
|
|
228
|
+
expect((await counts(dbName)).checkpoints).toBe(2);
|
|
229
|
+
|
|
230
|
+
// Answer 2 -> run completes; both answers applied, three checkpoints
|
|
231
|
+
// total (two interrupt boundaries + final exit).
|
|
232
|
+
await drain(
|
|
233
|
+
graph.streamEvents(new Command({ resume: 'A2' }), cfg, {
|
|
234
|
+
raiseError: true,
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
state = await graph.getState(cfg);
|
|
238
|
+
expect(state.next).toEqual([]);
|
|
239
|
+
expect(state.values.qa).toEqual(['Q1=A1', 'Q2=A2', 'done']);
|
|
240
|
+
expect((await counts(dbName)).checkpoints).toBe(3);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -761,6 +761,173 @@ describe('Run integration — HITL fallback checkpointer + resume', () => {
|
|
|
761
761
|
expect(run.Graph?.compileOptions?.checkpointer).toBe(hostCheckpointer);
|
|
762
762
|
});
|
|
763
763
|
|
|
764
|
+
it('processStream defaults durability to "exit" when a checkpointer is active', async () => {
|
|
765
|
+
const { Run } = await import('@/run');
|
|
766
|
+
const { Providers } = await import('@/common');
|
|
767
|
+
|
|
768
|
+
const run = await Run.create<t.IState>({
|
|
769
|
+
runId: 'durability-exit-default',
|
|
770
|
+
graphConfig: {
|
|
771
|
+
type: 'standard',
|
|
772
|
+
agents: [
|
|
773
|
+
{
|
|
774
|
+
agentId: 'a',
|
|
775
|
+
provider: Providers.OPENAI,
|
|
776
|
+
clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
777
|
+
instructions: 'noop',
|
|
778
|
+
maxContextTokens: 8000,
|
|
779
|
+
},
|
|
780
|
+
],
|
|
781
|
+
},
|
|
782
|
+
humanInTheLoop: { enabled: true },
|
|
783
|
+
});
|
|
784
|
+
expect(run.Graph?.compileOptions?.checkpointer).toBeInstanceOf(MemorySaver);
|
|
785
|
+
|
|
786
|
+
const graph = new StateGraph(MessagesAnnotation)
|
|
787
|
+
.addNode('noop', (): MessagesUpdate => ({ messages: [] }))
|
|
788
|
+
.addEdge(START, 'noop')
|
|
789
|
+
.addEdge('noop', END)
|
|
790
|
+
.compile();
|
|
791
|
+
const spy = jest.spyOn(graph, 'streamEvents');
|
|
792
|
+
run.graphRunnable = graph as unknown as t.CompiledStateWorkflow;
|
|
793
|
+
|
|
794
|
+
await run.processStream(
|
|
795
|
+
{ messages: [] },
|
|
796
|
+
{ version: 'v2', configurable: { thread_id: 't' } }
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
const streamedConfig = spy.mock.calls[0]?.[1] as
|
|
800
|
+
| t.RunStreamConfig
|
|
801
|
+
| undefined;
|
|
802
|
+
expect(streamedConfig?.durability).toBe('exit');
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('processStream respects an explicit caller durability over the checkpointer default', async () => {
|
|
806
|
+
const { Run } = await import('@/run');
|
|
807
|
+
const { Providers } = await import('@/common');
|
|
808
|
+
|
|
809
|
+
const run = await Run.create<t.IState>({
|
|
810
|
+
runId: 'durability-explicit-override',
|
|
811
|
+
graphConfig: {
|
|
812
|
+
type: 'standard',
|
|
813
|
+
agents: [
|
|
814
|
+
{
|
|
815
|
+
agentId: 'a',
|
|
816
|
+
provider: Providers.OPENAI,
|
|
817
|
+
clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
818
|
+
instructions: 'noop',
|
|
819
|
+
maxContextTokens: 8000,
|
|
820
|
+
},
|
|
821
|
+
],
|
|
822
|
+
},
|
|
823
|
+
humanInTheLoop: { enabled: true },
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
const graph = new StateGraph(MessagesAnnotation)
|
|
827
|
+
.addNode('noop', (): MessagesUpdate => ({ messages: [] }))
|
|
828
|
+
.addEdge(START, 'noop')
|
|
829
|
+
.addEdge('noop', END)
|
|
830
|
+
.compile();
|
|
831
|
+
const spy = jest.spyOn(graph, 'streamEvents');
|
|
832
|
+
run.graphRunnable = graph as unknown as t.CompiledStateWorkflow;
|
|
833
|
+
|
|
834
|
+
await run.processStream(
|
|
835
|
+
{ messages: [] },
|
|
836
|
+
{ version: 'v2', durability: 'sync', configurable: { thread_id: 't' } }
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
const streamedConfig = spy.mock.calls[0]?.[1] as
|
|
840
|
+
| t.RunStreamConfig
|
|
841
|
+
| undefined;
|
|
842
|
+
expect(streamedConfig?.durability).toBe('sync');
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('processStream leaves durability unset when no checkpointer is active', async () => {
|
|
846
|
+
const { Run } = await import('@/run');
|
|
847
|
+
const { Providers } = await import('@/common');
|
|
848
|
+
|
|
849
|
+
const run = await Run.create<t.IState>({
|
|
850
|
+
runId: 'durability-no-checkpointer',
|
|
851
|
+
graphConfig: {
|
|
852
|
+
type: 'standard',
|
|
853
|
+
agents: [
|
|
854
|
+
{
|
|
855
|
+
agentId: 'a',
|
|
856
|
+
provider: Providers.OPENAI,
|
|
857
|
+
clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
858
|
+
instructions: 'noop',
|
|
859
|
+
maxContextTokens: 8000,
|
|
860
|
+
},
|
|
861
|
+
],
|
|
862
|
+
},
|
|
863
|
+
// humanInTheLoop omitted — no checkpointer installed
|
|
864
|
+
});
|
|
865
|
+
expect(run.Graph?.compileOptions?.checkpointer).toBeUndefined();
|
|
866
|
+
|
|
867
|
+
const graph = new StateGraph(MessagesAnnotation)
|
|
868
|
+
.addNode('noop', (): MessagesUpdate => ({ messages: [] }))
|
|
869
|
+
.addEdge(START, 'noop')
|
|
870
|
+
.addEdge('noop', END)
|
|
871
|
+
.compile();
|
|
872
|
+
const spy = jest.spyOn(graph, 'streamEvents');
|
|
873
|
+
run.graphRunnable = graph as unknown as t.CompiledStateWorkflow;
|
|
874
|
+
|
|
875
|
+
await run.processStream(
|
|
876
|
+
{ messages: [] },
|
|
877
|
+
{ version: 'v2', configurable: { thread_id: 't' } }
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
const streamedConfig = spy.mock.calls[0]?.[1] as
|
|
881
|
+
| t.RunStreamConfig
|
|
882
|
+
| undefined;
|
|
883
|
+
expect(streamedConfig?.durability).toBeUndefined();
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it('defaults durability to "exit" when HITL installs the fallback checkpointer but caller compileOptions omit it', async () => {
|
|
887
|
+
const { Run } = await import('@/run');
|
|
888
|
+
const { Providers } = await import('@/common');
|
|
889
|
+
|
|
890
|
+
const run = await Run.create<t.IState>({
|
|
891
|
+
runId: 'durability-hitl-fallback-compileopts',
|
|
892
|
+
graphConfig: {
|
|
893
|
+
type: 'standard',
|
|
894
|
+
agents: [
|
|
895
|
+
{
|
|
896
|
+
agentId: 'a',
|
|
897
|
+
provider: Providers.OPENAI,
|
|
898
|
+
clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
899
|
+
instructions: 'noop',
|
|
900
|
+
maxContextTokens: 8000,
|
|
901
|
+
},
|
|
902
|
+
],
|
|
903
|
+
// Caller compileOptions without a checkpointer: HITL adds a MemorySaver
|
|
904
|
+
// fallback to the compiled graph, but the constructor restores this raw
|
|
905
|
+
// metadata (no checkpointer) onto Graph.compileOptions.
|
|
906
|
+
compileOptions: { interruptBefore: [] },
|
|
907
|
+
},
|
|
908
|
+
humanInTheLoop: { enabled: true },
|
|
909
|
+
});
|
|
910
|
+
expect(run.Graph?.compileOptions?.checkpointer).toBeUndefined();
|
|
911
|
+
|
|
912
|
+
const graph = new StateGraph(MessagesAnnotation)
|
|
913
|
+
.addNode('noop', (): MessagesUpdate => ({ messages: [] }))
|
|
914
|
+
.addEdge(START, 'noop')
|
|
915
|
+
.addEdge('noop', END)
|
|
916
|
+
.compile();
|
|
917
|
+
const spy = jest.spyOn(graph, 'streamEvents');
|
|
918
|
+
run.graphRunnable = graph as unknown as t.CompiledStateWorkflow;
|
|
919
|
+
|
|
920
|
+
await run.processStream(
|
|
921
|
+
{ messages: [] },
|
|
922
|
+
{ version: 'v2', configurable: { thread_id: 't' } }
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
const streamedConfig = spy.mock.calls[0]?.[1] as
|
|
926
|
+
| t.RunStreamConfig
|
|
927
|
+
| undefined;
|
|
928
|
+
expect(streamedConfig?.durability).toBe('exit');
|
|
929
|
+
});
|
|
930
|
+
|
|
764
931
|
it('Run.resume forwards update + goto into the resume Command (langgraph 1.4.5)', async () => {
|
|
765
932
|
const { Run } = await import('@/run');
|
|
766
933
|
const { Providers } = await import('@/common');
|
package/src/types/run.ts
CHANGED
|
@@ -261,3 +261,22 @@ export type EventStreamOptions = {
|
|
|
261
261
|
callbacks?: g.ClientCallbacks;
|
|
262
262
|
keepContent?: boolean;
|
|
263
263
|
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* When to persist checkpoints during a run. Mirrors langgraph's option:
|
|
267
|
+
* `'async'`/`'sync'` checkpoint every superstep; `'exit'` checkpoints only
|
|
268
|
+
* at the graph's exit/interrupt boundary. Kept as a local union to avoid
|
|
269
|
+
* coupling to langgraph internals (it is not exported from the root).
|
|
270
|
+
*/
|
|
271
|
+
export type Durability = 'async' | 'sync' | 'exit';
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Config accepted by `processStream`/`resume`. Extends `RunnableConfig` with
|
|
275
|
+
* the stream `version`, an optional `run_id`, and an optional `durability`
|
|
276
|
+
* override forwarded to langgraph's run options.
|
|
277
|
+
*/
|
|
278
|
+
export type RunStreamConfig = Partial<RunnableConfig> & {
|
|
279
|
+
version: 'v1' | 'v2';
|
|
280
|
+
run_id?: string;
|
|
281
|
+
durability?: Durability;
|
|
282
|
+
};
|