@langchain/langgraph-api 1.3.1 → 1.4.1

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,35 @@
1
+ import type { StartServerOptions } from "../server.mjs";
2
+ /** Default loader: tsx CLI with built-in watch. */
3
+ export declare const DEFAULT_NODE_LOADER = "tsx";
4
+ type LoaderRegistration = {
5
+ specifier: string;
6
+ flag: "--loader" | "--import";
7
+ };
8
+ /**
9
+ * Shorthand loader names mapped to Node registration hooks.
10
+ * Use `"tsx"` (not `"tsx/esm"`) to spawn via the tsx CLI instead.
11
+ *
12
+ * `ts-node` uses `--loader` (not `--import`) because `ts-node/esm` is a
13
+ * custom ESM loader hook. Node 20+ deprecates `--loader` in favor of
14
+ * `module.register()`, but ts-node still documents this entrypoint.
15
+ */
16
+ export declare const LOADER_REGISTRATIONS: Record<string, LoaderRegistration>;
17
+ export declare function resolveNodeLoader(configLoader: string | undefined, env?: NodeJS.ProcessEnv): string;
18
+ export declare function resolveLoaderRegistration(loader: string, resolve: (specifier: string) => string): LoaderRegistration & {
19
+ path: string;
20
+ };
21
+ export declare function resolveLoaderPath(specifier: string, loader: string, resolve: (specifier: string) => string): string;
22
+ /** Convert a resolved filesystem path to a Node `--loader`/`--import` URL. */
23
+ export declare function toNodeModuleUrl(path: string): string;
24
+ export declare function usesTsxCli(loader: string): boolean;
25
+ export declare function buildSpawnArgs(options: {
26
+ nodeLoader: string;
27
+ reload: boolean;
28
+ pid: number;
29
+ payload: StartServerOptions;
30
+ resolve: (specifier: string) => string;
31
+ }): {
32
+ command: string;
33
+ args: string[];
34
+ };
35
+ export {};
@@ -0,0 +1,85 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { fileURLToPath, pathToFileURL } from "node:url";
3
+ /** Default loader: tsx CLI with built-in watch. */
4
+ export const DEFAULT_NODE_LOADER = "tsx";
5
+ /**
6
+ * Shorthand loader names mapped to Node registration hooks.
7
+ * Use `"tsx"` (not `"tsx/esm"`) to spawn via the tsx CLI instead.
8
+ *
9
+ * `ts-node` uses `--loader` (not `--import`) because `ts-node/esm` is a
10
+ * custom ESM loader hook. Node 20+ deprecates `--loader` in favor of
11
+ * `module.register()`, but ts-node still documents this entrypoint.
12
+ */
13
+ export const LOADER_REGISTRATIONS = {
14
+ "ts-node": { specifier: "ts-node/esm", flag: "--loader" },
15
+ "ts-node/esm": { specifier: "ts-node/esm", flag: "--loader" },
16
+ };
17
+ export function resolveNodeLoader(configLoader, env = process.env) {
18
+ const envLoader = env.LANGGRAPH_NODE_LOADER?.trim();
19
+ if (envLoader)
20
+ return envLoader;
21
+ return configLoader ?? DEFAULT_NODE_LOADER;
22
+ }
23
+ export function resolveLoaderRegistration(loader, resolve) {
24
+ const registration = LOADER_REGISTRATIONS[loader] ?? {
25
+ specifier: loader,
26
+ flag: "--import",
27
+ };
28
+ const path = resolveLoaderPath(registration.specifier, loader, resolve);
29
+ return { ...registration, path };
30
+ }
31
+ export function resolveLoaderPath(specifier, loader, resolve) {
32
+ if (specifier.startsWith("file://")) {
33
+ return fileURLToPath(specifier);
34
+ }
35
+ if (isAbsolute(specifier)) {
36
+ return specifier;
37
+ }
38
+ try {
39
+ return fileURLToPath(new URL(resolve(specifier)));
40
+ }
41
+ catch {
42
+ const hint = loader === "ts-node" || loader.startsWith("ts-node/")
43
+ ? ' Install "ts-node" and enable "emitDecoratorMetadata" in tsconfig when using reflect-metadata.'
44
+ : "";
45
+ throw new Error(`node_loader "${loader}" could not be resolved.${hint} Install the package or provide a valid module specifier, file path, or file:// URL.`);
46
+ }
47
+ }
48
+ /** Convert a resolved filesystem path to a Node `--loader`/`--import` URL. */
49
+ export function toNodeModuleUrl(path) {
50
+ if (path.startsWith("file://"))
51
+ return path;
52
+ return pathToFileURL(path).href;
53
+ }
54
+ export function usesTsxCli(loader) {
55
+ return loader === DEFAULT_NODE_LOADER;
56
+ }
57
+ export function buildSpawnArgs(options) {
58
+ const payloadArg = JSON.stringify(options.payload);
59
+ const sharedArgs = [options.pid.toString(), payloadArg];
60
+ const preloadUrl = new URL(options.resolve("../preload.mjs")).toString();
61
+ const entrypointPath = fileURLToPath(new URL(options.resolve("./entrypoint.mjs")));
62
+ if (usesTsxCli(options.nodeLoader)) {
63
+ const args = [
64
+ fileURLToPath(new URL("../../cli.mjs", options.resolve("tsx/esm/api"))),
65
+ ...(options.reload ? ["watch"] : []),
66
+ "--clear-screen=false",
67
+ "--import",
68
+ preloadUrl,
69
+ entrypointPath,
70
+ ...sharedArgs,
71
+ ];
72
+ return { command: process.execPath, args };
73
+ }
74
+ const loader = resolveLoaderRegistration(options.nodeLoader, options.resolve);
75
+ const args = [
76
+ ...(options.reload ? ["--watch"] : []),
77
+ loader.flag,
78
+ toNodeModuleUrl(loader.path),
79
+ "--import",
80
+ preloadUrl,
81
+ entrypointPath,
82
+ ...sharedArgs,
83
+ ];
84
+ return { command: process.execPath, args };
85
+ }
@@ -1,7 +1,9 @@
1
+ import { type StartServerOptions } from "../server.mjs";
1
2
  export declare function spawnServer(args: {
2
3
  host: string;
3
4
  port: string;
4
5
  nJobsPerWorker: string;
6
+ reload?: boolean;
5
7
  }, context: {
6
8
  config: {
7
9
  graphs: Record<string, string | {
@@ -16,23 +18,8 @@ export declare function spawnServer(args: {
16
18
  path?: string;
17
19
  disable_studio_auth?: boolean;
18
20
  };
19
- http?: {
20
- app?: string;
21
- disable_assistants?: boolean;
22
- disable_threads?: boolean;
23
- disable_runs?: boolean;
24
- disable_store?: boolean;
25
- disable_meta?: boolean;
26
- cors?: {
27
- allow_origins?: string[];
28
- allow_methods?: string[];
29
- allow_headers?: string[];
30
- allow_credentials?: boolean;
31
- allow_origin_regex?: string;
32
- expose_headers?: string[];
33
- max_age?: number;
34
- };
35
- };
21
+ node_loader?: string;
22
+ http?: StartServerOptions["http"];
36
23
  };
37
24
  env: NodeJS.ProcessEnv;
38
25
  hostUrl: string;
@@ -1,5 +1,6 @@
1
- import { fileURLToPath } from "node:url";
2
1
  import { spawn } from "node:child_process";
2
+ import { StartServerSchema } from "../server.mjs";
3
+ import { buildSpawnArgs, resolveNodeLoader } from "./spawn-args.mjs";
3
4
  export async function spawnServer(args, context, options) {
4
5
  const localUrl = `http://${args.host}:${args.port}`;
5
6
  const studioUrl = `${context.hostUrl}/studio?baseUrl=${localUrl}`;
@@ -17,26 +18,26 @@ This in-memory server is designed for development and testing.
17
18
  For production use, please use LangSmith Deployment.
18
19
 
19
20
  `);
20
- return spawn(process.execPath, [
21
- fileURLToPath(new URL("../../cli.mjs", import.meta.resolve("tsx/esm/api"))),
22
- "watch",
23
- "--clear-screen=false",
24
- "--import",
25
- new URL(import.meta.resolve("../preload.mjs")).toString(),
26
- fileURLToPath(new URL(import.meta.resolve("./entrypoint.mjs"))),
27
- options.pid.toString(),
28
- JSON.stringify({
29
- port: Number.parseInt(args.port, 10),
30
- nWorkers: Number.parseInt(args.nJobsPerWorker, 10),
31
- host: args.host,
32
- graphs: context.config.graphs,
33
- auth: context.config.auth,
34
- ui: context.config.ui,
35
- ui_config: context.config.ui_config,
36
- cwd: options.projectCwd,
37
- http: context.config.http,
38
- }),
39
- ], {
21
+ const nodeLoader = resolveNodeLoader(context.config.node_loader, context.env);
22
+ const payload = StartServerSchema.parse({
23
+ port: Number.parseInt(args.port, 10),
24
+ nWorkers: Number.parseInt(args.nJobsPerWorker, 10),
25
+ host: args.host,
26
+ graphs: context.config.graphs,
27
+ auth: context.config.auth,
28
+ ui: context.config.ui,
29
+ ui_config: context.config.ui_config,
30
+ cwd: options.projectCwd,
31
+ http: context.config.http,
32
+ });
33
+ const { command, args: spawnArgs } = buildSpawnArgs({
34
+ nodeLoader,
35
+ reload: args.reload ?? true,
36
+ pid: options.pid,
37
+ payload,
38
+ resolve: (specifier) => import.meta.resolve(specifier),
39
+ });
40
+ return spawn(command, spawnArgs, {
40
41
  stdio: ["inherit", "inherit", "inherit", "ipc"],
41
42
  env: {
42
43
  ...context.env,
@@ -298,11 +298,31 @@ export function registerProtocolRoutes(api, context, upgradeWebSocket) {
298
298
  message: "Thread has no active assistant; call run.start first.",
299
299
  });
300
300
  }
301
+ // `update` / `goto` are the optional state mutation and directed jump
302
+ // the SDK's `respond({ update, goto })` forwards on `input.respond`
303
+ // (`InputRespondOne` / `InputRespondMany`). Fold them into the same
304
+ // `Command(resume, update, goto)` as the resume so the resumed run
305
+ // produces a single checkpoint reflecting all of them — the HITL
306
+ // "push card into state + resume" flow. Read leniently (update: object
307
+ // or `[key, value][]` tuples; goto: node name, Send object, or a list
308
+ // of either), same posture as `config` / `metadata`.
309
+ const update = isRecord(params.update) || Array.isArray(params.update)
310
+ ? params.update
311
+ : undefined;
312
+ const goto = typeof params.goto === "string" ||
313
+ Array.isArray(params.goto) ||
314
+ isRecord(params.goto)
315
+ ? params.goto
316
+ : undefined;
301
317
  const run = createStubRun(thread.threadId, {
302
318
  assistant_id: assistantId,
303
319
  on_disconnect: "cancel",
304
320
  input: null,
305
- command: { resume: resumeInput },
321
+ command: {
322
+ resume: resumeInput,
323
+ ...(update != null ? { update } : {}),
324
+ ...(goto != null ? { goto } : {}),
325
+ },
306
326
  // Carry the SDK's `respond({ config, metadata })` onto the resumed
307
327
  // run so it applies the same run config / metadata a fresh
308
328
  // `run.start` would (both are part of the `input.respond` params).
@@ -267,9 +267,26 @@ export class ProtocolService {
267
267
  // context, …) and metadata (trigger source, test flags, …) a fresh
268
268
  // `run.start` would. Read them leniently — same posture as
269
269
  // `normalizeRunStart`.
270
+ // `update` / `goto` are the optional state mutation and directed jump
271
+ // applied in the same superstep as the resume (`InputRespondOne` /
272
+ // `InputRespondMany`). The SDK's `respond({ update, goto })` forwards them
273
+ // on the `input.respond` command; they are folded into
274
+ // `Command(resume, update, goto)` by `createOrResumeRun`. Read leniently
275
+ // (update: object or [key, value][] tuples; goto: node name, Send object,
276
+ // or a list of either) — same posture as `config` / `metadata`.
277
+ const update = isRecord(rawParams.update) || Array.isArray(rawParams.update)
278
+ ? rawParams.update
279
+ : undefined;
280
+ const goto = typeof rawParams.goto === "string" ||
281
+ Array.isArray(rawParams.goto) ||
282
+ isRecord(rawParams.goto)
283
+ ? rawParams.goto
284
+ : undefined;
270
285
  await this.createOrResumeRun(record, {
271
286
  assistant_id: record.assistantId,
272
287
  input: resumeInput,
288
+ update,
289
+ goto,
273
290
  config: isRecord(rawParams.config) ? rawParams.config : undefined,
274
291
  metadata: isRecord(rawParams.metadata) ? rawParams.metadata : undefined,
275
292
  });
@@ -313,7 +330,16 @@ export class ProtocolService {
313
330
  assistant_id: assistantId,
314
331
  input: isResume ? null : params.input,
315
332
  command: isResume
316
- ? { resume: params.input }
333
+ ? {
334
+ resume: params.input,
335
+ // Apply an optional state update and/or directed jump in the
336
+ // same superstep as the resume (HITL "push card into state +
337
+ // resume" flows, or resume-and-redirect). Mapped straight onto
338
+ // LangGraph's `Command(resume, update, goto)`, so the resumed
339
+ // run produces a single checkpoint reflecting all of them.
340
+ ...(params.update != null ? { update: params.update } : {}),
341
+ ...(params.goto != null ? { goto: params.goto } : {}),
342
+ }
317
343
  : undefined,
318
344
  config: runConfig,
319
345
  metadata: params.metadata,
@@ -357,6 +357,15 @@ export const normalizeProtocolStateMessage = (value) => {
357
357
  if (typeof value.name === "string") {
358
358
  message.name = value.name;
359
359
  }
360
+ // Preserve `response_metadata` when present. It is a first-class protocol
361
+ // message field, and HITL flows rely on it: an interrupt's card is carried
362
+ // on `AIMessage.response_metadata` (e.g. `{ cards: ... }`) and the frontend
363
+ // pushes that message into state via `respond(decision, { update })`. We
364
+ // only forward a non-empty record to keep the common (empty) case minimal.
365
+ if (isRecord(value.response_metadata) &&
366
+ Object.keys(value.response_metadata).length > 0) {
367
+ message.response_metadata = value.response_metadata;
368
+ }
360
369
  if ((type === "ai" || type === "human") &&
361
370
  typeof value.example === "boolean") {
362
371
  message.example = value.example;
package/dist/server.d.mts CHANGED
@@ -167,7 +167,8 @@ export declare const StartServerSchema: z.ZodObject<{
167
167
  disable_meta?: boolean | undefined;
168
168
  } | undefined;
169
169
  }>;
170
- export declare function startServer(options: z.infer<typeof StartServerSchema>, storage?: {
170
+ export type StartServerOptions = z.infer<typeof StartServerSchema>;
171
+ export declare function startServer(options: StartServerOptions, storage?: {
171
172
  ops?: Ops;
172
173
  }): Promise<{
173
174
  host: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/langgraph-api",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": "^18.19.0 || >=20.16.0"
@@ -55,13 +55,13 @@
55
55
  "@hono/node-server": "^1.19.13",
56
56
  "@hono/node-ws": "^1.3.0",
57
57
  "@hono/zod-validator": "^0.7.6",
58
- "@langchain/protocol": "^0.0.16",
58
+ "@langchain/protocol": "^0.0.18",
59
59
  "@types/json-schema": "^7.0.15",
60
60
  "@typescript/vfs": "^1.6.0",
61
61
  "dedent": "^1.5.3",
62
62
  "dotenv": "^16.4.7",
63
63
  "exit-hook": "^4.0.0",
64
- "hono": "^4.12.21",
64
+ "hono": "^4.12.25",
65
65
  "langsmith": ">=0.5.19 <1.0.0",
66
66
  "open": "^10.1.0",
67
67
  "semver": "^7.7.1",
@@ -71,37 +71,42 @@
71
71
  "winston": "^3.17.0",
72
72
  "winston-console-format": "^1.0.8",
73
73
  "zod": "^3.25.76 || ^4",
74
- "@langchain/langgraph-ui": "1.3.1"
74
+ "@langchain/langgraph-ui": "1.4.1"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "@langchain/core": "^1.1.48",
78
78
  "@langchain/langgraph": "^1.3.6",
79
79
  "@langchain/langgraph-checkpoint": "~0.0.16 || ^0.1.0 || ^1.0.0",
80
80
  "@langchain/langgraph-sdk": "^1.9.3-rc.0",
81
+ "ts-node": "^10.9.2",
81
82
  "typescript": "^5.5.4"
82
83
  },
83
84
  "peerDependenciesMeta": {
84
85
  "@langchain/langgraph-sdk": {
85
86
  "optional": true
87
+ },
88
+ "ts-node": {
89
+ "optional": true
86
90
  }
87
91
  },
88
92
  "devDependencies": {
89
- "@langchain/core": "^1.1.48",
93
+ "@langchain/core": "^1.2.1",
90
94
  "@types/babel__code-frame": "^7.0.6",
91
95
  "@types/node": "^18.15.11",
92
96
  "@types/react": "^19.2.16",
93
97
  "@types/react-dom": "^19.0.3",
94
98
  "@types/semver": "^7.7.0",
95
- "deepagents": "^1.9.0",
99
+ "deepagents": "^1.10.5",
96
100
  "jose": "^6.0.10",
97
- "langchain": "^1.4.4",
101
+ "langchain": "^1.5.1",
98
102
  "postgres": "^3.4.5",
103
+ "ts-node": "^10.9.2",
99
104
  "typescript": "^4.9.5 || ^5.4.5",
100
105
  "vitest": "^4.1.0",
101
106
  "wait-port": "^1.1.0",
102
- "@langchain/langgraph": "1.4.2",
103
- "@langchain/langgraph-checkpoint": "1.1.1",
104
- "@langchain/langgraph-sdk": "1.9.22"
107
+ "@langchain/langgraph": "1.4.6",
108
+ "@langchain/langgraph-checkpoint": "1.1.3",
109
+ "@langchain/langgraph-sdk": "1.9.25"
105
110
  },
106
111
  "scripts": {
107
112
  "clean": "rm -rf dist/ .turbo/ ./tests/graphs/.langgraph_api/ ./tests/protocol-v2/graphs/.langgraph_api/",