@langchain/langgraph-api 0.0.34 → 0.0.36

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/api/runs.mjs CHANGED
@@ -75,6 +75,7 @@ const createValidRun = async (threadId, payload, kwargs) => {
75
75
  feedback_keys: feedbackKeys,
76
76
  temporary: threadId == null && (run.on_completion ?? "delete") === "delete",
77
77
  subgraphs: run.stream_subgraphs ?? false,
78
+ resumable: run.stream_resumable ?? false,
78
79
  }, {
79
80
  threadId,
80
81
  userId,
@@ -139,7 +140,11 @@ api.post("/runs/stream", zValidator("json", schemas.RunCreate), async (c) => {
139
140
  ? getDisconnectAbortSignal(c, stream)
140
141
  : undefined;
141
142
  try {
142
- for await (const { event, data } of Runs.Stream.join(run.run_id, undefined, { cancelOnDisconnect, ignore404: true }, c.var.auth)) {
143
+ for await (const { event, data } of Runs.Stream.join(run.run_id, undefined, {
144
+ cancelOnDisconnect,
145
+ lastEventId: run.kwargs.resumable ? "-1" : undefined,
146
+ ignore404: true,
147
+ }, c.var.auth)) {
143
148
  await stream.writeSSE({ data: serialiseAsDict(data), event });
144
149
  }
145
150
  }
@@ -148,6 +153,25 @@ api.post("/runs/stream", zValidator("json", schemas.RunCreate), async (c) => {
148
153
  }
149
154
  });
150
155
  });
156
+ // TODO: port to Python API
157
+ api.get("/runs/:run_id/stream", zValidator("param", z.object({ run_id: z.string().uuid() })), zValidator("query", z.object({ cancel_on_disconnect: schemas.coercedBoolean.optional() })), async (c) => {
158
+ const { run_id } = c.req.valid("param");
159
+ const query = c.req.valid("query");
160
+ const lastEventId = c.req.header("Last-Event-ID") || undefined;
161
+ return streamSSE(c, async (stream) => {
162
+ const cancelOnDisconnect = query.cancel_on_disconnect
163
+ ? getDisconnectAbortSignal(c, stream)
164
+ : undefined;
165
+ try {
166
+ for await (const { id, event, data } of Runs.Stream.join(run_id, undefined, { cancelOnDisconnect, lastEventId, ignore404: true }, c.var.auth)) {
167
+ await stream.writeSSE({ id, data: serialiseAsDict(data), event });
168
+ }
169
+ }
170
+ catch (error) {
171
+ logError(error, { prefix: "Error streaming run" });
172
+ }
173
+ });
174
+ });
151
175
  api.post("/runs/wait", zValidator("json", schemas.RunCreate), async (c) => {
152
176
  // Wait Stateless Run
153
177
  const payload = c.req.valid("json");
@@ -198,6 +222,7 @@ api.post("/threads/:thread_id/runs", zValidator("param", z.object({ thread_id: z
198
222
  auth: c.var.auth,
199
223
  headers: c.req.raw.headers,
200
224
  });
225
+ c.header("Content-Location", `/threads/${thread_id}/runs/${run.run_id}`);
201
226
  return jsonExtra(c, run);
202
227
  });
203
228
  api.post("/threads/:thread_id/runs/stream", zValidator("param", z.object({ thread_id: z.string().uuid() })), zValidator("json", schemas.RunCreate), async (c) => {
@@ -208,13 +233,17 @@ api.post("/threads/:thread_id/runs/stream", zValidator("param", z.object({ threa
208
233
  auth: c.var.auth,
209
234
  headers: c.req.raw.headers,
210
235
  });
236
+ c.header("Content-Location", `/threads/${thread_id}/runs/${run.run_id}`);
211
237
  return streamSSE(c, async (stream) => {
212
238
  const cancelOnDisconnect = payload.on_disconnect === "cancel"
213
239
  ? getDisconnectAbortSignal(c, stream)
214
240
  : undefined;
215
241
  try {
216
- for await (const { event, data } of Runs.Stream.join(run.run_id, thread_id, { cancelOnDisconnect }, c.var.auth)) {
217
- await stream.writeSSE({ data: serialiseAsDict(data), event });
242
+ for await (const { id, event, data } of Runs.Stream.join(run.run_id, thread_id, {
243
+ cancelOnDisconnect,
244
+ lastEventId: run.kwargs.resumable ? "-1" : undefined,
245
+ }, c.var.auth)) {
246
+ await stream.writeSSE({ id, data: serialiseAsDict(data), event });
218
247
  }
219
248
  }
220
249
  catch (error) {
@@ -230,6 +259,7 @@ api.post("/threads/:thread_id/runs/wait", zValidator("param", z.object({ thread_
230
259
  auth: c.var.auth,
231
260
  headers: c.req.raw.headers,
232
261
  });
262
+ c.header("Content-Location", `/threads/${thread_id}/runs/${run.run_id}`);
233
263
  return waitKeepAlive(c, Runs.join(run.run_id, thread_id, c.var.auth));
234
264
  });
235
265
  api.get("/threads/:thread_id/runs/:run_id", zValidator("param", z.object({ thread_id: z.string().uuid(), run_id: z.string().uuid() })), async (c) => {
@@ -257,12 +287,13 @@ api.get("/threads/:thread_id/runs/:run_id/stream", zValidator("param", z.object(
257
287
  // Stream Run Http
258
288
  const { thread_id, run_id } = c.req.valid("param");
259
289
  const { cancel_on_disconnect } = c.req.valid("query");
290
+ const lastEventId = c.req.header("Last-Event-ID") || undefined;
260
291
  return streamSSE(c, async (stream) => {
261
292
  const signal = cancel_on_disconnect
262
293
  ? getDisconnectAbortSignal(c, stream)
263
294
  : undefined;
264
- for await (const { event, data } of Runs.Stream.join(run_id, thread_id, { cancelOnDisconnect: signal }, c.var.auth)) {
265
- await stream.writeSSE({ data: serialiseAsDict(data), event });
295
+ for await (const { id, event, data } of Runs.Stream.join(run_id, thread_id, { cancelOnDisconnect: signal, lastEventId }, c.var.auth)) {
296
+ await stream.writeSSE({ id, data: serialiseAsDict(data), event });
266
297
  }
267
298
  });
268
299
  });
@@ -10,8 +10,14 @@ export interface GraphSpec {
10
10
  sourceFile: string;
11
11
  exportSymbol: string;
12
12
  }
13
+ type GraphSchemaWithSubgraphs = Record<string, GraphSchema>;
13
14
  export declare function getStaticGraphSchema(spec: GraphSpec, options?: {
14
15
  mainThread?: boolean;
15
16
  timeoutMs?: number;
16
- }): Promise<Record<string, GraphSchema>>;
17
+ }): Promise<GraphSchemaWithSubgraphs>;
18
+ export declare function getStaticGraphSchema(specMap: Record<string, GraphSpec>, options?: {
19
+ mainThread?: boolean;
20
+ timeoutMs?: number;
21
+ }): Promise<Record<string, GraphSchemaWithSubgraphs>>;
17
22
  export declare function getRuntimeGraphSchema(graph: Pregel<any, any, any, any, any>): Promise<GraphSchema | undefined>;
23
+ export {};
@@ -1,25 +1,42 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import { Worker } from "node:worker_threads";
3
- export async function getStaticGraphSchema(spec, options) {
4
- if (options?.mainThread) {
5
- const { SubgraphExtractor } = await import("./parser.mjs");
6
- return SubgraphExtractor.extractSchemas(spec.sourceFile, spec.exportSymbol, { strict: false });
7
- }
8
- return await new Promise((resolve, reject) => {
9
- const worker = new Worker(fileURLToPath(new URL("./parser.worker.mjs", import.meta.url)), { argv: process.argv.slice(-1) });
10
- // Set a timeout to reject if the worker takes too long
11
- const timeoutId = setTimeout(() => {
12
- worker.terminate();
13
- reject(new Error("Schema extract worker timed out"));
14
- }, options?.timeoutMs ?? 30000);
15
- worker.on("message", (result) => {
16
- worker.terminate();
17
- clearTimeout(timeoutId);
18
- resolve(result);
3
+ const isGraphSpec = (spec) => {
4
+ if (typeof spec !== "object" || spec == null)
5
+ return false;
6
+ if (!("sourceFile" in spec) || typeof spec.sourceFile !== "string")
7
+ return false;
8
+ if (!("exportSymbol" in spec) || typeof spec.exportSymbol !== "string")
9
+ return false;
10
+ return true;
11
+ };
12
+ export async function getStaticGraphSchema(input, options) {
13
+ async function execute(specs) {
14
+ if (options?.mainThread) {
15
+ const { SubgraphExtractor } = await import("./parser.mjs");
16
+ return SubgraphExtractor.extractSchemas(specs, { strict: false });
17
+ }
18
+ return await new Promise((resolve, reject) => {
19
+ const worker = new Worker(fileURLToPath(new URL("./parser.worker.mjs", import.meta.url)), { argv: process.argv.slice(-1) });
20
+ // Set a timeout to reject if the worker takes too long
21
+ const timeoutId = setTimeout(() => {
22
+ worker.terminate();
23
+ reject(new Error("Schema extract worker timed out"));
24
+ }, options?.timeoutMs ?? 30000);
25
+ worker.on("message", (result) => {
26
+ worker.terminate();
27
+ clearTimeout(timeoutId);
28
+ resolve(result);
29
+ });
30
+ worker.on("error", reject);
31
+ worker.postMessage(specs);
19
32
  });
20
- worker.on("error", reject);
21
- worker.postMessage(spec);
22
- });
33
+ }
34
+ const specs = isGraphSpec(input) ? [input] : Object.values(input);
35
+ const results = await execute(specs);
36
+ if (isGraphSpec(input)) {
37
+ return results[0];
38
+ }
39
+ return Object.fromEntries(Object.keys(input).map((graphId, idx) => [graphId, results[idx]]));
23
40
  }
24
41
  export async function getRuntimeGraphSchema(graph) {
25
42
  try {
@@ -35,8 +35,15 @@ export declare class SubgraphExtractor {
35
35
  node: ts.Node;
36
36
  };
37
37
  }[];
38
- getAugmentedSourceFile: (name: string) => {
39
- files: [filePath: string, contents: string][];
38
+ getAugmentedSourceFile: (suffix: string, name: string) => {
39
+ inferFile: {
40
+ fileName: string;
41
+ contents: string;
42
+ };
43
+ sourceFile: {
44
+ fileName: string;
45
+ contents: string;
46
+ };
40
47
  exports: {
41
48
  typeName: string;
42
49
  valueName: string;
@@ -53,11 +60,15 @@ export declare class SubgraphExtractor {
53
60
  protected isGraphOrPregelType: (type: ts.Type) => boolean;
54
61
  protected getText(node: ts.Node): string;
55
62
  protected reduceChildren<Acc>(node: ts.Node, fn: (acc: Acc, node: ts.Node) => Acc, initial: Acc): Acc;
56
- static extractSchemas(target: string | {
57
- contents: string;
58
- files?: [fileName: string, contents: string][];
59
- }, name: string, options?: {
63
+ static extractSchemas(target: {
64
+ sourceFile: string | {
65
+ name: string;
66
+ contents: string;
67
+ main?: boolean;
68
+ }[];
69
+ exportSymbol: string;
70
+ }[], options?: {
60
71
  strict?: boolean;
61
- }): Record<string, GraphSchema>;
72
+ }): Record<string, GraphSchema>[];
62
73
  }
63
74
  export {};
@@ -11,6 +11,7 @@ const OVERRIDE_RESOLVE = [
11
11
  new RegExp(`^@langchain\/langgraph(\/.+)?$`),
12
12
  new RegExp(`^@langchain\/langgraph-checkpoint(\/.+)?$`),
13
13
  ];
14
+ const INFER_TEMPLATE_PATH = path.resolve(__dirname, "./schema/types.template.mts");
14
15
  const compilerOptions = {
15
16
  noEmit: true,
16
17
  strict: true,
@@ -144,7 +145,7 @@ export class SubgraphExtractor {
144
145
  return this.findSubgraphs(varDecl.initializer, [name]);
145
146
  });
146
147
  };
147
- getAugmentedSourceFile = (name) => {
148
+ getAugmentedSourceFile = (suffix, name) => {
148
149
  const vars = this.getSubgraphsVariables(name);
149
150
  const typeExports = [
150
151
  { typeName: `__langgraph__${name}`, valueName: name, graphName: name },
@@ -156,14 +157,14 @@ export class SubgraphExtractor {
156
157
  graphName: [...namespace, node].join("|"),
157
158
  });
158
159
  }
159
- const sourceFilePath = "__langgraph__source.mts";
160
+ const sourceFilePath = `__langgraph__source_${suffix}.mts`;
160
161
  const sourceContents = [
161
162
  this.getText(this.sourceFile),
162
163
  ...typeExports.map(({ typeName, valueName }) => `export type ${typeName} = typeof ${valueName}`),
163
164
  ].join("\n\n");
164
- const inferFilePath = "__langgraph__infer.mts";
165
+ const inferFilePath = `__langgraph__infer_${suffix}.mts`;
165
166
  const inferContents = [
166
- ...typeExports.map(({ typeName }) => `import type { ${typeName}} from "./__langgraph__source.mts"`),
167
+ ...typeExports.map(({ typeName }) => `import type { ${typeName}} from "./__langgraph__source_${suffix}.mts"`),
167
168
  this.inferFile.getText(this.inferFile),
168
169
  ...typeExports.flatMap(({ typeName }) => {
169
170
  return [
@@ -181,10 +182,8 @@ export class SubgraphExtractor {
181
182
  }),
182
183
  ].join("\n\n");
183
184
  return {
184
- files: [
185
- [sourceFilePath, sourceContents],
186
- [inferFilePath, inferContents],
187
- ],
185
+ inferFile: { fileName: inferFilePath, contents: inferContents },
186
+ sourceFile: { fileName: sourceFilePath, contents: sourceContents },
188
187
  exports: typeExports,
189
188
  };
190
189
  };
@@ -228,11 +227,34 @@ export class SubgraphExtractor {
228
227
  ts.forEachChild(node, it.bind(this));
229
228
  return acc;
230
229
  }
231
- static extractSchemas(target, name, options) {
232
- const dirname = typeof target === "string" ? path.dirname(target) : __dirname;
230
+ static extractSchemas(target, options) {
231
+ if (!target.length)
232
+ throw new Error("No graphs found");
233
+ function getCommonPath(a, b) {
234
+ const aSeg = path.normalize(a).split(path.sep);
235
+ const bSeg = path.normalize(b).split(path.sep);
236
+ const maxIter = Math.min(aSeg.length, bSeg.length);
237
+ const result = [];
238
+ for (let i = 0; i < maxIter; ++i) {
239
+ if (aSeg[i] !== bSeg[i])
240
+ break;
241
+ result.push(aSeg[i]);
242
+ }
243
+ return result.join(path.sep);
244
+ }
245
+ const isTestTarget = (check) => {
246
+ return check.every((x) => typeof x.sourceFile === "string");
247
+ };
248
+ const projectDirname = isTestTarget(target)
249
+ ? target.reduce((acc, item) => {
250
+ if (!acc)
251
+ return path.dirname(item.sourceFile);
252
+ return getCommonPath(acc, path.dirname(item.sourceFile));
253
+ }, "")
254
+ : __dirname;
233
255
  // This API is not well made for Windows, ensure that the paths are UNIX slashes
234
256
  const fsMap = new Map();
235
- const system = vfs.createFSBackedSystem(fsMap, dirname, ts);
257
+ const system = vfs.createFSBackedSystem(fsMap, projectDirname, ts);
236
258
  // TODO: investigate if we should create a PR in @typescript/vfs
237
259
  const oldReadFile = system.readFile.bind(system);
238
260
  system.readFile = (fileName) => oldReadFile(fileName) ?? "// Non-existent file";
@@ -243,17 +265,24 @@ export class SubgraphExtractor {
243
265
  };
244
266
  const vfsHost = vfs.createVirtualCompilerHost(system, compilerOptions, ts);
245
267
  const host = vfsHost.compilerHost;
246
- const targetPath = typeof target === "string"
247
- ? target
248
- : path.resolve(dirname, "./__langgraph__target.mts");
249
- const inferTemplatePath = path.resolve(__dirname, "./schema/types.template.mts");
250
- if (typeof target !== "string") {
251
- fsMap.set(vfsPath(targetPath), target.contents);
252
- for (const [name, contents] of target.files ?? []) {
253
- fsMap.set(vfsPath(path.resolve(dirname, name)), contents);
268
+ const targetPaths = [];
269
+ for (const item of target) {
270
+ if (typeof item.sourceFile === "string") {
271
+ targetPaths.push({ ...item, sourceFile: item.sourceFile });
272
+ }
273
+ else {
274
+ for (const { name, contents, main } of item.sourceFile ?? []) {
275
+ fsMap.set(vfsPath(path.resolve(projectDirname, name)), contents);
276
+ if (main) {
277
+ targetPaths.push({
278
+ ...item,
279
+ sourceFile: path.resolve(projectDirname, name),
280
+ });
281
+ }
282
+ }
254
283
  }
255
284
  }
256
- const moduleCache = ts.createModuleResolutionCache(dirname, (x) => x);
285
+ const moduleCache = ts.createModuleResolutionCache(projectDirname, (x) => x);
257
286
  host.resolveModuleNameLiterals = (entries, containingFile, redirectedReference, options) => entries.flatMap((entry) => {
258
287
  const specifier = entry.text;
259
288
  // Force module resolution to use @langchain/langgraph from the local project
@@ -263,7 +292,7 @@ export class SubgraphExtractor {
263
292
  // check if we're not already importing from node_modules
264
293
  if (!containingFile.split(path.sep).includes("node_modules")) {
265
294
  // Doesn't matter if the file exists, only used to nudge `ts.resolveModuleName`
266
- targetFile = path.resolve(dirname, "__langgraph__resolve.mts");
295
+ targetFile = path.resolve(projectDirname, "__langgraph__resolve.mts");
267
296
  }
268
297
  }
269
298
  return [
@@ -271,17 +300,24 @@ export class SubgraphExtractor {
271
300
  ];
272
301
  });
273
302
  const research = ts.createProgram({
274
- rootNames: [inferTemplatePath, targetPath],
303
+ rootNames: [INFER_TEMPLATE_PATH, ...targetPaths.map((i) => i.sourceFile)],
275
304
  options: compilerOptions,
276
305
  host,
277
306
  });
278
- const extractor = new SubgraphExtractor(research, research.getSourceFile(targetPath), research.getSourceFile(inferTemplatePath), options);
279
- const { files, exports } = extractor.getAugmentedSourceFile(name);
280
- for (const [name, source] of files) {
281
- system.writeFile(vfsPath(path.resolve(dirname, name)), source);
307
+ const researchTargets = [];
308
+ for (const targetPath of targetPaths) {
309
+ const extractor = new SubgraphExtractor(research, research.getSourceFile(targetPath.sourceFile), research.getSourceFile(INFER_TEMPLATE_PATH), options);
310
+ const { sourceFile, inferFile, exports } = extractor.getAugmentedSourceFile(path.basename(targetPath.sourceFile), targetPath.exportSymbol);
311
+ for (const { fileName, contents } of [sourceFile, inferFile]) {
312
+ system.writeFile(vfsPath(path.resolve(projectDirname, fileName)), contents);
313
+ }
314
+ researchTargets.push({
315
+ rootName: path.resolve(projectDirname, inferFile.fileName),
316
+ exports,
317
+ });
282
318
  }
283
319
  const extract = ts.createProgram({
284
- rootNames: [path.resolve(dirname, "./__langgraph__infer.mts")],
320
+ rootNames: researchTargets.map((i) => i.rootName),
285
321
  options: compilerOptions,
286
322
  host,
287
323
  });
@@ -295,7 +331,7 @@ export class SubgraphExtractor {
295
331
  }
296
332
  return undefined;
297
333
  };
298
- return Object.fromEntries(exports.map(({ typeName, graphName }) => [
334
+ return researchTargets.map(({ exports }) => Object.fromEntries(exports.map(({ typeName, graphName }) => [
299
335
  graphName,
300
336
  {
301
337
  state: trySymbol(schemaGenerator, `${typeName}__update`),
@@ -303,6 +339,6 @@ export class SubgraphExtractor {
303
339
  output: trySymbol(schemaGenerator, `${typeName}__output`),
304
340
  config: trySymbol(schemaGenerator, `${typeName}__config`),
305
341
  },
306
- ]));
342
+ ])));
307
343
  }
308
344
  }
@@ -2,6 +2,6 @@ import { tsImport } from "tsx/esm/api";
2
2
  import { parentPort } from "node:worker_threads";
3
3
  parentPort?.on("message", async (payload) => {
4
4
  const { SubgraphExtractor } = await tsImport("./parser.mjs", import.meta.url);
5
- const result = SubgraphExtractor.extractSchemas(payload.sourceFile, payload.exportSymbol, { strict: false });
5
+ const result = SubgraphExtractor.extractSchemas(payload, { strict: false });
6
6
  parentPort?.postMessage(result);
7
7
  });
@@ -1,7 +1,11 @@
1
1
  import { cors as honoCors } from "hono/cors";
2
2
  export const cors = (cors) => {
3
- if (cors == null)
4
- return honoCors();
3
+ if (cors == null) {
4
+ return honoCors({
5
+ origin: "*",
6
+ exposeHeaders: ["content-location"],
7
+ });
8
+ }
5
9
  const originRegex = cors.allow_origin_regex
6
10
  ? new RegExp(cors.allow_origin_regex)
7
11
  : undefined;
@@ -13,6 +17,11 @@ export const cors = (cors) => {
13
17
  return undefined;
14
18
  }
15
19
  : (cors.allow_origins ?? []);
20
+ if (!!cors.expose_headers?.length &&
21
+ cors.expose_headers.some((i) => i.toLocaleLowerCase() === "content-location")) {
22
+ console.warn("Adding missing `Content-Location` header in `cors.expose_headers`.");
23
+ cors.expose_headers.push("content-location");
24
+ }
16
25
  // TODO: handle `cors.allow_credentials`
17
26
  return honoCors({
18
27
  origin,
package/dist/queue.mjs CHANGED
@@ -45,17 +45,24 @@ const worker = async (run, attempt, abortSignal) => {
45
45
  if (attempt > MAX_RETRY_ATTEMPTS) {
46
46
  throw new Error(`Run ${run.run_id} exceeded max attempts`);
47
47
  }
48
+ const runId = run.run_id;
49
+ const resumable = run.kwargs?.resumable ?? false;
48
50
  try {
49
51
  const stream = streamState(run, attempt, {
50
52
  signal: abortSignal,
51
53
  ...(!temporary ? { onCheckpoint, onTaskResult } : undefined),
52
54
  });
53
55
  for await (const { event, data } of stream) {
54
- await Runs.Stream.publish(run.run_id, event, data);
56
+ await Runs.Stream.publish({ runId, resumable, event, data });
55
57
  }
56
58
  }
57
59
  catch (error) {
58
- await Runs.Stream.publish(run.run_id, "error", serializeError(error));
60
+ await Runs.Stream.publish({
61
+ runId,
62
+ resumable,
63
+ event: "error",
64
+ data: serializeError(error),
65
+ });
59
66
  throw error;
60
67
  }
61
68
  endedAt = new Date();
@@ -687,6 +687,7 @@ export declare const RunCreate: z.ZodObject<{
687
687
  multitask_strategy: z.ZodOptional<z.ZodEnum<["reject", "rollback", "interrupt", "enqueue"]>>;
688
688
  stream_mode: z.ZodOptional<z.ZodUnion<[z.ZodArray<z.ZodEnum<["values", "messages", "messages-tuple", "updates", "events", "debug", "custom"]>, "many">, z.ZodEnum<["values", "messages", "messages-tuple", "updates", "events", "debug", "custom"]>]>>;
689
689
  stream_subgraphs: z.ZodOptional<z.ZodBoolean>;
690
+ stream_resumable: z.ZodOptional<z.ZodBoolean>;
690
691
  after_seconds: z.ZodOptional<z.ZodNumber>;
691
692
  if_not_exists: z.ZodOptional<z.ZodEnum<["reject", "create"]>>;
692
693
  on_completion: z.ZodOptional<z.ZodEnum<["delete", "keep"]>>;
@@ -736,6 +737,7 @@ export declare const RunCreate: z.ZodObject<{
736
737
  if_not_exists?: "reject" | "create" | undefined;
737
738
  after_seconds?: number | undefined;
738
739
  stream_subgraphs?: boolean | undefined;
740
+ stream_resumable?: boolean | undefined;
739
741
  on_completion?: "delete" | "keep" | undefined;
740
742
  }, {
741
743
  assistant_id: string;
@@ -782,6 +784,7 @@ export declare const RunCreate: z.ZodObject<{
782
784
  after_seconds?: number | undefined;
783
785
  on_disconnect?: "cancel" | "continue" | undefined;
784
786
  stream_subgraphs?: boolean | undefined;
787
+ stream_resumable?: boolean | undefined;
785
788
  on_completion?: "delete" | "keep" | undefined;
786
789
  }>;
787
790
  export declare const RunBatchCreate: z.ZodArray<z.ZodObject<{
@@ -892,6 +895,7 @@ export declare const RunBatchCreate: z.ZodArray<z.ZodObject<{
892
895
  multitask_strategy: z.ZodOptional<z.ZodEnum<["reject", "rollback", "interrupt", "enqueue"]>>;
893
896
  stream_mode: z.ZodOptional<z.ZodUnion<[z.ZodArray<z.ZodEnum<["values", "messages", "messages-tuple", "updates", "events", "debug", "custom"]>, "many">, z.ZodEnum<["values", "messages", "messages-tuple", "updates", "events", "debug", "custom"]>]>>;
894
897
  stream_subgraphs: z.ZodOptional<z.ZodBoolean>;
898
+ stream_resumable: z.ZodOptional<z.ZodBoolean>;
895
899
  after_seconds: z.ZodOptional<z.ZodNumber>;
896
900
  if_not_exists: z.ZodOptional<z.ZodEnum<["reject", "create"]>>;
897
901
  on_completion: z.ZodOptional<z.ZodEnum<["delete", "keep"]>>;
@@ -941,6 +945,7 @@ export declare const RunBatchCreate: z.ZodArray<z.ZodObject<{
941
945
  if_not_exists?: "reject" | "create" | undefined;
942
946
  after_seconds?: number | undefined;
943
947
  stream_subgraphs?: boolean | undefined;
948
+ stream_resumable?: boolean | undefined;
944
949
  on_completion?: "delete" | "keep" | undefined;
945
950
  }, {
946
951
  assistant_id: string;
@@ -987,6 +992,7 @@ export declare const RunBatchCreate: z.ZodArray<z.ZodObject<{
987
992
  after_seconds?: number | undefined;
988
993
  on_disconnect?: "cancel" | "continue" | undefined;
989
994
  stream_subgraphs?: boolean | undefined;
995
+ stream_resumable?: boolean | undefined;
990
996
  on_completion?: "delete" | "keep" | undefined;
991
997
  }>, "many">;
992
998
  export declare const SearchResult: z.ZodObject<{
package/dist/schemas.mjs CHANGED
@@ -205,6 +205,7 @@ export const RunCreate = z
205
205
  ])
206
206
  .optional(),
207
207
  stream_subgraphs: z.boolean().optional(),
208
+ stream_resumable: z.boolean().optional(),
208
209
  after_seconds: z.number().optional(),
209
210
  if_not_exists: z.enum(["reject", "create"]).optional(),
210
211
  on_completion: z.enum(["delete", "keep"]).optional(),
package/dist/server.d.mts CHANGED
@@ -40,19 +40,19 @@ export declare const StartServerSchema: z.ZodObject<{
40
40
  max_age: z.ZodOptional<z.ZodNumber>;
41
41
  }, "strip", z.ZodTypeAny, {
42
42
  allow_origin_regex?: string | undefined;
43
+ expose_headers?: string[] | undefined;
43
44
  allow_origins?: string[] | undefined;
44
45
  allow_methods?: string[] | undefined;
45
46
  allow_headers?: string[] | undefined;
46
47
  allow_credentials?: boolean | undefined;
47
- expose_headers?: string[] | undefined;
48
48
  max_age?: number | undefined;
49
49
  }, {
50
50
  allow_origin_regex?: string | undefined;
51
+ expose_headers?: string[] | undefined;
51
52
  allow_origins?: string[] | undefined;
52
53
  allow_methods?: string[] | undefined;
53
54
  allow_headers?: string[] | undefined;
54
55
  allow_credentials?: boolean | undefined;
55
- expose_headers?: string[] | undefined;
56
56
  max_age?: number | undefined;
57
57
  }>>;
58
58
  }, "strip", z.ZodTypeAny, {
@@ -63,22 +63,22 @@ export declare const StartServerSchema: z.ZodObject<{
63
63
  disable_meta: boolean;
64
64
  cors?: {
65
65
  allow_origin_regex?: string | undefined;
66
+ expose_headers?: string[] | undefined;
66
67
  allow_origins?: string[] | undefined;
67
68
  allow_methods?: string[] | undefined;
68
69
  allow_headers?: string[] | undefined;
69
70
  allow_credentials?: boolean | undefined;
70
- expose_headers?: string[] | undefined;
71
71
  max_age?: number | undefined;
72
72
  } | undefined;
73
73
  app?: string | undefined;
74
74
  }, {
75
75
  cors?: {
76
76
  allow_origin_regex?: string | undefined;
77
+ expose_headers?: string[] | undefined;
77
78
  allow_origins?: string[] | undefined;
78
79
  allow_methods?: string[] | undefined;
79
80
  allow_headers?: string[] | undefined;
80
81
  allow_credentials?: boolean | undefined;
81
- expose_headers?: string[] | undefined;
82
82
  max_age?: number | undefined;
83
83
  } | undefined;
84
84
  app?: string | undefined;
@@ -110,11 +110,11 @@ export declare const StartServerSchema: z.ZodObject<{
110
110
  disable_meta: boolean;
111
111
  cors?: {
112
112
  allow_origin_regex?: string | undefined;
113
+ expose_headers?: string[] | undefined;
113
114
  allow_origins?: string[] | undefined;
114
115
  allow_methods?: string[] | undefined;
115
116
  allow_headers?: string[] | undefined;
116
117
  allow_credentials?: boolean | undefined;
117
- expose_headers?: string[] | undefined;
118
118
  max_age?: number | undefined;
119
119
  } | undefined;
120
120
  app?: string | undefined;
@@ -136,11 +136,11 @@ export declare const StartServerSchema: z.ZodObject<{
136
136
  http?: {
137
137
  cors?: {
138
138
  allow_origin_regex?: string | undefined;
139
+ expose_headers?: string[] | undefined;
139
140
  allow_origins?: string[] | undefined;
140
141
  allow_methods?: string[] | undefined;
141
142
  allow_headers?: string[] | undefined;
142
143
  allow_credentials?: boolean | undefined;
143
- expose_headers?: string[] | undefined;
144
144
  max_age?: number | undefined;
145
145
  } | undefined;
146
146
  app?: string | undefined;
@@ -46,6 +46,7 @@ export interface RunKwargs {
46
46
  interrupt_after?: "*" | string[] | undefined;
47
47
  config?: RunnableConfig;
48
48
  subgraphs?: boolean;
49
+ resumable?: boolean;
49
50
  temporary?: boolean;
50
51
  webhook?: unknown;
51
52
  feedback_keys?: string[] | undefined;
@@ -75,13 +76,19 @@ interface Message {
75
76
  data: unknown;
76
77
  }
77
78
  declare class Queue {
78
- private buffer;
79
+ private log;
79
80
  private listeners;
81
+ private nextId;
82
+ private resumable;
83
+ constructor(options: {
84
+ resumable: boolean;
85
+ });
80
86
  push(item: Message): void;
81
87
  get(options: {
82
88
  timeout: number;
89
+ lastEventId?: string;
83
90
  signal?: AbortSignal;
84
- }): Promise<Message>;
91
+ }): Promise<[id: string, message: Message]>;
85
92
  }
86
93
  declare class CancellationAbortController extends AbortController {
87
94
  abort(reason: "rollback" | "interrupt"): void;
@@ -91,10 +98,8 @@ declare class StreamManagerImpl {
91
98
  control: Record<string, CancellationAbortController>;
92
99
  getQueue(runId: string, options: {
93
100
  ifNotFound: "create";
101
+ resumable: boolean;
94
102
  }): Queue;
95
- getQueue(runId: string, options: {
96
- ifNotFound: "ignore";
97
- }): Queue | undefined;
98
103
  getControl(runId: string): CancellationAbortController | undefined;
99
104
  isLocked(runId: string): boolean;
100
105
  lock(runId: string): AbortSignal;
@@ -272,11 +277,18 @@ export declare class Runs {
272
277
  join(runId: string, threadId: string | undefined, options: {
273
278
  ignore404?: boolean;
274
279
  cancelOnDisconnect?: AbortSignal;
280
+ lastEventId: string | undefined;
275
281
  }, auth: AuthContext | undefined): AsyncGenerator<{
282
+ id?: string;
276
283
  event: string;
277
284
  data: unknown;
278
285
  }>;
279
- publish(runId: string, topic: string, data: unknown): Promise<void>;
286
+ publish(payload: {
287
+ runId: string;
288
+ event: string;
289
+ data: unknown;
290
+ resumable: boolean;
291
+ }): Promise<void>;
280
292
  };
281
293
  }
282
294
  export declare class Crons {
@@ -20,28 +20,58 @@ class TimeoutError extends Error {
20
20
  class AbortError extends Error {
21
21
  }
22
22
  class Queue {
23
- buffer = [];
23
+ log = [];
24
24
  listeners = [];
25
+ nextId = 0;
26
+ resumable = false;
27
+ constructor(options) {
28
+ this.resumable = options.resumable;
29
+ }
25
30
  push(item) {
26
- this.buffer.push(item);
27
- for (const listener of this.listeners) {
28
- listener();
29
- }
31
+ this.log.push(item);
32
+ for (const listener of this.listeners)
33
+ listener(this.nextId);
34
+ this.nextId += 1;
30
35
  }
31
36
  async get(options) {
32
- if (this.buffer.length > 0) {
33
- return this.buffer.shift();
37
+ if (this.resumable) {
38
+ const lastEventId = options.lastEventId;
39
+ // Generator stores internal state of the read head index,
40
+ let targetId = lastEventId != null ? +lastEventId + 1 : null;
41
+ if (targetId == null ||
42
+ isNaN(targetId) ||
43
+ targetId < 0 ||
44
+ targetId >= this.log.length) {
45
+ targetId = null;
46
+ }
47
+ if (targetId != null)
48
+ return [String(targetId), this.log[targetId]];
49
+ }
50
+ else {
51
+ if (this.log.length) {
52
+ const nextId = this.nextId - this.log.length;
53
+ const nextItem = this.log.shift();
54
+ return [String(nextId), nextItem];
55
+ }
34
56
  }
35
57
  let timeout = undefined;
36
58
  let resolver = undefined;
37
59
  const clean = new AbortController();
60
+ // listen to new item
38
61
  return await new Promise((resolve, reject) => {
39
62
  timeout = setTimeout(() => reject(new TimeoutError()), options.timeout);
40
63
  resolver = resolve;
41
64
  options.signal?.addEventListener("abort", () => reject(new AbortError()), { signal: clean.signal });
42
65
  this.listeners.push(resolver);
43
66
  })
44
- .then(() => this.buffer.shift())
67
+ .then((idx) => {
68
+ if (this.resumable) {
69
+ return [String(idx), this.log[idx]];
70
+ }
71
+ const nextId = this.nextId - this.log.length;
72
+ const nextItem = this.log.shift();
73
+ return [String(nextId), nextItem];
74
+ })
45
75
  .finally(() => {
46
76
  this.listeners = this.listeners.filter((l) => l !== resolver);
47
77
  clearTimeout(timeout);
@@ -59,12 +89,7 @@ class StreamManagerImpl {
59
89
  control = {};
60
90
  getQueue(runId, options) {
61
91
  if (this.readers[runId] == null) {
62
- if (options?.ifNotFound === "create") {
63
- this.readers[runId] = new Queue();
64
- }
65
- else {
66
- return undefined;
67
- }
92
+ this.readers[runId] = new Queue(options);
68
93
  }
69
94
  return this.readers[runId];
70
95
  }
@@ -883,7 +908,7 @@ export class Runs {
883
908
  });
884
909
  }
885
910
  static async wait(runId, threadId, auth) {
886
- const runStream = Runs.Stream.join(runId, threadId, { ignore404: threadId == null }, auth);
911
+ const runStream = Runs.Stream.join(runId, threadId, { ignore404: threadId == null, lastEventId: undefined }, auth);
887
912
  const lastChunk = new Promise(async (resolve, reject) => {
888
913
  try {
889
914
  let lastChunk = null;
@@ -1012,7 +1037,10 @@ export class Runs {
1012
1037
  yield* conn.withGenerator(async function* (STORE) {
1013
1038
  // TODO: what if we're joining an already completed run? Should we check before?
1014
1039
  const signal = options?.cancelOnDisconnect;
1015
- const queue = StreamManager.getQueue(runId, { ifNotFound: "create" });
1040
+ const queue = StreamManager.getQueue(runId, {
1041
+ ifNotFound: "create",
1042
+ resumable: options.lastEventId != null,
1043
+ });
1016
1044
  const [filters] = await handleAuthEvent(auth, "threads:read", {
1017
1045
  thread_id: threadId,
1018
1046
  });
@@ -1027,16 +1055,22 @@ export class Runs {
1027
1055
  return;
1028
1056
  }
1029
1057
  }
1058
+ let lastEventId = options?.lastEventId;
1030
1059
  while (!signal?.aborted) {
1031
1060
  try {
1032
- const message = await queue.get({ timeout: 500, signal });
1061
+ const [id, message] = await queue.get({
1062
+ timeout: 500,
1063
+ signal,
1064
+ lastEventId,
1065
+ });
1066
+ lastEventId = id;
1033
1067
  if (message.topic === `run:${runId}:control`) {
1034
1068
  if (message.data === "done")
1035
1069
  break;
1036
1070
  }
1037
1071
  else {
1038
1072
  const streamTopic = message.topic.substring(`run:${runId}:stream:`.length);
1039
- yield { event: streamTopic, data: message.data };
1073
+ yield { id, event: streamTopic, data: message.data };
1040
1074
  }
1041
1075
  }
1042
1076
  catch (error) {
@@ -1058,9 +1092,15 @@ export class Runs {
1058
1092
  }
1059
1093
  });
1060
1094
  }
1061
- static async publish(runId, topic, data) {
1062
- const queue = StreamManager.getQueue(runId, { ifNotFound: "create" });
1063
- queue.push({ topic: `run:${runId}:stream:${topic}`, data });
1095
+ static async publish(payload) {
1096
+ const queue = StreamManager.getQueue(payload.runId, {
1097
+ ifNotFound: "create",
1098
+ resumable: payload.resumable,
1099
+ });
1100
+ queue.push({
1101
+ topic: `run:${payload.runId}:stream:${payload.event}`,
1102
+ data: payload.data,
1103
+ });
1064
1104
  }
1065
1105
  };
1066
1106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/langgraph-api",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": "^18.19.0 || >=20.16.0"
@@ -53,7 +53,7 @@
53
53
  "winston": "^3.17.0",
54
54
  "winston-console-format": "^1.0.8",
55
55
  "zod": "^3.23.8",
56
- "@langchain/langgraph-ui": "0.0.34"
56
+ "@langchain/langgraph-ui": "0.0.36"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@langchain/core": "^0.3.42",
@@ -69,7 +69,7 @@
69
69
  },
70
70
  "devDependencies": {
71
71
  "typescript": "^5.5.4",
72
- "@langchain/langgraph-sdk": "^0.0.70",
72
+ "@langchain/langgraph-sdk": "^0.0.77",
73
73
  "@types/babel__code-frame": "^7.0.6",
74
74
  "@types/node": "^22.2.0",
75
75
  "@types/react": "^19.0.8",