@langchain/langgraph-api 1.2.4 → 1.3.0

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.
@@ -103,7 +103,11 @@ export declare class ProtocolService {
103
103
  }
104
104
  /**
105
105
  * Check whether a protocol event matches an SSE event sink filter.
106
- * Mirrors the subscription matching logic in {@link RunProtocolSession}.
106
+ *
107
+ * Shares its channel inference and namespace matching with
108
+ * {@link RunProtocolSession} (and the core streaming toolkit) so SSE and
109
+ * WebSocket subscribers apply identical semantics, including dynamic
110
+ * namespace-suffix normalization.
107
111
  */
108
112
  export declare function matchesSinkFilter(filter: EventSinkFilter, event: ProtocolEvent): boolean;
109
113
  export {};
@@ -1,6 +1,6 @@
1
+ import { inferChannel, isPrefixMatch } from "@langchain/langgraph/stream";
1
2
  import { v7 as uuid7 } from "uuid";
2
3
  import { getAssistantId } from "../graph/load.mjs";
3
- import { isSupportedChannel, isRecord as isRecordInternal, } from "./session/internal-types.mjs";
4
4
  import { PROTOCOL_STREAM_RUN_KEY } from "./constants.mjs";
5
5
  import { RunProtocolSession } from "./session/index.mjs";
6
6
  const DEFAULT_RUN_STREAM_MODES = [
@@ -564,35 +564,24 @@ export class ProtocolService {
564
564
  };
565
565
  }
566
566
  }
567
- function isPrefixMatch(namespace, prefix) {
568
- if (prefix.length > namespace.length)
569
- return false;
570
- return prefix.every((segment, i) => namespace[i] === segment);
571
- }
572
567
  /**
573
568
  * Check whether a protocol event matches an SSE event sink filter.
574
- * Mirrors the subscription matching logic in {@link RunProtocolSession}.
569
+ *
570
+ * Shares its channel inference and namespace matching with
571
+ * {@link RunProtocolSession} (and the core streaming toolkit) so SSE and
572
+ * WebSocket subscribers apply identical semantics, including dynamic
573
+ * namespace-suffix normalization.
575
574
  */
