@langchain/langgraph 0.1.1 → 0.1.2

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 (44) hide show
  1. package/README.md +2 -2
  2. package/dist/constants.cjs +1 -0
  3. package/dist/constants.d.ts +1 -0
  4. package/dist/constants.js +1 -0
  5. package/dist/graph/annotation.cjs +78 -4
  6. package/dist/graph/annotation.d.ts +82 -5
  7. package/dist/graph/annotation.js +77 -2
  8. package/dist/graph/graph.cjs +14 -8
  9. package/dist/graph/graph.d.ts +14 -5
  10. package/dist/graph/graph.js +14 -8
  11. package/dist/graph/message.cjs +6 -0
  12. package/dist/graph/message.d.ts +6 -0
  13. package/dist/graph/message.js +6 -0
  14. package/dist/graph/messages_annotation.cjs +5 -0
  15. package/dist/graph/messages_annotation.d.ts +5 -0
  16. package/dist/graph/messages_annotation.js +5 -0
  17. package/dist/graph/state.cjs +84 -3
  18. package/dist/graph/state.d.ts +75 -5
  19. package/dist/graph/state.js +86 -5
  20. package/dist/prebuilt/agent_executor.cjs +1 -0
  21. package/dist/prebuilt/agent_executor.d.ts +2 -0
  22. package/dist/prebuilt/agent_executor.js +1 -0
  23. package/dist/prebuilt/chat_agent_executor.cjs +1 -0
  24. package/dist/prebuilt/chat_agent_executor.d.ts +2 -0
  25. package/dist/prebuilt/chat_agent_executor.js +1 -0
  26. package/dist/pregel/algo.cjs +3 -41
  27. package/dist/pregel/algo.d.ts +0 -5
  28. package/dist/pregel/algo.js +2 -39
  29. package/dist/pregel/debug.d.ts +1 -1
  30. package/dist/pregel/index.cjs +17 -30
  31. package/dist/pregel/index.d.ts +2 -0
  32. package/dist/pregel/index.js +18 -31
  33. package/dist/pregel/loop.cjs +5 -2
  34. package/dist/pregel/loop.js +5 -2
  35. package/dist/pregel/read.cjs +19 -1
  36. package/dist/pregel/read.d.ts +5 -0
  37. package/dist/pregel/read.js +19 -1
  38. package/dist/pregel/retry.cjs +147 -0
  39. package/dist/pregel/retry.d.ts +11 -0
  40. package/dist/pregel/retry.js +143 -0
  41. package/dist/pregel/types.d.ts +4 -2
  42. package/dist/pregel/utils.d.ts +26 -0
  43. package/dist/web.d.ts +1 -0
  44. package/package.json +5 -4
@@ -9,10 +9,11 @@ import { mapDebugTaskResults, printStepCheckpoint, printStepTasks, printStepWrit
9
9
  import { ChannelWrite, PASSTHROUGH } from "./write.js";
10
10
  import { CONFIG_KEY_CHECKPOINTER, CONFIG_KEY_READ, CONFIG_KEY_SEND, ERROR, INTERRUPT, } from "../constants.js";
11
11
  import { GraphRecursionError, GraphValueError, InvalidUpdateError, } from "../errors.js";
12
- import { executeTasks, _prepareNextTasks, _localRead, _applyWrites, } from "./algo.js";
12
+ import { _prepareNextTasks, _localRead, _applyWrites, } from "./algo.js";
13
13
  import { prefixGenerator } from "../utils.js";
14
14
  import { _coerceToDict, getNewChannelVersions } from "./utils.js";
15
15
  import { PregelLoop } from "./loop.js";
16
+ import { executeTasksWithRetry } from "./retry.js";
16
17
  function isString(value) {
17
18
  return typeof value === "string";
18
19
  }
@@ -155,6 +156,12 @@ export class Pregel extends Runnable {
155
156
  writable: true,
156
157
  value: void 0
157
158
  });
159
+ Object.defineProperty(this, "retryPolicy", {
160
+ enumerable: true,
161
+ configurable: true,
162
+ writable: true,
163
+ value: void 0
164
+ });
158
165
  let { streamMode } = fields;
