@langchain/langgraph-api 0.0.21 → 0.0.23

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.
@@ -0,0 +1,5 @@
1
+ import { Hono } from "hono";
2
+ const api = new Hono();
3
+ api.get("/info", (c) => c.json({ flags: { assistants: true, crons: false } }));
4
+ api.get("/ok", (c) => c.json({ ok: true }));
5
+ export default api;
@@ -13,6 +13,23 @@ export declare function spawnServer(args: {
13
13
  path?: string;
14
14
  disable_studio_auth?: boolean;
15
15
  };
16
+ http?: {
17
+ app?: string;
18
+ disable_assistants?: boolean;
19
+ disable_threads?: boolean;
20
+ disable_runs?: boolean;
21
+ disable_store?: boolean;
22
+ disable_meta?: boolean;
23
+ cors?: {
24
+ allow_origins?: string[];
25
+ allow_methods?: string[];
26
+ allow_headers?: string[];
27
+ allow_credentials?: boolean;
28
+ allow_origin_regex?: string;
29
+ expose_headers?: string[];
30
+ max_age?: number;
31
+ };
32
+ };
16
33
  };
17
34
  env: NodeJS.ProcessEnv;
18
35
  hostUrl: string;
@@ -34,6 +34,7 @@ For production use, please use LangGraph Cloud.
34
34
  ui: context.config.ui,
35
35
  ui_config: context.config.ui_config,
36
36
  cwd: options.projectCwd,
37
+ http: context.config.http,
37
38
  }),