576
575
  export function matchesSinkFilter(filter, event) {
577
576
  if (filter.since != null && (event.seq ?? 0) <= filter.since)
578
577
  return false;
579
- const channel = event.method === "input.requested"
580
- ? "input"
581
- : isSupportedChannel(event.method)
582
- ? event.method
583
- : undefined;
578
+ // `inferChannel` resolves named custom channels to `custom:<name>`; a bare
579
+ // `custom` filter still matches via the prefix check below.
580
+ const channel = inferChannel(event);
584
581
  if (channel == null)
585
582
  return false;
586
- let channelMatched = filter.channels.has(channel);
587
- if (!channelMatched && channel === "custom") {
588
- const params = event.params;
589
- const eventName = isRecordInternal(params.data) && typeof params.data.name === "string"
590
- ? params.data.name
591
- : undefined;
592
- if (eventName != null) {
593
- channelMatched = filter.channels.has(`custom:${eventName}`);
594
- }
595
- }
583
+ const channelMatched = filter.channels.has(channel) ||
584
+ (channel.startsWith("custom:") && filter.channels.has("custom"));
596
585
  if (!channelMatched)
597
586
  return false;
598
587
  if (filter.namespaces == null || filter.namespaces.length === 0) {
@@ -1,7 +1,8 @@
1
+ import { inferChannel, isSupportedChannel } from "@langchain/langgraph/stream";
1
2
  import { v7 as uuid7 } from "uuid";
2
3
  import { serialiseAsDict, serializeError } from "../../utils/serde.mjs";
3
4
  import { normalizeInputRequestedData, normalizeToolData, stripInterruptsFromValues, toLifecycleStatus, } from "./event-normalizers.mjs";
4
- import { SUPPORTED_CHANNELS, isRecord, isSupportedChannel, } from "./internal-types.mjs";
5
+ import { isRecord } from "./internal-types.mjs";
5
6
  import { guessGraphName, isPrefixMatch, normalizeNamespace, parseEventName, toNamespaceKey, } from "./namespace.mjs";
6
7
  import { normalizeProtocolStatePayload } from "./state-normalizers.mjs";
7
8
  /**
@@ -305,15 +306,20 @@ export class RunProtocolSession {
305
306
  case "tools":
306
307
  await this.pushEvent(this.createEvent("tools", namespace, normalizeToolData(event.data, event.id ?? uuid7())));
307
308
  return;
308
- default:
309
- // Route unknown methods as named custom events. This allows
310
- // reducers to emit("a2a", data) and have it appear on the
311
- // custom channel with name set for client-side filtering.
309
+ default: {
310
+ // Route unknown methods as named custom events. Extension
311
+ // StreamChannels emit `custom:<name>` on the wire (matching
312
+ // Python's mux); strip the prefix so `custom:<name>`
313
+ // subscriptions match on `data.name === <name>`.
314
+ const channelName = method.startsWith("custom:")
315
+ ? method.slice("custom:".length)
316
+ : method;
312
317
  await this.pushEvent(this.createEvent("custom", namespace, {
313
- name: method,
318
+ name: channelName,
314
319
  payload: event.data,
315
320
  }));
316
321
  return;
322
+ }
317
323
  }
318
324
  }
319
325
  /**
@@ -367,8 +373,21 @@ export class RunProtocolSession {
367
373
  await this.pushEvent(this.createEvent("checkpoints", namespace, data));
368
374
  return true;
369
375
  }
370
- default:
371
- return false;
376
+ default: {
377
+ if (!method.startsWith("custom:")) {
378
+ return false;
379
+ }
380
+ const channelName = method.slice("custom:".length);
381
+ if (!channelName) {
382
+ return false;
383
+ }
384
+ await this.ensureNamespaces(namespace);
385
+ await this.pushEvent(this.createEvent("custom", namespace, {
386
+ name: channelName,
387
+ payload: data,
388
+ }));
389
+ return true;
390
+ }
372
391
  }
373
392
  }
374
393
  /**
@@ -472,25 +491,13 @@ export class RunProtocolSession {
472
491
  * @returns Whether the event matches channel and namespace filters.
473
492
  */
474
493
  matchesSubscription(subscription, event) {
475
- const channel = event.method === "input.requested"
476
- ? "input"
477
- : isSupportedChannel(event.method)
478
- ? event.method
479
- : undefined;
494
+ // `inferChannel` resolves named custom channels to `custom:<name>`; a bare
495
+ // `custom` subscription still matches via the prefix check below.
496
+ const channel = inferChannel(event);
480
497
  if (channel == null)
481
498
  return false;
482
- // Support "custom:name" subscriptions: "custom:a2a" matches custom
483
- // events with data.name === "a2a", while "custom" matches all.
484
- let channelMatched = subscription.channels.has(channel);
485
- if (!channelMatched && channel === "custom") {
486
- const params = event.params;
487
- const eventName = isRecord(params.data) && typeof params.data.name === "string"
488
- ? params.data.name
489
- : undefined;
490
- if (eventName != null) {
491
- channelMatched = subscription.channels.has(`custom:${eventName}`);
492
- }
493
- }
499
+ const channelMatched = subscription.channels.has(channel) ||
500
+ (channel.startsWith("custom:") && subscription.channels.has("custom"));
494
501
  if (!channelMatched)
495
502
  return false;
496
503
  if (subscription.namespaces == null ||
@@ -562,9 +569,7 @@ export class RunProtocolSession {
562
569
  await this.sendError(command.id, "invalid_argument", "subscription.subscribe requires a non-empty channels array.");
563
570
  return;
564
571
  }
565
- const channels = rawChannels.filter((value) => typeof value === "string" &&
566
- (SUPPORTED_CHANNELS.has(value) ||
567
- value.startsWith("custom:")));
572
+ const channels = rawChannels.filter((value) => typeof value === "string" && isSupportedChannel(value));
568
573
  if (channels.length !== rawChannels.length) {
569
574
  await this.sendError(command.id, "invalid_argument", "subscription.subscribe received an unsupported channel.");
570
575
  return;
@@ -648,9 +653,7 @@ export class RunProtocolSession {
648
653
  if (!Array.isArray(rawChannels) || rawChannels.length === 0) {
649
654
  return this.error(command.id, "invalid_argument", "subscription.subscribe requires a non-empty channels array.", meta);
650
655
  }
651
- const channels = rawChannels.filter((value) => typeof value === "string" &&
652
- (SUPPORTED_CHANNELS.has(value) ||
653
- value.startsWith("custom:")));
656
+ const channels = rawChannels.filter((value) => typeof value === "string" && isSupportedChannel(value));
654
657
  if (channels.length !== rawChannels.length) {
655
658
  return this.error(command.id, "invalid_argument", "subscription.subscribe received an unsupported channel.", meta);
656
659
  }
@@ -47,10 +47,6 @@ export type ProtocolEventDataMap = {
47
47
  input: ProtocolEventByMethod<"input">["params"]["data"];
48
48
  tasks: ProtocolEventByMethod<"tasks">["params"]["data"];
49
49
  };
50
- /**
51
- * Channel names supported by the run-scoped protocol session.
52
- */
53
- export declare const SUPPORTED_CHANNELS: Set<SupportedChannel>;
54
50
  /**
55
51
  * Checks whether a value is a non-null object record.
56
52
  *
@@ -58,10 +54,3 @@ export declare const SUPPORTED_CHANNELS: Set<SupportedChannel>;
58
54
  * @returns Whether the value can be treated as a keyed object.
59
55
  */
60
56
  export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
61
- /**
62
- * Checks whether a channel name is supported by the session transport.
63
- *
64
- * @param value - Raw channel name.
65
- * @returns Whether the name matches a supported protocol channel.
66
- */
67
- export declare const isSupportedChannel: (value: string) => value is SupportedChannel;
@@ -1,17 +1,3 @@
1
- /**
2
- * Channel names supported by the run-scoped protocol session.
3
- */
4
- export const SUPPORTED_CHANNELS = new Set([
5
- "values",
6
- "updates",
7
- "checkpoints",
8
- "messages",
9
- "tools",
10
- "custom",
11
- "lifecycle",
12
- "input",
13
- "tasks",
14
- ]);
15
1
  /**
16
2
  * Checks whether a value is a non-null object record.
17
3
  *
@@ -19,10 +5,3 @@ export const SUPPORTED_CHANNELS = new Set([
19
5
  * @returns Whether the value can be treated as a keyed object.
20
6
  */
21
7
  export const isRecord = (value) => typeof value === "object" && value !== null;
22
- /**
23
- * Checks whether a channel name is supported by the session transport.
24
- *
25
- * @param value - Raw channel name.
26
- * @returns Whether the name matches a supported protocol channel.
27
- */
28
- export const isSupportedChannel = (value) => SUPPORTED_CHANNELS.has(value);
@@ -1,4 +1,11 @@
1
+ import { isPrefixMatch, normalizeNamespaceSegment } from "@langchain/langgraph/stream";
1
2
  import type { Namespace } from "../types.mjs";
3
+ /**
4
+ * Namespace prefix matching and dynamic-suffix normalization are shared with
5
+ * the core streaming toolkit so the server, the SDK, and custom transports all
6
+ * agree on subscription semantics.
7
+ */
8
+ export { isPrefixMatch, normalizeNamespaceSegment };
2
9
  /**
3
10
  * Converts a namespace array into a stable internal lookup key.
4
11
  *
@@ -6,13 +13,6 @@ import type { Namespace } from "../types.mjs";
6
13
  * @returns A string key that preserves segment boundaries.
7
14
  */
8
15
  export declare const toNamespaceKey: (namespace: Namespace) => string;
9
- /**
10
- * Strips dynamic suffixes from a namespace segment for display purposes.
11
- *
12
- * @param segment - Raw namespace segment.
13
- * @returns The stable graph-oriented portion of the segment.
14
- */
15
- export declare const normalizeNamespaceSegment: (segment: string) => string;
16
16
  /**
17
17
  * Preserves raw namespace segments in protocol events.
18
18
  *
@@ -30,14 +30,6 @@ export declare const parseEventName: (event: string) => {
30
30
  method: string;
31
31
  namespace: string[];
32
32
  };
33
- /**
34
- * Checks whether a namespace starts with a requested prefix.
35
- *
36
- * @param namespace - Event namespace to test.
37
- * @param prefix - Subscription namespace prefix.
38
- * @returns Whether the namespace matches the prefix semantics.
39
- */
40
- export declare const isPrefixMatch: (namespace: Namespace, prefix: Namespace) => boolean;
41
33
  /**
42
34
  * Guesses a human-readable graph name from a namespace.
43
35
  *
@@ -1,3 +1,10 @@
1
+ import { isPrefixMatch, normalizeNamespaceSegment, } from "@langchain/langgraph/stream";
2
+ /**
3
+ * Namespace prefix matching and dynamic-suffix normalization are shared with
4
+ * the core streaming toolkit so the server, the SDK, and custom transports all
5
+ * agree on subscription semantics.
6
+ */
7
+ export { isPrefixMatch, normalizeNamespaceSegment };
1
8
  /**
2
9
  * Converts a namespace array into a stable internal lookup key.
3
10
  *
@@ -5,13 +12,6 @@
5
12
  * @returns A string key that preserves segment boundaries.
6
13
  */
7
14
  export const toNamespaceKey = (namespace) => namespace.join("\0");
8
- /**
9
- * Strips dynamic suffixes from a namespace segment for display purposes.
10
- *
11
- * @param segment - Raw namespace segment.
12
- * @returns The stable graph-oriented portion of the segment.
13
- */
14
- export const normalizeNamespaceSegment = (segment) => segment.split(":")[0];
15
15
  /**
16
16
  * Preserves raw namespace segments in protocol events.
17
17
  *
@@ -29,25 +29,6 @@ export const parseEventName = (event) => {
29
29
  const [method, ...namespace] = event.split("|");
30
30
  return { method, namespace };
31
31
  };
32
- /**
33
- * Checks whether a namespace starts with a requested prefix.
34
- *
35
- * @param namespace - Event namespace to test.
36
- * @param prefix - Subscription namespace prefix.
37
- * @returns Whether the namespace matches the prefix semantics.
38
- */
39
- export const isPrefixMatch = (namespace, prefix) => {
40
- if (prefix.length > namespace.length)
41
- return false;
42
- return prefix.every((segment, index) => {
43
- const candidate = namespace[index];
44
- if (candidate === segment)
45
- return true;
46
- if (segment.includes(":"))
47
- return false;
48
- return normalizeNamespaceSegment(candidate) === segment;
49
- });
50
- };
51
32
  /**
52
33
  * Guesses a human-readable graph name from a namespace.
53
34
  *
package/dist/stream.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { isBaseMessage } from "@langchain/core/messages";
2
2
  import { LangChainTracer } from "@langchain/core/tracers/tracer_langchain";
3
- import { convertToProtocolEvent, STREAM_EVENTS_V3_MODES, } from "@langchain/langgraph/web";
3
+ import { convertToProtocolEvent, isCheckpointEnvelope, STREAM_EVENTS_V3_MODES, } from "@langchain/langgraph/web";
4
4
  import { Client as LangSmithClient, getDefaultProjectName } from "langsmith";
5
5
  import { getLangGraphCommand } from "./command.mjs";
6
6
  import { PROTOCOL_STREAM_RUN_KEY } from "./protocol/constants.mjs";
@@ -136,14 +136,10 @@ export async function* streamState(run, options) {
136
136
  continue;
137
137
  if (event.event === "on_chain_stream" &&
138
138
  (kwargs.subgraphs || event.run_id === run.run_id)) {
139
- // Pregel's stream tuple is `[ns, mode, payload, meta?]` (4th element
140
- // is the optional `StreamChunkMeta`, preserved when streaming with
141
- // `subgraphs: true`). The meta carries the lightweight checkpoint
142
- // envelope attached by `_emitValuesWithCheckpointMeta`, which we
143
- // forward as a companion `checkpoints` source event below.
139
+ // Pregel stream chunks are `[ns, mode, payload]`. Lightweight checkpoint
140
+ // envelopes arrive as a separate `checkpoints` chunk before `values`.
144
141
  const rawTuple = (kwargs.subgraphs ? event.data.chunk : [null, ...event.data.chunk]);
145
142
  const [ns, mode, chunk] = rawTuple;
146
- const chunkMeta = rawTuple[3];
147
143
  let data = chunk;
148
144
  if (mode === "debug") {
149
145
  const debugChunk = chunk;
@@ -159,9 +155,26 @@ export async function* streamState(run, options) {
159
155
  }
160
156
  }
161
157
  else if (mode === "checkpoints") {
162
- const debugCheckpoint = preprocessDebugCheckpoint(chunk);
158
+ if (isCheckpointEnvelope(chunk)) {
159
+ // Lightweight envelopes pair with `values` on the v3 path.
160
+ // Legacy `checkpoints` stream mode consumers expect full debug
161
+ // snapshots (`values` / `metadata` / `next`) from `mapDebugCheckpoint`.
162
+ if (!userStreamMode.includes("checkpoints")) {
163
+ const sseEvent = kwargs.subgraphs && ns?.length
164
+ ? `checkpoints|${ns.join("|")}`
165
+ : "checkpoints";
166
+ yield { event: sseEvent, data: chunk };
167
+ }
168
+ continue;
169
+ }
170
+ const debugPayload = chunk;
171
+ const debugCheckpoint = preprocessDebugCheckpoint(debugPayload);
163
172
  options?.onCheckpoint?.(debugCheckpoint);
164
- data = debugCheckpoint;
173
+ data = {
174
+ values: debugPayload.values,
175
+ metadata: debugPayload.metadata,
176
+ next: debugPayload.next,
177
+ };
165
178
  }
166
179
  else if (mode === "tasks") {
167
180
  const debugTask = preprocessDebugCheckpointTask(chunk);
@@ -170,20 +183,6 @@ export async function* streamState(run, options) {
170
183
  }
171
184
  data = debugTask;
172
185
  }
173
- // Emit the lightweight checkpoint envelope as a dedicated
174
- // `checkpoints` source event immediately BEFORE the companion
175
- // `values` event so clients subscribed to both channels have the
176
- // envelope buffered by the time the values payload arrives
177
- // (`useMessageMetadata(msg.id).parentCheckpointId` for fork /
178
- // edit flows). Clients that only want fork / time-travel metadata
179
- // subscribe to `checkpoints` alone and avoid the full-state
180
- // payload.
181
- if (mode === "values" && chunkMeta?.checkpoint != null) {
182
- const sseEvent = kwargs.subgraphs && ns?.length
183
- ? `checkpoints|${ns.join("|")}`
184
- : "checkpoints";
185
- yield { event: sseEvent, data: chunkMeta.checkpoint };
186
- }
187
186
  if (mode === "messages") {
188
187
  if (userStreamMode.includes("messages-tuple")) {
189
188
  if (kwargs.subgraphs && ns?.length) {
@@ -270,13 +269,12 @@ async function* fallbackProtocolStreamFromGraphStream(graph, input, options) {
270
269
  let seq = 0;
271
270
  const stream = await graph.stream(input, options);
272
271
  for await (const tuple of stream) {
273
- const [namespace, mode, payload, meta] = tuple;
272
+ const [namespace, mode, payload] = tuple;
274
273
  const events = convertToProtocolEvent({
275
274
  namespace: namespace ?? [],
276
275
  mode,
277
276
  payload,
278
277
  seq,
279
- meta: meta,
280
278
  });
281
279
  seq += events.length;
282
280
  for (const event of events) {
@@ -411,6 +409,7 @@ export async function* streamStateV2(run, options) {
411
409
  const normalized = mode === "tools" ||
412
410
  mode === "updates" ||
413
411
  mode === "custom" ||
412
+ mode.startsWith("custom:") ||
414
413
  mode === "messages" ||
415
414
  mode === "checkpoints" ||
416
415
  mode === "lifecycle";
@@ -1,2 +1,2 @@
1
- declare const _default: import("vitest/config.js").UserConfigFnObject;
1
+ declare const _default: import("vitest/config").ViteUserConfigFnObject;
2
2
  export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/langgraph-api",
3
- "version": "1.2.4",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": "^18.19.0 || >=20.16.0"
@@ -61,7 +61,7 @@
61
61
  "dedent": "^1.5.3",
62
62
  "dotenv": "^16.4.7",
63
63
  "exit-hook": "^4.0.0",
64
- "hono": "^4.12.18",
64
+ "hono": "^4.12.21",
65
65
  "langsmith": ">=0.5.19 <1.0.0",
66
66
  "open": "^10.1.0",
67
67
  "semver": "^7.7.1",
@@ -72,11 +72,11 @@
72
72
  "winston": "^3.17.0",
73
73
  "winston-console-format": "^1.0.8",
74
74
  "zod": "^3.25.76 || ^4",
75
- "@langchain/langgraph-ui": "1.2.4"
75
+ "@langchain/langgraph-ui": "1.3.0"
76
76
  },
77
77
  "peerDependencies": {
78
- "@langchain/core": "^1.1.44",
79
- "@langchain/langgraph": "^0.2.57 || ^0.3.0 || ^0.4.0 || ^1.0.0-alpha || ^1.0.0 || ^1.3.1-rc.0",
78
+ "@langchain/core": "^1.1.48",
79
+ "@langchain/langgraph": "^1.3.6",
80
80
  "@langchain/langgraph-checkpoint": "~0.0.16 || ^0.1.0 || ~1.0.0",
81
81
  "@langchain/langgraph-sdk": "^1.9.3-rc.0",
82
82
  "typescript": "^5.5.4"
@@ -87,23 +87,23 @@
87
87
  }
88
88
  },
89
89
  "devDependencies": {
90
- "@langchain/core": "^1.1.44",
90
+ "@langchain/core": "^1.1.48",
91
91
  "@types/babel__code-frame": "^7.0.6",
92
92
  "@types/node": "^18.15.11",
93
- "@types/react": "^19.0.8",
93
+ "@types/react": "^19.2.16",
94
94
  "@types/react-dom": "^19.0.3",
95
95
  "@types/semver": "^7.7.0",
96
96
  "@types/uuid": "^10.0.0",
97
97
  "deepagents": "^1.9.0",
98
98
  "jose": "^6.0.10",
99
- "langchain": "^1.2.30",
99
+ "langchain": "^1.4.4",
100
100
  "postgres": "^3.4.5",
101
101
  "typescript": "^4.9.5 || ^5.4.5",
102
- "vitest": "^3.2.4",
102
+ "vitest": "^4.1.0",
103
103
  "wait-port": "^1.1.0",
104
- "@langchain/langgraph": "1.3.3",
104
+ "@langchain/langgraph": "1.3.7",
105
105
  "@langchain/langgraph-checkpoint": "1.0.4",
106
- "@langchain/langgraph-sdk": "1.9.11"
106
+ "@langchain/langgraph-sdk": "1.9.19"
107
107
  },
108
108
  "scripts": {
109
109
  "clean": "rm -rf dist/ .turbo/ ./tests/graphs/.langgraph_api/ ./tests/protocol-v2/graphs/.langgraph_api/",