159
166
  if (streamMode != null && !Array.isArray(streamMode)) {
160
167
  streamMode = [streamMode];
@@ -171,6 +178,7 @@ export class Pregel extends Runnable {
171
178
  this.stepTimeout = fields.stepTimeout ?? this.stepTimeout;
172
179
  this.debug = fields.debug ?? this.debug;
173
180
  this.checkpointer = fields.checkpointer;
181
+ this.retryPolicy = fields.retryPolicy;
174
182
  if (this.autoValidate) {
175
183
  this.validate();
176
184
  }
@@ -340,12 +348,11 @@ export class Pregel extends Runnable {
340
348
  writers.length > 1 ? RunnableSequence.from(writers) : writers[0],
341
349
  writes: [],
342
350
  triggers: [INTERRUPT],
343
- config: undefined,
344
351
  id: uuid5(INTERRUPT, checkpoint.id),
345
352
  };
346
353
  // execute task
347
354
  await task.proc.invoke(task.input, patchConfig(config, {
348
- runName: `${this.name}UpdateState`,
355
+ runName: config.runName ?? `${this.getName()}UpdateState`,
349
356
  configurable: {
350
357
  [CONFIG_KEY_SEND]: (items) => task.writes.push(...items),
351
358
  [CONFIG_KEY_READ]: _localRead.bind(undefined, checkpoint, channels,
@@ -486,35 +493,15 @@ export class Pregel extends Runnable {
486
493
  if (debug) {
487
494
  printStepTasks(loop.step, loop.tasks);
488
495
  }
489
- // execute tasks, and wait for one to fail or all to finish.
490
- // each task is independent from all other concurrent tasks
491
- // yield updates/debug output as each task finishes
492
- const tasks = Object.fromEntries(loop.tasks
493
- .filter((task) => task.writes.length === 0)
494
- .map((pregelTask) => {
495
- return [
496
- pregelTask.id,
497
- async () => {
498
- let error;
499
- let result;
500
- try {
501
- result = await pregelTask.proc.invoke(pregelTask.input, pregelTask.config);
502
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
503
- }
504
- catch (e) {
505
- error = e;
506
- error.pregelTaskId = pregelTask.id;
507
- }
508
- return {
509
- task: pregelTask,
510
- result,
511
- error,
512
- };
513
- },
514
- ];
515
- }));
516
496
  try {
517
- for await (const task of executeTasks(tasks, this.stepTimeout, config.signal)) {
497
+ // execute tasks, and wait for one to fail or all to finish.
498
+ // each task is independent from all other concurrent tasks
499
+ // yield updates/debug output as each task finishes
500
+ for await (const task of executeTasksWithRetry(loop.tasks.filter((task) => task.writes.length === 0), {
501
+ stepTimeout: this.stepTimeout,
502
+ signal: config.signal,
503
+ retryPolicy: this.retryPolicy,
504
+ })) {
518
505
  loop.putWrites(task.id, task.writes);
519
506
  if (streamMode.includes("updates")) {
520
507
  yield* prefixGenerator(mapOutputUpdates(outputKeys, [task]), streamMode.length > 1 ? "updates" : undefined);
@@ -269,7 +269,7 @@ class PregelLoop {
269
269
  : (0, io_js_1.mapOutputValues)(outputKeys, writes, this.channels).next().value;
270
270
  await this._putCheckpoint({
271
271
  source: "loop",
272
- writes: metadataWrites,
272
+ writes: metadataWrites ?? null,
273
273
  });
274
274
  // after execution, check if we should interrupt
275
275
  if ((0, algo_js_1.shouldInterrupt)(this.checkpoint, interruptAfter, this.tasks)) {
@@ -377,7 +377,10 @@ class PregelLoop {
377
377
  },
378
378
  ]), this.checkpointerGetNextVersion);
379
379
  // save input checkpoint
380
- await this._putCheckpoint({ source: "input", writes: this.input });
380
+ await this._putCheckpoint({
381
+ source: "input",
382
+ writes: this.input ?? null,
383
+ });
381
384
  }
382
385
  // done with input
383
386
  this.input = isResuming ? INPUT_RESUMING : INPUT_DONE;
@@ -263,7 +263,7 @@ export class PregelLoop {
263
263
  : mapOutputValues(outputKeys, writes, this.channels).next().value;
264
264
  await this._putCheckpoint({
265
265
  source: "loop",
266
- writes: metadataWrites,
266
+ writes: metadataWrites ?? null,
267
267
  });
268
268
  // after execution, check if we should interrupt
269
269
  if (shouldInterrupt(this.checkpoint, interruptAfter, this.tasks)) {
@@ -371,7 +371,10 @@ export class PregelLoop {
371
371
  },
372
372
  ]), this.checkpointerGetNextVersion);
373
373
  // save input checkpoint
374
- await this._putCheckpoint({ source: "input", writes: this.input });
374
+ await this._putCheckpoint({
375
+ source: "input",
376
+ writes: this.input ?? null,
377
+ });
375
378
  }
376
379
  // done with input
377
380
  this.input = isResuming ? INPUT_RESUMING : INPUT_DONE;
@@ -63,7 +63,7 @@ const defaultRunnableBound =
63
63
  /* #__PURE__ */ new runnables_1.RunnablePassthrough();
64
64
  class PregelNode extends runnables_1.RunnableBinding {
65
65
  constructor(fields) {
66
- const { channels, triggers, mapper, writers, bound, kwargs } = fields;
66
+ const { channels, triggers, mapper, writers, bound, kwargs, metadata, retryPolicy, } = fields;
67
67
  const mergedTags = [
68
68
  ...(fields.config?.tags ? fields.config.tags : []),
69
69
  ...(fields.tags ? fields.tags : []),
@@ -121,12 +121,26 @@ class PregelNode extends runnables_1.RunnableBinding {
121
121
  writable: true,
122
122
  value: {}
123
123
  });
124
+ Object.defineProperty(this, "metadata", {
125
+ enumerable: true,
126
+ configurable: true,
127
+ writable: true,
128
+ value: {}
129
+ });
130
+ Object.defineProperty(this, "retryPolicy", {
131
+ enumerable: true,
132
+ configurable: true,
133
+ writable: true,
134
+ value: void 0
135
+ });
124
136
  this.channels = channels;
125
137
  this.triggers = triggers;
126
138
  this.mapper = mapper;
127
139
  this.writers = writers ?? this.writers;
128
140
  this.bound = bound ?? this.bound;
129
141
  this.kwargs = kwargs ?? this.kwargs;
142
+ this.metadata = metadata ?? this.metadata;
143
+ this.retryPolicy = retryPolicy;
130
144
  }
131
145
  getWriters() {
132
146
  const newWriters = [...this.writers];
@@ -188,6 +202,7 @@ class PregelNode extends runnables_1.RunnableBinding {
188
202
  bound: this.bound,
189
203
  kwargs: this.kwargs,
190
204
  config: this.config,
205
+ retryPolicy: this.retryPolicy,
191
206
  });
192
207
  }
193
208
  pipe(coerceable) {
@@ -200,6 +215,7 @@ class PregelNode extends runnables_1.RunnableBinding {
200
215
  bound: this.bound,
201
216
  config: this.config,
202
217
  kwargs: this.kwargs,
218
+ retryPolicy: this.retryPolicy,
203
219
  });
204
220
  }
205
221
  else if (this.bound === defaultRunnableBound) {
@@ -211,6 +227,7 @@ class PregelNode extends runnables_1.RunnableBinding {
211
227
  bound: (0, runnables_1._coerceToRunnable)(coerceable),
212
228
  config: this.config,
213
229
  kwargs: this.kwargs,
230
+ retryPolicy: this.retryPolicy,
214
231
  });
215
232
  }
216
233
  else {
@@ -222,6 +239,7 @@ class PregelNode extends runnables_1.RunnableBinding {
222
239
  bound: this.bound.pipe(coerceable),
223
240
  config: this.config,
224
241
  kwargs: this.kwargs,
242
+ retryPolicy: this.retryPolicy,
225
243
  });
226
244
  }
227
245
  }
@@ -1,5 +1,6 @@
1
1
  import { Runnable, RunnableBinding, RunnableBindingArgs, RunnableConfig, RunnableLike } from "@langchain/core/runnables";
2
2
  import { RunnableCallable } from "../utils.js";
3
+ import type { RetryPolicy } from "./utils.js";
3
4
  export declare class ChannelRead<RunInput = any> extends RunnableCallable {
4
5
  lc_graph_name: string;
5
6
  channel: string | Array<string>;
@@ -17,6 +18,8 @@ interface PregelNodeArgs<RunInput, RunOutput> extends Partial<RunnableBindingArg
17
18
  bound?: Runnable<RunInput, RunOutput>;
18
19
  kwargs?: Record<string, any>;
19
20
  config?: RunnableConfig;
21
+ metadata?: Record<string, unknown>;
22
+ retryPolicy?: RetryPolicy;
20
23
  }
21
24
  export type PregelNodeInputType = any;
22
25
  export type PregelNodeOutputType = any;
@@ -28,6 +31,8 @@ export declare class PregelNode<RunInput = PregelNodeInputType, RunOutput = Preg
28
31
  writers: Runnable[];
29
32
  bound: Runnable<RunInput, RunOutput>;
30
33
  kwargs: Record<string, any>;
34
+ metadata: Record<string, unknown>;
35
+ retryPolicy?: RetryPolicy;
31
36
  constructor(fields: PregelNodeArgs<RunInput, RunOutput>);
32
37
  getWriters(): Array<Runnable>;
33
38
  getNode(): Runnable<RunInput, RunOutput> | undefined;
@@ -59,7 +59,7 @@ const defaultRunnableBound =
59
59
  /* #__PURE__ */ new RunnablePassthrough();
60
60
  export class PregelNode extends RunnableBinding {
61
61
  constructor(fields) {
62
- const { channels, triggers, mapper, writers, bound, kwargs } = fields;
62
+ const { channels, triggers, mapper, writers, bound, kwargs, metadata, retryPolicy, } = fields;
63
63
  const mergedTags = [
64
64
  ...(fields.config?.tags ? fields.config.tags : []),
65
65
  ...(fields.tags ? fields.tags : []),
@@ -117,12 +117,26 @@ export class PregelNode extends RunnableBinding {
117
117
  writable: true,
118
118
  value: {}
119
119
  });
120
+ Object.defineProperty(this, "metadata", {
121
+ enumerable: true,
122
+ configurable: true,
123
+ writable: true,
124
+ value: {}
125
+ });
126
+ Object.defineProperty(this, "retryPolicy", {
127
+ enumerable: true,
128
+ configurable: true,
129
+ writable: true,
130
+ value: void 0
131
+ });
120
132
  this.channels = channels;
121
133
  this.triggers = triggers;
122
134
  this.mapper = mapper;
123
135
  this.writers = writers ?? this.writers;
124
136
  this.bound = bound ?? this.bound;
125
137
  this.kwargs = kwargs ?? this.kwargs;
138
+ this.metadata = metadata ?? this.metadata;
139
+ this.retryPolicy = retryPolicy;
126
140
  }
127
141
  getWriters() {
128
142
  const newWriters = [...this.writers];
@@ -184,6 +198,7 @@ export class PregelNode extends RunnableBinding {
184
198
  bound: this.bound,
185
199
  kwargs: this.kwargs,
186
200
  config: this.config,
201
+ retryPolicy: this.retryPolicy,
187
202
  });
188
203
  }
189
204
  pipe(coerceable) {
@@ -196,6 +211,7 @@ export class PregelNode extends RunnableBinding {
196
211
  bound: this.bound,
197
212
  config: this.config,
198
213
  kwargs: this.kwargs,
214
+ retryPolicy: this.retryPolicy,
199
215
  });
200
216
  }
201
217
  else if (this.bound === defaultRunnableBound) {
@@ -207,6 +223,7 @@ export class PregelNode extends RunnableBinding {
207
223
  bound: _coerceToRunnable(coerceable),
208
224
  config: this.config,
209
225
  kwargs: this.kwargs,
226
+ retryPolicy: this.retryPolicy,
210
227
  });
211
228
  }
212
229
  else {
@@ -218,6 +235,7 @@ export class PregelNode extends RunnableBinding {
218
235
  bound: this.bound.pipe(coerceable),
219
236
  config: this.config,
220
237
  kwargs: this.kwargs,
238
+ retryPolicy: this.retryPolicy,
221
239
  });
222
240
  }
223
241
  }
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeTasksWithRetry = exports.DEFAULT_MAX_RETRIES = exports.DEFAULT_MAX_INTERVAL = exports.DEFAULT_BACKOFF_FACTOR = exports.DEFAULT_INITIAL_INTERVAL = void 0;
4
+ const errors_js_1 = require("../errors.cjs");
5
+ exports.DEFAULT_INITIAL_INTERVAL = 500;
6
+ exports.DEFAULT_BACKOFF_FACTOR = 2;
7
+ exports.DEFAULT_MAX_INTERVAL = 128000;
8
+ exports.DEFAULT_MAX_RETRIES = 3;
9
+ const DEFAULT_STATUS_NO_RETRY = [
10
+ 400,
11
+ 401,
12
+ 402,
13
+ 403,
14
+ 404,
15
+ 405,
16
+ 406,
17
+ 407,
18
+ 409, // Conflict
19
+ ];
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ const DEFAULT_RETRY_ON_HANDLER = (error) => {
22
+ if (error.message.startsWith("Cancel") ||
23
+ error.message.startsWith("AbortError") ||
24
+ error.name === "AbortError") {
25
+ return false;
26
+ }
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ if (error?.code === "ECONNABORTED") {
29
+ return false;
30
+ }
31
+ const status =
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ error?.response?.status ?? error?.status;
34
+ if (status && DEFAULT_STATUS_NO_RETRY.includes(+status)) {
35
+ return false;
36
+ }
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ if (error?.error?.code === "insufficient_quota") {
39
+ return false;
40
+ }
41
+ return true;
42
+ };
43
+ async function* executeTasksWithRetry(
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ tasks, options
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ ) {
48
+ const { stepTimeout, retryPolicy } = options ?? {};
49
+ let signal = options?.signal;
50
+ // Start tasks
51
+ const executingTasksMap = Object.fromEntries(tasks.map((pregelTask) => {
52
+ return [pregelTask.id, _runWithRetry(pregelTask, retryPolicy)];
53
+ }));
54
+ if (stepTimeout && signal) {
55
+ if ("any" in AbortSignal) {
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ signal = AbortSignal.any([
58
+ signal,
59
+ AbortSignal.timeout(stepTimeout),
60
+ ]);
61
+ }
62
+ }
63
+ else if (stepTimeout) {
64
+ signal = AbortSignal.timeout(stepTimeout);
65
+ }
66
+ // Abort if signal is aborted
67
+ signal?.throwIfAborted();
68
+ let listener;
69
+ const signalPromise = new Promise((_resolve, reject) => {
70
+ listener = () => reject(new Error("Abort"));
71
+ signal?.addEventListener("abort", listener);
72
+ }).finally(() => signal?.removeEventListener("abort", listener));
73
+ while (Object.keys(executingTasksMap).length > 0) {
74
+ const { task, error } = await Promise.race([
75
+ ...Object.values(executingTasksMap),
76
+ signalPromise,
77
+ ]);
78
+ if (error !== undefined) {
79
+ // TODO: don't stop others if exception is interrupt
80
+ throw error;
81
+ }
82
+ yield task;
83
+ delete executingTasksMap[task.id];
84
+ }
85
+ }
86
+ exports.executeTasksWithRetry = executeTasksWithRetry;
87
+ async function _runWithRetry(
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ pregelTask, retryPolicy) {
90
+ const resolvedRetryPolicy = pregelTask.retry_policy ?? retryPolicy;
91
+ let interval = resolvedRetryPolicy !== undefined
92
+ ? resolvedRetryPolicy.initialInterval ?? exports.DEFAULT_INITIAL_INTERVAL
93
+ : 0;
94
+ let attempts = 0;
95
+ let error;
96
+ let result;
97
+ // eslint-disable-next-line no-constant-condition
98
+ while (true) {
99
+ // Modify writes in place to clear any previous retries
100
+ while (pregelTask.writes.length > 0) {
101
+ pregelTask.writes.pop();
102
+ }
103
+ error = undefined;
104
+ try {
105
+ result = await pregelTask.proc.invoke(pregelTask.input, pregelTask.config);
106
+ break;
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
+ }
109
+ catch (e) {
110
+ error = e;
111
+ error.pregelTaskId = pregelTask.id;
112
+ if (error.name === errors_js_1.GraphInterrupt.unminifiable_name) {
113
+ break;
114
+ }
115
+ if (resolvedRetryPolicy === undefined) {
116
+ break;
117
+ }
118
+ attempts += 1;
119
+ // check if we should give up
120
+ if (attempts >= (resolvedRetryPolicy.maxAttempts ?? exports.DEFAULT_MAX_RETRIES)) {
121
+ break;
122
+ }
123
+ const retryOn = resolvedRetryPolicy.retryOn ?? DEFAULT_RETRY_ON_HANDLER;
124
+ if (!retryOn(error)) {
125
+ break;
126
+ }
127
+ interval = Math.min(resolvedRetryPolicy.maxInterval ?? exports.DEFAULT_MAX_INTERVAL, interval * (resolvedRetryPolicy.backoffFactor ?? exports.DEFAULT_BACKOFF_FACTOR));
128
+ const intervalWithJitter = resolvedRetryPolicy.jitter
129
+ ? Math.floor(interval + Math.random() * 1000)
130
+ : interval;
131
+ // sleep before retrying
132
+ // eslint-disable-next-line no-promise-executor-return
133
+ await new Promise((resolve) => setTimeout(resolve, intervalWithJitter));
134
+ // log the retry
135
+ const errorName = error.name ??
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ error.constructor.unminifiable_name ??
138
+ error.constructor.name;
139
+ console.log(`Retrying task "${pregelTask.name}" after ${interval.toFixed(2)} seconds (attempt ${attempts}) after ${errorName}: ${error}`);
140
+ }
141
+ }
142
+ return {
143
+ task: pregelTask,
144
+ result,
145
+ error,
146
+ };
147
+ }
@@ -0,0 +1,11 @@
1
+ import { PregelExecutableTask } from "./types.js";
2
+ import type { RetryPolicy } from "./utils.js";
3
+ export declare const DEFAULT_INITIAL_INTERVAL = 500;
4
+ export declare const DEFAULT_BACKOFF_FACTOR = 2;
5
+ export declare const DEFAULT_MAX_INTERVAL = 128000;
6
+ export declare const DEFAULT_MAX_RETRIES = 3;
7
+ export declare function executeTasksWithRetry(tasks: PregelExecutableTask<any, any>[], options?: {
8
+ stepTimeout?: number;
9
+ signal?: AbortSignal;
10
+ retryPolicy?: RetryPolicy;
11
+ }): AsyncGenerator<PregelExecutableTask<any, any>>;
@@ -0,0 +1,143 @@
1
+ import { GraphInterrupt } from "../errors.js";
2
+ export const DEFAULT_INITIAL_INTERVAL = 500;
3
+ export const DEFAULT_BACKOFF_FACTOR = 2;
4
+ export const DEFAULT_MAX_INTERVAL = 128000;
5
+ export const DEFAULT_MAX_RETRIES = 3;
6
+ const DEFAULT_STATUS_NO_RETRY = [
7
+ 400,
8
+ 401,
9
+ 402,
10
+ 403,
11
+ 404,
12
+ 405,
13
+ 406,
14
+ 407,
15
+ 409, // Conflict
16
+ ];
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ const DEFAULT_RETRY_ON_HANDLER = (error) => {
19
+ if (error.message.startsWith("Cancel") ||
20
+ error.message.startsWith("AbortError") ||
21
+ error.name === "AbortError") {
22
+ return false;
23
+ }
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ if (error?.code === "ECONNABORTED") {
26
+ return false;
27
+ }
28
+ const status =
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ error?.response?.status ?? error?.status;
31
+ if (status && DEFAULT_STATUS_NO_RETRY.includes(+status)) {
32
+ return false;
33
+ }
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ if (error?.error?.code === "insufficient_quota") {
36
+ return false;
37
+ }
38
+ return true;
39
+ };
40
+ export async function* executeTasksWithRetry(
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ tasks, options
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ ) {
45
+ const { stepTimeout, retryPolicy } = options ?? {};
46
+ let signal = options?.signal;
47
+ // Start tasks
48
+ const executingTasksMap = Object.fromEntries(tasks.map((pregelTask) => {
49
+ return [pregelTask.id, _runWithRetry(pregelTask, retryPolicy)];
50
+ }));
51
+ if (stepTimeout && signal) {
52
+ if ("any" in AbortSignal) {
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ signal = AbortSignal.any([
55
+ signal,
56
+ AbortSignal.timeout(stepTimeout),
57
+ ]);
58
+ }
59
+ }
60
+ else if (stepTimeout) {
61
+ signal = AbortSignal.timeout(stepTimeout);
62
+ }
63
+ // Abort if signal is aborted
64
+ signal?.throwIfAborted();
65
+ let listener;
66
+ const signalPromise = new Promise((_resolve, reject) => {
67
+ listener = () => reject(new Error("Abort"));
68
+ signal?.addEventListener("abort", listener);
69
+ }).finally(() => signal?.removeEventListener("abort", listener));
70
+ while (Object.keys(executingTasksMap).length > 0) {
71
+ const { task, error } = await Promise.race([
72
+ ...Object.values(executingTasksMap),
73
+ signalPromise,
74
+ ]);
75
+ if (error !== undefined) {
76
+ // TODO: don't stop others if exception is interrupt
77
+ throw error;
78
+ }
79
+ yield task;
80
+ delete executingTasksMap[task.id];
81
+ }
82
+ }
83
+ async function _runWithRetry(
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ pregelTask, retryPolicy) {
86
+ const resolvedRetryPolicy = pregelTask.retry_policy ?? retryPolicy;
87
+ let interval = resolvedRetryPolicy !== undefined
88
+ ? resolvedRetryPolicy.initialInterval ?? DEFAULT_INITIAL_INTERVAL
89
+ : 0;
90
+ let attempts = 0;
91
+ let error;
92
+ let result;
93
+ // eslint-disable-next-line no-constant-condition
94
+ while (true) {
95
+ // Modify writes in place to clear any previous retries
96
+ while (pregelTask.writes.length > 0) {
97
+ pregelTask.writes.pop();
98
+ }
99
+ error = undefined;
100
+ try {
101
+ result = await pregelTask.proc.invoke(pregelTask.input, pregelTask.config);
102
+ break;
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ }
105
+ catch (e) {
106
+ error = e;
107
+ error.pregelTaskId = pregelTask.id;
108
+ if (error.name === GraphInterrupt.unminifiable_name) {
109
+ break;
110
+ }
111
+ if (resolvedRetryPolicy === undefined) {
112
+ break;
113
+ }
114
+ attempts += 1;
115
+ // check if we should give up
116
+ if (attempts >= (resolvedRetryPolicy.maxAttempts ?? DEFAULT_MAX_RETRIES)) {
117
+ break;
118
+ }
119
+ const retryOn = resolvedRetryPolicy.retryOn ?? DEFAULT_RETRY_ON_HANDLER;
120
+ if (!retryOn(error)) {
121
+ break;
122
+ }
123
+ interval = Math.min(resolvedRetryPolicy.maxInterval ?? DEFAULT_MAX_INTERVAL, interval * (resolvedRetryPolicy.backoffFactor ?? DEFAULT_BACKOFF_FACTOR));
124
+ const intervalWithJitter = resolvedRetryPolicy.jitter
125
+ ? Math.floor(interval + Math.random() * 1000)
126
+ : interval;
127
+ // sleep before retrying
128
+ // eslint-disable-next-line no-promise-executor-return
129
+ await new Promise((resolve) => setTimeout(resolve, intervalWithJitter));
130
+ // log the retry
131
+ const errorName = error.name ??
132
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
133
+ error.constructor.unminifiable_name ??
134
+ error.constructor.name;
135
+ console.log(`Retrying task "${pregelTask.name}" after ${interval.toFixed(2)} seconds (attempt ${attempts}) after ${errorName}: ${error}`);
136
+ }
137
+ }
138
+ return {
139
+ task: pregelTask,
140
+ result,
141
+ error,
142
+ };
143
+ }
@@ -2,6 +2,7 @@ import type { Runnable, RunnableConfig } from "@langchain/core/runnables";
2
2
  import type { PendingWrite, CheckpointMetadata, BaseCheckpointSaver } from "@langchain/langgraph-checkpoint";