38
39
  ], {
39
40
  stdio: ["inherit", "inherit", "inherit", "ipc"],
@@ -0,0 +1,10 @@
1
+ import * as path from "node:path";
2
+ import * as url from "node:url";
3
+ export async function registerHttp(appPath, options) {
4
+ const [userFile, exportSymbol] = appPath.split(":", 2);
5
+ const sourceFile = path.resolve(options.cwd, userFile);
6
+ const user = (await import(url.pathToFileURL(sourceFile).toString()).then((module) => module[exportSymbol || "default"]));
7
+ if (!user)
8
+ throw new Error(`Failed to load HTTP app: ${appPath}`);
9
+ return { api: user };
10
+ }
@@ -0,0 +1,38 @@
1
+ import { cors as honoCors } from "hono/cors";
2
+ export const cors = (cors) => {
3
+ if (cors == null)
4
+ return honoCors();
5
+ const originRegex = cors.allow_origin_regex
6
+ ? new RegExp(cors.allow_origin_regex)
7
+ : undefined;
8
+ const origin = originRegex
9
+ ? (origin) => {
10
+ originRegex.lastIndex = 0; // reset regex in case it's a global regex
11
+ if (originRegex.test(origin))
12
+ return origin;
13
+ return undefined;
14
+ }
15
+ : (cors.allow_origins ?? []);
16
+ // TODO: handle `cors.allow_credentials`
17
+ return honoCors({
18
+ origin,
19
+ maxAge: cors.max_age,
20
+ allowMethods: cors.allow_methods,
21
+ allowHeaders: cors.allow_headers,
22
+ credentials: cors.allow_credentials,
23
+ exposeHeaders: cors.expose_headers,
24
+ });
25
+ };
26
+ // This is used to match the behavior of the original LangGraph API
27
+ // where the content-type is not being validated. Might be nice
28
+ // to warn about this in the future and throw an error instead.
29
+ export const ensureContentType = () => {
30
+ return async (c, next) => {
31
+ if (c.req.header("content-type")?.startsWith("text/plain") &&
32
+ c.req.method !== "GET" &&
33
+ c.req.method !== "OPTIONS") {
34
+ c.req.raw.headers.set("content-type", "application/json");
35
+ }
36
+ await next();
37
+ };
38
+ };
@@ -0,0 +1,10 @@
1
+ const fetchSmb = Symbol.for("langgraph_api:fetch");
2
+ const global = globalThis;
3
+ export function getLoopbackFetch() {
4
+ if (!global[fetchSmb])
5
+ throw new Error("Loopback fetch is not bound");
6
+ return global[fetchSmb];
7
+ }
8
+ export const bindLoopbackFetch = (app) => {
9
+ global[fetchSmb] = async (url, init) => app.request(url, init);
10
+ };
package/dist/queue.mjs CHANGED
@@ -2,6 +2,7 @@ import { Runs, Threads } from "./storage/ops.mjs";
2
2
  import { streamState, } from "./stream.mjs";
3
3
  import { logError, logger } from "./logging.mjs";
4
4
  import { serializeError } from "./utils/serde.mjs";
5
+ import { callWebhook } from "./webhook.mjs";
5
6
  const MAX_RETRY_ATTEMPTS = 3;
6
7
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7
8
  export const queue = async () => {
@@ -15,9 +16,12 @@ export const queue = async () => {
15
16
  };
16
17
  const worker = async (run, attempt, abortSignal) => {
17
18
  const startedAt = new Date();
19
+ let endedAt = undefined;
18
20
  let checkpoint = undefined;
19
21
  let exception = undefined;
22
+ let status = undefined;
20
23
  const temporary = run.kwargs.temporary;
24
+ const webhook = run.kwargs.webhook;
21
25
  logger.info("Starting background run", {
22
26
  run_id: run.run_id,
23
27
  run_attempt: attempt,
@@ -54,7 +58,7 @@ const worker = async (run, attempt, abortSignal) => {
54
58
  await Runs.Stream.publish(run.run_id, "error", serializeError(error));
55
59
  throw error;
56
60
  }
57
- const endedAt = new Date();
61
+ endedAt = new Date();
58
62
  logger.info("Background run succeeded", {
59
63
  run_id: run.run_id,
60
64
  run_attempt: attempt,
@@ -63,10 +67,11 @@ const worker = async (run, attempt, abortSignal) => {
63
67
  run_ended_at: endedAt,
64
68
  run_exec_ms: endedAt.valueOf() - startedAt.valueOf(),
65
69
  });
66
- await Runs.setStatus(run.run_id, "success");
70
+ status = "success";
71
+ await Runs.setStatus(run.run_id, status);
67
72
  }
68
73
  catch (error) {
69
- const endedAt = new Date();
74
+ endedAt = new Date();
70
75
  if (error instanceof Error)
71
76
  exception = error;
72
77
  logError(error, {
@@ -80,6 +85,7 @@ const worker = async (run, attempt, abortSignal) => {
80
85
  run_exec_ms: endedAt.valueOf() - startedAt.valueOf(),
81
86
  },
82
87
  });
88
+ status = "error";
83
89
  await Runs.setStatus(run.run_id, "error");
84
90
  }
85
91
  finally {
@@ -89,5 +95,16 @@ const worker = async (run, attempt, abortSignal) => {
89
95
  else {
90
96
  await Threads.setStatus(run.thread_id, { checkpoint, exception });
91
97
  }
98
+ if (webhook) {
99
+ await callWebhook({
100
+ checkpoint,
101
+ status,
102
+ exception,
103
+ run,
104
+ webhook,
105
+ run_started_at: startedAt,
106
+ run_ended_at: endedAt,
107
+ });
108
+ }
92
109
  }
93
110
  };
package/dist/schemas.mjs CHANGED
@@ -89,7 +89,7 @@ export const CronCreate = z
89
89
  .describe("Metadata for the run.")
90
90
  .optional(),
91
91
  config: AssistantConfig.optional(),
92
- webhook: z.string().url().optional(),
92
+ webhook: z.string().optional(),
93
93
  interrupt_before: z.union([z.enum(["*"]), z.array(z.string())]).optional(),
94
94
  interrupt_after: z.union([z.enum(["*"]), z.array(z.string())]).optional(),
95
95
  multitask_strategy: z
@@ -172,7 +172,7 @@ export const RunCreate = z
172
172
  .describe("Metadata for the run.")
173
173
  .optional(),
174
174
  config: AssistantConfig.optional(),
175
- webhook: z.string().url().optional(),
175
+ webhook: z.string().optional(),
176
176
  interrupt_before: z.union([z.enum(["*"]), z.array(z.string())]).optional(),
177
177
  interrupt_after: z.union([z.enum(["*"]), z.array(z.string())]).optional(),
178
178
  on_disconnect: z
package/dist/server.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  import { serve } from "@hono/node-server";
2
2
  import { Hono } from "hono";
3
- import { cors } from "hono/cors";
4
3
  import { registerFromEnv } from "./graph/load.mjs";
5
4
  import runs from "./api/runs.mjs";
6
5
  import threads from "./api/threads.mjs";
7
6
  import assistants from "./api/assistants.mjs";
8
7
  import store from "./api/store.mjs";
8
+ import meta from "./api/meta.mjs";
9
9
  import { truncate, conn as opsConn } from "./storage/ops.mjs";
10
10
  import { zValidator } from "@hono/zod-validator";
11
11
  import { z } from "zod";
@@ -15,37 +15,9 @@ import { checkpointer } from "./storage/checkpoint.mjs";
15
15
  import { store as graphStore } from "./storage/store.mjs";
16
16
  import { auth } from "./auth/custom.mjs";
17
17
  import { registerAuth } from "./auth/index.mjs";
18
- const app = new Hono();
19
- // This is used to match the behavior of the original LangGraph API
20
- // where the content-type is not being validated. Might be nice
21
- // to warn about this in the future and throw an error instead.
22
- app.use(async (c, next) => {
23
- if (c.req.header("content-type")?.startsWith("text/plain") &&
24
- c.req.method !== "GET" &&
25
- c.req.method !== "OPTIONS") {
26
- c.req.raw.headers.set("content-type", "application/json");
27
- }
28
- await next();
29
- });
30
- app.use(cors());
31
- app.use(requestLogger());
32
- app.get("/info", (c) => c.json({ flags: { assistants: true, crons: false } }));
33
- app.post("/internal/truncate", zValidator("json", z.object({
34
- runs: z.boolean().optional(),
35
- threads: z.boolean().optional(),
36
- assistants: z.boolean().optional(),
37
- checkpointer: z.boolean().optional(),
38
- store: z.boolean().optional(),
39
- })), (c) => {
40
- const { runs, threads, assistants, checkpointer, store } = c.req.valid("json");
41
- truncate({ runs, threads, assistants, checkpointer, store });
42
- return c.json({ ok: true });
43
- });
44
- app.use(auth());
45
- app.route("/", assistants);
46
- app.route("/", runs);
47
- app.route("/", threads);
48
- app.route("/", store);
18
+ import { registerHttp } from "./http/custom.mjs";
19
+ import { cors, ensureContentType } from "./http/middleware.mjs";
20
+ import { bindLoopbackFetch } from "./loopback.mjs";
49
21
  export const StartServerSchema = z.object({
50
22
  port: z.number(),
51
23
  nWorkers: z.number(),
@@ -60,6 +32,27 @@ export const StartServerSchema = z.object({
60
32
  .optional(),
61
33
  ui: z.record(z.string()).optional(),
62
34
  ui_config: z.object({ shared: z.array(z.string()).optional() }).optional(),
35
+ http: z
36
+ .object({
37
+ app: z.string().optional(),
38
+ disable_assistants: z.boolean().default(false),
39
+ disable_threads: z.boolean().default(false),
40
+ disable_runs: z.boolean().default(false),
41
+ disable_store: z.boolean().default(false),
42
+ disable_meta: z.boolean().default(false),
43
+ cors: z
44
+ .object({
45
+ allow_origins: z.array(z.string()).optional(),
46
+ allow_methods: z.array(z.string()).optional(),
47
+ allow_headers: z.array(z.string()).optional(),
48
+ allow_credentials: z.boolean().optional(),
49
+ allow_origin_regex: z.string().optional(),
50
+ expose_headers: z.array(z.string()).optional(),
51
+ max_age: z.number().optional(),
52
+ })
53
+ .optional(),
54
+ })
55
+ .optional(),
63
56
  });
64
57
  export async function startServer(options) {
65
58
  logger.info(`Initializing storage...`);
@@ -74,19 +67,51 @@ export async function startServer(options) {
74
67
  };
75
68
  logger.info(`Registering graphs from ${options.cwd}`);
76
69
  await registerFromEnv(options.graphs, { cwd: options.cwd });
70
+ const app = new Hono();
71
+ // Loopback fetch used by webhooks and custom routes
72
+ bindLoopbackFetch(app);
77
73
  if (options.auth?.path) {
78
74
  logger.info(`Loading auth from ${options.auth.path}`);
79
75
  await registerAuth(options.auth, { cwd: options.cwd });
76
+ app.use(auth());
80
77
  }
81
- if (options.ui) {
82
- logger.info(`Loading UI`);
83
- const { api, registerGraphUi } = await import("./ui/load.mjs");
78
+ if (options.http?.app) {
79
+ logger.info(`Loading HTTP app from ${options.http.app}`);
80
+ const { api } = await registerHttp(options.http.app, { cwd: options.cwd });
84
81
  app.route("/", api);
82
+ }
83
+ app.use(cors(options.http?.cors));
84
+ app.use(requestLogger());
85
+ app.use(ensureContentType());
86
+ app.post("/internal/truncate", zValidator("json", z.object({
87
+ runs: z.boolean().optional(),
88
+ threads: z.boolean().optional(),
89
+ assistants: z.boolean().optional(),
90
+ checkpointer: z.boolean().optional(),
91
+ store: z.boolean().optional(),
92
+ })), (c) => {
93
+ const { runs, threads, assistants, checkpointer, store } = c.req.valid("json");
94
+ truncate({ runs, threads, assistants, checkpointer, store });
95
+ return c.json({ ok: true });
96
+ });
97
+ if (!options.http?.disable_meta)
98
+ app.route("/", meta);
99
+ if (!options.http?.disable_assistants)
100
+ app.route("/", assistants);
101
+ if (!options.http?.disable_runs)
102
+ app.route("/", runs);
103
+ if (!options.http?.disable_threads)
104
+ app.route("/", threads);
105
+ if (!options.http?.disable_store)
106
+ app.route("/", store);
107
+ if (options.ui) {
85
108
  logger.info(`Registering UI from ${options.cwd}`);
109
+ const { api, registerGraphUi } = await import("./ui/load.mjs");
86
110
  await registerGraphUi(options.ui, {
87
111
  cwd: options.cwd,
88
112
  config: options.ui_config,
89
113
  });
114
+ app.route("/", api);
90
115
  }
91
116
  logger.info(`Starting ${options.nWorkers} workers`);
92
117
  for (let i = 0; i < options.nWorkers; i++)
@@ -0,0 +1,30 @@
1
+ import { serializeError } from "./utils/serde.mjs";
2
+ import { getLoopbackFetch } from "./loopback.mjs";
3
+ export async function callWebhook(result) {
4
+ const payload = {
5
+ ...result.run,
6
+ status: result.status,
7
+ run_started_at: result.run_started_at.toISOString(),
8
+ run_ended_at: result.run_ended_at?.toISOString(),
9
+ webhook_sent_at: new Date().toISOString(),
10
+ values: result.checkpoint?.values,
11
+ ...(result.exception
12
+ ? { error: serializeError(result.exception).message }
13
+ : undefined),
14
+ };
15
+ if (result.webhook.startsWith("/")) {
16
+ const fetch = getLoopbackFetch();
17
+ if (!fetch)
18
+ throw new Error("Loopback fetch is not bound");
19
+ await fetch(result.webhook, {
20
+ method: "POST",
21
+ body: JSON.stringify(payload),
22
+ });
23
+ }
24
+ else {
25
+ await fetch(result.webhook, {
26
+ method: "POST",
27
+ body: JSON.stringify(payload),
28
+ });
29
+ }
30
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/langgraph-api",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": "^18.19.0 || >=20.16.0"
@@ -43,13 +43,13 @@
43
43
  "winston": "^3.17.0",
44
44
  "winston-console-format": "^1.0.8",
45
45
  "zod": "^3.23.8",
46
- "@langchain/langgraph-ui": "0.0.21"
46
+ "@langchain/langgraph-ui": "0.0.23"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@langchain/core": "^0.3.42",
50
50
  "@langchain/langgraph": "^0.2.57",
51
51
  "@langchain/langgraph-checkpoint": "^0.0.16",
52
- "@langchain/langgraph-sdk": "^0.0.66",
52
+ "@langchain/langgraph-sdk": "^0.0.67",
53
53
  "typescript": "^5.5.4"
54
54
  },
55
55
  "peerDependenciesMeta": {
@@ -58,7 +58,7 @@
58
58
  }
59
59
  },
60
60
  "devDependencies": {
61
- "@langchain/langgraph-sdk": "^0.0.66",
61
+ "@langchain/langgraph-sdk": "^0.0.67",
62
62
  "@types/babel__code-frame": "^7.0.6",
63
63
  "@types/react": "^19.0.8",
64
64
  "@types/react-dom": "^19.0.3",