3
3
  import type { BaseChannel } from "../channels/base.js";
4
4
  import type { PregelNode } from "./read.js";
5
+ import { RetryPolicy } from "./utils.js";
5
6
  export type StreamMode = "values" | "updates" | "debug";
6
7
  /**
7
8
  * Construct a type with a set of properties K of type T
@@ -41,6 +42,7 @@ export interface PregelInterface<Nn extends StrRecord<string, PregelNode>, Cc ex
41
42
  */
42
43
  debug?: boolean;
43
44
  checkpointer?: BaseCheckpointSaver;
45
+ retryPolicy?: RetryPolicy;
44
46
  }
45
47
  export type PregelParams<Nn extends StrRecord<string, PregelNode>, Cc extends StrRecord<string, BaseChannel>> = Omit<PregelInterface<Nn, Cc>, "streamChannelsAsIs">;
46
48
  export interface PregelTaskDescription {
@@ -53,9 +55,9 @@ export interface PregelExecutableTask<N extends PropertyKey, C extends PropertyK
53
55
  readonly input: unknown;
54
56
  readonly proc: Runnable;
55
57
  readonly writes: PendingWrite<C>[];
56
- readonly config: RunnableConfig | undefined;
58
+ readonly config?: RunnableConfig;
57
59
  readonly triggers: Array<string>;
58
- readonly retry_policy?: string;
60
+ readonly retry_policy?: RetryPolicy;
59
61
  readonly id: string;
60
62
  }
61
63
  export interface StateSnapshot {
@@ -8,3 +8,29 @@ export declare function _getIdMetadata(metadata: Record<string, unknown>): {
8
8
  langgraph_triggers: unknown;
9
9
  langgraph_task_idx: unknown;
10
10
  };
11
+ export type RetryPolicy = {
12
+ /**
13
+ * Amount of time that must elapse before the first retry occurs in milliseconds.
14
+ * @default 500
15
+ */
16
+ initialInterval?: number;
17
+ /**
18
+ * Multiplier by which the interval increases after each retry.
19
+ * @default 2
20
+ */
21
+ backoffFactor?: number;
22
+ /**
23
+ * Maximum amount of time that may elapse between retries in milliseconds.
24
+ * @default 128000
25
+ */
26
+ maxInterval?: number;
27
+ /**
28
+ * Maximum amount of time that may elapse between retries.
29
+ * @default 3
30
+ */
31
+ maxAttempts?: number;
32
+ /** Whether to add random jitter to the interval between retries. */
33
+ jitter?: boolean;
34
+ /** A function that returns True for exceptions that should trigger a retry. */
35
+ retryOn?: (e: any) => boolean;
36
+ };
package/dist/web.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { END, Graph, type StateGraphArgs, START, StateGraph, type CompiledStateGraph, MessageGraph, messagesStateReducer, Annotation, type StateType, type UpdateType, type CompiledGraph, } from "./graph/index.js";
2
2
  export { GraphRecursionError, GraphValueError, InvalidUpdateError, EmptyChannelError, } from "./errors.js";
3
+ export { type RetryPolicy } from "./pregel/utils.js";
3
4
  export { Send } from "./constants.js";
4
5
  export { MemorySaver, type Checkpoint, type CheckpointMetadata, type CheckpointTuple, copyCheckpoint, emptyCheckpoint, BaseCheckpointSaver, } from "@langchain/langgraph-checkpoint";