langgraph-api 0.18.0.dev10__py3-none-any.whl → 0.18.0.dev12__py3-none-any.whl

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.
langgraph_api/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.18.0.dev10"
1
+ __version__ = "0.18.0.dev12"
@@ -7,6 +7,7 @@ from starlette.responses import Response
7
7
  from starlette.routing import BaseRoute
8
8
 
9
9
  from langgraph_api import config as lg_api_config
10
+ from langgraph_api.api.runs import parse_stream_mode_param
10
11
  from langgraph_api.config import USE_CUSTOM_CHECKPOINTER
11
12
  from langgraph_api.encryption.shared import (
12
13
  using_encryption,
@@ -589,24 +590,20 @@ async def join_thread_stream(request: ApiRequest):
589
590
  last_event_id, "Invalid last-event-id: must be a valid Redis stream ID"
590
591
  )
591
592
 
592
- # Parse stream_modes parameter - can be single string or comma-separated list
593
- stream_modes_param = request.query_params.get("stream_modes")
594
- if stream_modes_param:
595
- if "," in stream_modes_param:
596
- # Handle comma-separated list
597
- stream_modes = [mode.strip() for mode in stream_modes_param.split(",")]
598
- else:
599
- # Handle single value
600
- stream_modes = [stream_modes_param]
601
- # Validate each mode
602
- for mode in stream_modes:
603
- if mode not in get_args(ThreadStreamMode):
604
- raise HTTPException(
605
- status_code=422, detail=f"Invalid stream mode: {mode}"
606
- )
607
- else:
608
- # Default to run_modes
609
- stream_modes = ["run_modes"]
593
+ # The SDKs send ``stream_mode``: once per mode, or as one JSON array from
594
+ # JS SDKs before 1.12.
595
+ params = request.query_params
596
+ values = params.getlist("stream_modes") or params.getlist("stream_mode")
597
+ stream_modes = [
598
+ mode.strip()
599
+ for value in values
600
+ if value
601
+ for item in parse_stream_mode_param(value)
602
+ for mode in str(item).split(",")
603
+ ] or ["run_modes"]
604
+ for mode in stream_modes:
605
+ if mode not in get_args(ThreadStreamMode):
606
+ raise HTTPException(status_code=422, detail=f"Invalid stream mode: {mode}")
610
607
 
611
608
  return EventSourceResponse(
612
609
  Threads.Stream.join(
langgraph_api/graph.py CHANGED
@@ -375,7 +375,10 @@ async def get_graph(
375
375
  CONFIG_KEY_RUNTIME,
376
376
  )
377
377
 
378
- if use_langgraph_runtime:
378
+ if use_langgraph_runtime or access_context in (
379
+ "threads.read",
380
+ "threads.update",
381
+ ):
379
382
  # xray draw calls runtime.override(), which the SDK ServerRuntime lacks (LSD-1713).
380
383
  from langgraph.runtime import Runtime # noqa: PLC0415
381
384
 
@@ -138,6 +138,14 @@ class GrpcClient:
138
138
  "grpc.http2.initial_window_size",
139
139
  config.GRPC_CLIENT_HTTP2_INITIAL_WINDOW_SIZE,
140
140
  ),
141
+ # The core runs next to the API, so retry a lost connection fast:
142
+ # gRPC's default backoff grows toward two minutes, which kept API
143
+ # pods failing for up to 90 seconds after a core restart that took
144
+ # seconds. Leave grpc.min_reconnect_backoff_ms alone: gRPC uses it
145
+ # as the minimum connect timeout (20 s), not as a floor between
146
+ # retries.
147
+ ("grpc.initial_reconnect_backoff_ms", 100),
148
+ ("grpc.max_reconnect_backoff_ms", 1000),
141
149
  ]
142
150
 
143
151
  self._channel = aio.insecure_channel(
@@ -1223,6 +1223,13 @@ class Runs(Authenticated):
1223
1223
  grpc_code=e.code().name,
1224
1224
  )
1225
1225
  raise
1226
+ except asyncio.CancelledError:
1227
+ logger.info(
1228
+ "Run cancelled, will not signal done",
1229
+ run_id=run_id,
1230
+ thread_id=thread_id,
1231
+ )
1232
+ raise
1226
1233
  except BaseException:
1227
1234
  logger.exception(
1228
1235
  "Non-retriable exception when marking run as done",
@@ -960,18 +960,21 @@ class Threads(Authenticated):
960
960
  Yields:
961
961
  Tuples of (event_bytes, message_bytes, stream_id_bytes|None)
962
962
  """
963
- async for (
964
- event_bytes,
965
- message_bytes,
966
- stream_id_bytes,
967
- _run_id,
968
- ) in Threads.Stream.join_event_streaming(
969
- thread_id,
970
- stream_modes=stream_modes,
971
- last_event_id=last_event_id,
972
- ctx=ctx,
973
- ):
974
- yield (event_bytes, message_bytes, stream_id_bytes)
963
+ async with aclosing(
964
+ Threads.Stream.join_event_streaming(
965
+ thread_id,
966
+ stream_modes=stream_modes,
967
+ last_event_id=last_event_id,
968
+ ctx=ctx,
969
+ )
970
+ ) as events:
971
+ async for (
972
+ event_bytes,
973
+ message_bytes,
974
+ stream_id_bytes,
975
+ _run_id,
976
+ ) in events:
977
+ yield (event_bytes, message_bytes, stream_id_bytes)
975
978
 
976
979
  @staticmethod
977
980
  async def join_event_streaming(
@@ -1006,9 +1009,9 @@ class Threads(Authenticated):
1006
1009
  request.last_event_id = last_event_id
1007
1010
 
1008
1011
  client = await get_shared_client()
1009
-
1012
+ call = client.threads.Stream(request)
1010
1013
  try:
1011
- async for event in client.threads.Stream(request):
1014
+ async for event in call:
1012
1015
  # Convert protobuf StreamEvent to tuple format
1013
1016
  event_bytes = event.event_type.encode("utf-8")
1014
1017
  message_bytes = event.message
@@ -1028,6 +1031,8 @@ class Threads(Authenticated):
1028
1031
  if isinstance(e, AioRpcError):
1029
1032
  _handle_grpc_error(e)
1030
1033
  raise
1034
+ finally:
1035
+ call.cancel()
1031
1036
 
1032
1037
  @staticmethod
1033
1038
  async def check_thread_stream_auth(
@@ -6,7 +6,7 @@ import { serve } from "@hono/node-server";
6
6
  import { zValidator } from "@hono/zod-validator";
7
7
  import { streamSSE, stream } from "hono/streaming";
8
8
  import { HTTPException } from "hono/http-exception";
9
- import { fetch } from "undici";
9
+ import { Agent, fetch as undiciFetch } from "undici";
10
10
  import pRetry from "p-retry";
11
11
  import {
12
12
  getConfig,
@@ -26,6 +26,7 @@ import {
26
26
  type ChannelVersions,
27
27
  type ChannelProtocol,
28
28
  } from "@langchain/langgraph-checkpoint";
29
+ import { AsyncLocalStorage } from "node:async_hooks";
29
30
  import * as fs from "node:fs/promises";
30
31
  import * as path from "node:path";
31
32
  import { serialiseAsDict, serializeError } from "./src/utils/serde.mjs";
@@ -227,6 +228,44 @@ async function getOrExtractSchema(graphId: string) {
227
228
  // Pinned by the Python parent (see remote.py); base ports are the fallback.
228
229
  const GRAPH_PORT = Number(process.env.LANGGRAPH_JS_GRAPH_PORT) || 5556;
229
230
  const REMOTE_PORT = Number(process.env.LANGGRAPH_JS_REMOTE_PORT) || 5555;
231
+ const REMOTE_SOCKET = process.env.LANGGRAPH_JS_REMOTE_SOCKET;
232
+
233
+ // Unix: HTTP over a Unix socket (dummy host; undici dials socketPath).
234
+ // Windows: TCP localhost for checkpointer/store only; no loopback client.
235
+ const remoteAgent = REMOTE_SOCKET
236
+ ? new Agent({ connect: { socketPath: REMOTE_SOCKET } })
237
+ : undefined;
238
+ const REMOTE_ORIGIN = REMOTE_SOCKET
239
+ ? "http://loopback"
240
+ : `http://localhost:${REMOTE_PORT}`;
241
+
242
+ const LOOPBACK_RUN_HEADER = "x-langgraph-loopback-run";
243
+ const loopbackRun = new AsyncLocalStorage<string>();
244
+
245
+ function remoteFetch(
246
+ input: Parameters<typeof undiciFetch>[0],
247
+ init?: Parameters<typeof undiciFetch>[1],
248
+ ) {
249
+ const token = loopbackRun.getStore();
250
+ if (token) {
251
+ const headers = new Headers(init?.headers as HeadersInit | undefined);
252
+ headers.set(LOOPBACK_RUN_HEADER, token);
253
+ init = { ...init, headers };
254
+ }
255
+ if (remoteAgent) {
256
+ return undiciFetch(input, { ...init, dispatcher: remoteAgent });
257
+ }
258
+ return undiciFetch(input, init);
259
+ }
260
+
261
+ // Default `new Client()` (no apiUrl) reads these symbols.
262
+ const LOOPBACK_URL = REMOTE_SOCKET ? `${REMOTE_ORIGIN}/loopback` : undefined;
263
+
264
+ if (LOOPBACK_URL) {
265
+ const g = globalThis as unknown as Record<symbol, unknown>;
266
+ g[Symbol.for("langgraph_api:url")] = LOOPBACK_URL;
267
+ if (remoteAgent) g[Symbol.for("langgraph_api:fetch")] = remoteFetch;
268
+ }
230
269
 
231
270
  const RunnableConfigSchema = z.object({
232
271
  tags: z.array(z.string()).optional(),
@@ -253,10 +292,10 @@ const getRunnableConfig = (
253
292
  };
254
293
  };
255
294
 
256
- function tryFetch(...args: Parameters<typeof fetch>) {
295
+ function tryFetch(...args: Parameters<typeof remoteFetch>) {
257
296
  return pRetry(
258
297
  async () => {
259
- const response = await fetch(...args).catch((error) => {
298
+ const response = await remoteFetch(...args).catch((error) => {
260
299
  throw new Error(`${args[0]} connecfailed: ${error}`);
261
300
  });
262
301
 
@@ -264,7 +303,7 @@ function tryFetch(...args: Parameters<typeof fetch>) {
264
303
  let errorMessage = `${args[0]} failed: HTTP ${response.status}`;
265
304
  try {
266
305
  errorMessage += `: ${await response.text()}`;
267
- } catch { }
306
+ } catch {}
268
307
  throw new Error(errorMessage);
269
308
  }
270
309
 
@@ -283,7 +322,7 @@ async function sendRecv<T = any>(
283
322
  method: `${"checkpointer" | "store"}_${string}`,
284
323
  data: unknown,
285
324
  ): Promise<T> {
286
- const res = await tryFetch(`http://localhost:${REMOTE_PORT}/${method}`, {
325
+ const res = await tryFetch(`${REMOTE_ORIGIN}/${method}`, {
287
326
  method: "POST",
288
327
  headers: { "Content-Type": "application/json" },
289
328
  body: JSON.stringify(data),
@@ -655,9 +694,7 @@ const StreamEventsPayload = z.object({
655
694
  .nullish(),
656
695
  })
657
696
  .nullish(),
658
- stream_mode: z
659
- .union([z.string(), z.array(z.string())])
660
- .optional(),
697
+ stream_mode: z.union([z.string(), z.array(z.string())]).optional(),
661
698
  config: RunnableConfigSchema.nullish(),
662
699
  context: z.record(z.unknown()).nullish(),
663
700
  interrupt_before: z.union([z.array(z.string()), z.literal("*")]).nullish(),
@@ -671,6 +708,7 @@ const StreamEventsPayload = z.object({
671
708
  // streaming endpoints.
672
709
  stream_protocol: z.enum(["v2", "v3"]).nullish(),
673
710
  output_keys: z.union([z.string(), z.array(z.string())]).nullish(),
711
+ loopback_token: z.string().nullish(),
674
712
  });
675
713
 
676
714
  function reviveCommand(
@@ -697,7 +735,8 @@ async function* streamEventsRequest(
697
735
  rawPayload: z.infer<typeof StreamEventsPayload>,
698
736
  signal: AbortSignal,
699
737
  ) {
700
- const { graph_id: graphId, ...payload } = rawPayload;
738
+ const { graph_id: graphId, loopback_token: token, ...payload } = rawPayload;
739
+ if (token) loopbackRun.enterWith(token);
701
740
  const config = {
702
741
  ...getRunnableConfig(payload.config),
703
742
  context: payload.context ?? undefined,
@@ -793,47 +832,41 @@ async function* streamEventsRequest(
793
832
  options: Record<string, unknown>,
794
833
  ) => Promise<AsyncIterable<unknown>>;
795
834
  }
796
- ).streamEvents(
797
- input,
798
- {
799
- ...config,
800
- signal,
801
- version: "v3",
802
- outputKeys:
803
- payload.output_keys === undefined
804
- ? graph.outputChannels
805
- : (payload.output_keys ?? undefined),
806
- interruptBefore,
807
- interruptAfter,
808
- ...(callbacks ? { callbacks } : {}),
809
- ...(payload.durability === undefined
810
- ? {}
811
- : { durability: payload.durability }),
812
- },
813
- );
814
-
815
- for await (const event of protocolStream) {
816
- yield event;
817
- }
818
- return;
819
- }
820
-
821
- for await (const data of graph.streamEvents(
822
- input,
823
- {
835
+ ).streamEvents(input, {
824
836
  ...config,
825
837
  signal,
826
- version: "v2",
827
- streamMode,
828
- subgraphs: payload.subgraphs,
838
+ version: "v3",
839
+ outputKeys:
840
+ payload.output_keys === undefined
841
+ ? graph.outputChannels
842
+ : (payload.output_keys ?? undefined),
829
843
  interruptBefore,
830
844
  interruptAfter,
831
845
  ...(callbacks ? { callbacks } : {}),
832
846
  ...(payload.durability === undefined
833
847
  ? {}
834
848
  : { durability: payload.durability }),
835
- },
836
- )) {
849
+ });
850
+
851
+ for await (const event of protocolStream) {
852
+ yield event;
853
+ }
854
+ return;
855
+ }
856
+
857
+ for await (const data of graph.streamEvents(input, {
858
+ ...config,
859
+ signal,
860
+ version: "v2",
861
+ streamMode,
862
+ subgraphs: payload.subgraphs,
863
+ interruptBefore,
864
+ interruptAfter,
865
+ ...(callbacks ? { callbacks } : {}),
866
+ ...(payload.durability === undefined
867
+ ? {}
868
+ : { durability: payload.durability }),
869
+ })) {
837
870
  // TODO: upstream this fix to LangGraphJS
838
871
  if (streamMode.length === 1 && !Array.isArray(data.data.chunk)) {
839
872
  data.data.chunk = [streamMode[0], data.data.chunk];
@@ -850,7 +883,11 @@ async function* streamEventsRequest(
850
883
  if (userStreamMode.includes("messages")) {
851
884
  if (data.event === "on_chain_stream" && data.run_id === config.runId) {
852
885
  const newMessages: Array<BaseMessageChunk> = [];
853
- const [_, chunk]: [string, any] = data.data.chunk;
886
+ const tuple: unknown[] = data.data.chunk;
887
+ // With subgraphs on, chunks are [ns, mode, chunk]; a subgraph's
888
+ // values are not the root graph's messages.
889
+ if (tuple.length === 3 && (tuple[0] as string[]).length > 0) continue;
890
+ const chunk: any = tuple.at(-1);
854
891
 
855
892
  let chunkMessages: Array<BaseMessageChunk> = [];
856
893
  if (
@@ -1140,16 +1177,16 @@ async function main() {
1140
1177
  meta.threshold_ms = GRAPH_ERROR_MS;
1141
1178
  logger.error(
1142
1179
  "Graph import exceeded error threshold. " +
1143
- "Slow initialization (often due to work executed at import time) can delay readiness, " +
1144
- "slow scale-out velocity, and may cause deployments to be marked unhealthy.",
1180
+ "Slow initialization (often due to work executed at import time) can delay readiness, " +
1181
+ "slow scale-out velocity, and may cause deployments to be marked unhealthy.",
1145
1182
  meta,
1146
1183
  );
1147
1184
  } else if (durationMs > GRAPH_WARN_MS) {
1148
1185
  meta.threshold_ms = GRAPH_WARN_MS;
1149
1186
  logger.warn(
1150
1187
  "Graph import exceeded warn threshold. " +
1151
- "Slow initialization (often due to work executed at import time) can delay readiness, " +
1152
- "slow scale-out velocity, and may cause deployments to be marked unhealthy.",
1188
+ "Slow initialization (often due to work executed at import time) can delay readiness, " +
1189
+ "slow scale-out velocity, and may cause deployments to be marked unhealthy.",
1153
1190
  meta,
1154
1191
  );
1155
1192
  } else {
@@ -1172,14 +1209,14 @@ async function main() {
1172
1209
  totalMeta.threshold_ms = GRAPH_ERROR_MS;
1173
1210
  logger.error(
1174
1211
  "All graphs resolved but exceeded error threshold. " +
1175
- "Consider reducing the number of graphs or optimizing their imports.",
1212
+ "Consider reducing the number of graphs or optimizing their imports.",
1176
1213
  totalMeta,
1177
1214
  );
1178
1215
  } else if (resolveTotal > GRAPH_WARN_MS) {
1179
1216
  totalMeta.threshold_ms = GRAPH_WARN_MS;
1180
1217
  logger.warn(
1181
1218
  "All graphs resolved but exceeded warn threshold. " +
1182
- "Consider optimizing graph imports to improve startup time.",
1219
+ "Consider optimizing graph imports to improve startup time.",
1183
1220
  totalMeta,
1184
1221
  );
1185
1222
  } else {
@@ -4,6 +4,7 @@ declare namespace NodeJS {
4
4
  LANGGRAPH_UI?: string;
5
5
  LANGGRAPH_UI_CONFIG?: string;
6
6
  LANGGRAPH_AUTH?: string;
7
+ LANGGRAPH_JS_REMOTE_SOCKET?: string;
7
8
  PORT: string;
8
9
  }
9
10
  }
@@ -1,13 +1,18 @@
1
1
  import asyncio
2
2
  import contextlib
3
+ import copy
3
4
  import json
4
5
  import logging
5
6
  import os
6
7
  import re
8
+ import secrets
7
9
  import shutil
8
10
  import ssl
11
+ import sys
12
+ import tempfile
13
+ import time
9
14
  from collections import deque
10
- from collections.abc import AsyncIterator, Callable
15
+ from collections.abc import AsyncIterator, Callable, Iterator, Mapping
11
16
  from inspect import signature
12
17
  from typing import Any, Literal, Self, cast
13
18
 
@@ -24,6 +29,7 @@ from langchain_core.runnables.schema import (
24
29
  StandardStreamEvent,
25
30
  StreamEvent,
26
31
  )
32
+ from langgraph.checkpoint.memory import InMemorySaver
27
33
  from langgraph.checkpoint.serde.base import SerializerProtocol
28
34
  from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
29
35
  from langgraph.types import Command, PregelTask, Send, StateSnapshot
@@ -52,13 +58,17 @@ from langgraph_api.js.sse import SSEDecoder, aiter_lines_raw
52
58
  from langgraph_api.route import ApiResponse
53
59
  from langgraph_api.schema import Config
54
60
  from langgraph_api.serde import json_dumpb
55
- from langgraph_api.utils import get_auth_ctx, get_user_id
61
+ from langgraph_api.utils import AuthContext, get_auth_ctx, get_user_id
56
62
 
57
63
  logger = structlog.stdlib.get_logger(__name__)
58
64
 
59
- # Loopback ports between the Python parent and its JS subprocess. Containers
60
- # colocated in one pod share a network namespace, so each entrypoint gets its
61
- # own range (via the offset below); LANGGRAPH_JS_*_PORT overrides any port.
65
+ # Loopback IPC between the Python parent and its JS subprocess.
66
+ # GRAPH_* stay on TCP; the remote server (checkpointer/store + /loopback) is a
67
+ # Unix socket except on Windows, where uvicorn cannot bind AF_UNIX.
68
+ # Containers colocated in one pod share a network namespace, so each
69
+ # entrypoint gets its own TCP range (via the offset below); LANGGRAPH_JS_*_PORT
70
+ # overrides any port. The remote socket path includes the pid so API and queue
71
+ # workers in one container do not collide.
62
72
  _BASE_REMOTE_PORT = 5555
63
73
  _BASE_GRAPH_PORT = 5556
64
74
  _BASE_GRAPH_HTTP_PORT = 5557
@@ -90,6 +100,11 @@ GRAPH_PORT = _resolve_js_port(
90
100
  GRAPH_HTTP_PORT = _resolve_js_port(
91
101
  _BASE_GRAPH_HTTP_PORT, _JS_PORT_OFFSET, os.getenv("LANGGRAPH_JS_GRAPH_HTTP_PORT")
92
102
  )
103
+ REMOTE_SOCKET = (
104
+ None
105
+ if sys.platform == "win32"
106
+ else os.path.join(tempfile.gettempdir(), f"lg-js-r{os.getpid()}.sock")
107
+ )
93
108
  SSL = ssl.create_default_context(cafile=certifi.where())
94
109
 
95
110
  if (port := int(os.getenv("PORT", "8080"))) and port in (GRAPH_PORT, REMOTE_PORT):
@@ -97,6 +112,40 @@ if (port := int(os.getenv("PORT", "8080"))) and port in (GRAPH_PORT, REMOTE_PORT
97
112
  f"PORT={port} is a reserved port for the JS worker. Please choose a different port."
98
113
  )
99
114
 
115
+
116
+ def _js_graph_worker_env(**extra: str | None) -> dict[str, str]:
117
+ """Env for the Node graph worker. Unix only: path of the remote UDS."""
118
+ env = {
119
+ "LANGCHAIN_CALLBACKS_BACKGROUND": "true",
120
+ "CHOKIDAR_USEPOLLING": "true",
121
+ **os.environ,
122
+ "LANGGRAPH_JS_REMOTE_PORT": str(REMOTE_PORT),
123
+ "LANGGRAPH_JS_GRAPH_PORT": str(GRAPH_PORT),
124
+ "LANGGRAPH_JS_GRAPH_HTTP_PORT": str(GRAPH_HTTP_PORT),
125
+ **{key: value for key, value in extra.items() if value is not None},
126
+ }
127
+ if REMOTE_SOCKET:
128
+ env["LANGGRAPH_JS_REMOTE_SOCKET"] = REMOTE_SOCKET
129
+ else:
130
+ env.pop("LANGGRAPH_JS_REMOTE_SOCKET", None)
131
+ return env
132
+
133
+
134
+ def _checkpointer_httpx_client() -> httpx.AsyncClient:
135
+ """Client for the remote server: Unix socket, or TCP localhost on Windows."""
136
+ if REMOTE_SOCKET:
137
+ return httpx.AsyncClient(
138
+ base_url="http://loopback",
139
+ limits=httpx.Limits(max_connections=1),
140
+ transport=httpx.AsyncHTTPTransport(uds=REMOTE_SOCKET, verify=SSL),
141
+ )
142
+ return httpx.AsyncClient(
143
+ base_url=f"http://localhost:{REMOTE_PORT}",
144
+ limits=httpx.Limits(max_connections=1),
145
+ transport=httpx.AsyncHTTPTransport(verify=SSL),
146
+ )
147
+
148
+
100
149
  # Per-graph context schema, prefetched at startup so get_context_jsonschema stays sync.
101
150
  _context_jsonschema_cache: dict[str, dict[str, Any] | None] = {}
102
151
 
@@ -259,11 +308,14 @@ class RemotePregel(BaseRemotePregel):
259
308
  **kwargs,
260
309
  }
261
310
 
262
- async for event in _client_stream("streamEvents", data):
263
- if event["event"] == "on_custom_event":
264
- yield CustomStreamEvent(**event)
265
- else:
266
- yield StandardStreamEvent(**event)
311
+ with loopback_grant() as token:
312
+ async for event in _client_stream(
313
+ "streamEvents", {**data, "loopback_token": token}
314
+ ):
315
+ if event["event"] == "on_custom_event":
316
+ yield CustomStreamEvent(**event)
317
+ else:
318
+ yield StandardStreamEvent(**event)
267
319
 
268
320
  async def astream_protocol_events(
269
321
  self,
@@ -292,8 +344,11 @@ class RemotePregel(BaseRemotePregel):
292
344
  **kwargs,
293
345
  }
294
346
 
295
- async for event in _client_stream("streamEvents", data):
296
- yield event
347
+ with loopback_grant() as token:
348
+ async for event in _client_stream(
349
+ "streamEvents", {**data, "loopback_token": token}
350
+ ):
351
+ yield event
297
352
 
298
353
  async def fetch_state_schema(self):
299
354
  return await _client_invoke(
@@ -540,6 +595,19 @@ class RemotePregel(BaseRemotePregel):
540
595
  return config
541
596
 
542
597
 
598
+ # A crash loop is this many JS process exits within JS_PROCESS_CRASH_LOOP_SECS,
599
+ # and takes the API down. Exits further apart, even many over the server's
600
+ # life, restart the process.
601
+ JS_PROCESS_CRASH_LOOP_EXITS = 4
602
+ JS_PROCESS_CRASH_LOOP_SECS = 600
603
+
604
+
605
+ def _is_crash_loop(exits: deque[float]) -> bool:
606
+ return (
607
+ len(exits) == exits.maxlen and exits[-1] - exits[0] < JS_PROCESS_CRASH_LOOP_SECS
608
+ )
609
+
610
+
543
611
  async def run_js_process(paths_str: str | None, watch: bool = False):
544
612
  # check if tsx is available
545
613
  tsx_path = shutil.which("tsx")
@@ -548,6 +616,7 @@ async def run_js_process(paths_str: str | None, watch: bool = False):
548
616
  "tsx not found in PATH. Please upgrade to latest LangGraph CLI to support running JS graphs."
549
617
  )
550
618
  attempt = 0
619
+ exits: deque[float] = deque(maxlen=JS_PROCESS_CRASH_LOOP_EXITS)
551
620
  while (current_task := asyncio.current_task()) and not current_task.cancelled():
552
621
  client_file = os.path.join(os.path.dirname(__file__), "client.mts")
553
622
  client_preload_file = os.path.join(
@@ -570,17 +639,10 @@ async def run_js_process(paths_str: str | None, watch: bool = False):
570
639
  try:
571
640
  process = await asyncio.create_subprocess_exec(
572
641
  *args,
573
- env={
574
- "LANGSERVE_GRAPHS": paths_str,
575
- "LANGCHAIN_CALLBACKS_BACKGROUND": "true",
576
- "NODE_ENV": "development" if watch else "production",
577
- "CHOKIDAR_USEPOLLING": "true",
578
- **os.environ,
579
- # Pin the child to the parent's resolved ports.
580
- "LANGGRAPH_JS_REMOTE_PORT": str(REMOTE_PORT),
581
- "LANGGRAPH_JS_GRAPH_PORT": str(GRAPH_PORT),
582
- "LANGGRAPH_JS_GRAPH_HTTP_PORT": str(GRAPH_HTTP_PORT),
583
- },
642
+ env=_js_graph_worker_env(
643
+ LANGSERVE_GRAPHS=paths_str,
644
+ NODE_ENV="development" if watch else "production",
645
+ ),
584
646
  )
585
647
  logger.info(
586
648
  "Started JS graphs process",
@@ -604,11 +666,11 @@ async def run_js_process(paths_str: str | None, watch: bool = False):
604
666
  await process.wait()
605
667
  raise
606
668
  except Exception:
607
- if attempt >= 3:
669
+ exits.append(time.monotonic())
670
+ if _is_crash_loop(exits):
608
671
  raise
609
- else:
610
- logger.warning(f"Retrying JS process {3 - attempt} more times...")
611
- attempt += 1
672
+ logger.warning("Restarting JS process", recent_exits=len(exits))
673
+ attempt += 1
612
674
 
613
675
 
614
676
  async def run_js_http_process(
@@ -622,6 +684,7 @@ async def run_js_http_process(
622
684
  )
623
685
 
624
686
  attempt = 0
687
+ exits: deque[float] = deque(maxlen=JS_PROCESS_CRASH_LOOP_EXITS)
625
688
  while (current_task := asyncio.current_task()) and not current_task.cancelled():
626
689
  client_file = os.path.join(os.path.dirname(__file__), "client.http.mts")
627
690
  args = ("tsx", "watch", client_file) if watch else ("tsx", client_file)
@@ -669,12 +732,12 @@ async def run_js_http_process(
669
732
  await process.wait()
670
733
  raise
671
734
  except Exception:
672
- if attempt >= 3:
735
+ exits.append(time.monotonic())
736
+ if _is_crash_loop(exits):
673
737
  logger.exception("JS HTTP process failed")
674
738
  raise
675
- else:
676
- logger.warning(f"Retrying JS HTTP process {3 - attempt} more times...")
677
- attempt += 1
739
+ logger.warning("Restarting JS HTTP process", recent_exits=len(exits))
740
+ attempt += 1
678
741
 
679
742
 
680
743
  _BAD_SURROGATE_RE = re.compile(r"\\u[dD][89a-fA-F][0-9a-fA-F]{2}")
@@ -718,6 +781,11 @@ async def _get_passthrough_checkpointer():
718
781
  from langgraph_api import _checkpointer as api_checkpointer # noqa: PLC0415
719
782
 
720
783
  checkpointer = await api_checkpointer.get_checkpointer()
784
+ # The in-memory runtime returns the saver Python graphs share, so swap the
785
+ # serde on a copy; the copy still shares the saved data. Other runtimes
786
+ # return a fresh saver per call.
787
+ if isinstance(checkpointer, InMemorySaver):
788
+ checkpointer = copy.copy(checkpointer)
721
789
  # This checkpointer does not attempt to revive LC-objects.
722
790
  # Instead, it will pass through the JSON values as-is.
723
791
  checkpointer.serde = PassthroughSerialiser()
@@ -975,20 +1043,125 @@ async def run_remote_checkpointer():
975
1043
  ]
976
1044
  )
977
1045
 
978
- server = NoSignalServer(
979
- uvicorn.Config(
980
- remote,
981
- port=REMOTE_PORT,
982
- # We need to _explicitly_ set these values in order
983
- # to avoid reinitialising the logger, which removes
984
- # the structlog logger setup before.
985
- # See: https://github.com/encode/uvicorn/blob/8f4c8a7f34914c16650ebd026127b96560425fde/uvicorn/config.py#L357-L393
986
- log_config=None,
987
- log_level=None,
988
- access_log=True,
1046
+ log_kwargs = {
1047
+ # We need to _explicitly_ set these values in order
1048
+ # to avoid reinitialising the logger, which removes
1049
+ # the structlog logger setup before.
1050
+ # See: https://github.com/encode/uvicorn/blob/8f4c8a7f34914c16650ebd026127b96560425fde/uvicorn/config.py#L357-L393
1051
+ "log_config": None,
1052
+ "log_level": None,
1053
+ "access_log": True,
1054
+ }
1055
+ if REMOTE_SOCKET:
1056
+ with contextlib.suppress(FileNotFoundError):
1057
+ os.unlink(REMOTE_SOCKET)
1058
+ server = NoSignalServer(
1059
+ uvicorn.Config(loopback_to_api(remote), uds=REMOTE_SOCKET, **log_kwargs)
989
1060
  )
990
- )
991
- await server.serve()
1061
+ else:
1062
+ # Windows: checkpointer/store over TCP, no /loopback client.
1063
+ server = NoSignalServer(
1064
+ uvicorn.Config(remote, host="127.0.0.1", port=REMOTE_PORT, **log_kwargs)
1065
+ )
1066
+ try:
1067
+ await server.serve()
1068
+ finally:
1069
+ if REMOTE_SOCKET:
1070
+ with contextlib.suppress(FileNotFoundError):
1071
+ os.unlink(REMOTE_SOCKET)
1072
+
1073
+
1074
+ LOOPBACK_PREFIX = "/loopback"
1075
+ """Path on the Unix remote socket under which JS graphs reach this server's API."""
1076
+
1077
+ LOOPBACK_RUN_HEADER = b"x-langgraph-loopback-run"
1078
+ """Header the JS worker sends so loopback calls run as the stream's user."""
1079
+
1080
+ _LOOPBACK_GRANTS: dict[str, Auth.types.BaseAuthContext | None] = {}
1081
+ """Live JS graph streams, and the auth context each stream's loopback calls use."""
1082
+
1083
+ _MISSING = object()
1084
+
1085
+
1086
+ @contextlib.contextmanager
1087
+ def loopback_grant() -> Iterator[str]:
1088
+ """Map one JS graph stream to the run's auth context.
1089
+
1090
+ Python graphs' in-process calls keep the run's AuthContext ContextVar
1091
+ because they stay on the same asyncio task. A JS graph's call arrives on
1092
+ the Unix-socket uvicorn task, where that var is empty, so the stream
1093
+ carries this token and loopback_to_api restores the context.
1094
+ """
1095
+ token = secrets.token_urlsafe(32)
1096
+ _LOOPBACK_GRANTS[token] = get_auth_ctx()
1097
+ try:
1098
+ yield token
1099
+ finally:
1100
+ _LOOPBACK_GRANTS.pop(token, None)
1101
+
1102
+
1103
+ def _loopback_api_path(path: str) -> str | None:
1104
+ """Return the API path for a `/loopback/...` request, or `None` if it isn't one."""
1105
+ if path == LOOPBACK_PREFIX:
1106
+ return "/"
1107
+ if not path.startswith(f"{LOOPBACK_PREFIX}/"):
1108
+ return None
1109
+ return path.removeprefix(LOOPBACK_PREFIX) or "/"
1110
+
1111
+
1112
+ def _loopback_grant(
1113
+ scope: types.Scope, grants: Mapping[str, Auth.types.BaseAuthContext | None]
1114
+ ) -> Auth.types.BaseAuthContext | None | object:
1115
+ """The run's auth context, or _MISSING if this isn't a live loopback call."""
1116
+ token: str | None = None
1117
+ for name, value in scope.get("headers") or ():
1118
+ if name == LOOPBACK_RUN_HEADER:
1119
+ token = value.decode()
1120
+ break
1121
+ if token is None or token not in grants:
1122
+ return _MISSING
1123
+ return grants[token]
1124
+
1125
+
1126
+ def loopback_to_api(
1127
+ remote: types.ASGIApp,
1128
+ grants: Mapping[str, Auth.types.BaseAuthContext | None] | None = None,
1129
+ ) -> types.ASGIApp:
1130
+ """Serve JS graphs' SDK calls in process, the way Python graphs' `get_client()` does.
1131
+
1132
+ A Python graph's SDK client reaches the API in memory at `root_path="/noauth"`,
1133
+ with its run's auth context still set. A JS graph sends the same calls over
1134
+ the Unix remote socket; a live grant header restores that run's user
1135
+ before the API handler runs. Unix only. Windows does not mount this.
1136
+ """
1137
+ if grants is None:
1138
+ grants = _LOOPBACK_GRANTS
1139
+
1140
+ async def app(scope: types.Scope, receive: types.Receive, send: types.Send) -> None:
1141
+ api_path = (
1142
+ _loopback_api_path(scope.get("path", ""))
1143
+ if scope["type"] == "http"
1144
+ else None
1145
+ )
1146
+ ctx = _loopback_grant(scope, grants) if api_path is not None else _MISSING
1147
+ if api_path is None or ctx is _MISSING:
1148
+ await remote(scope, receive, send)
1149
+ return
1150
+ from langgraph_api.server import app as api # noqa: PLC0415
1151
+
1152
+ scope = {
1153
+ **scope,
1154
+ "root_path": "/noauth",
1155
+ "path": api_path,
1156
+ "raw_path": api_path.encode(),
1157
+ }
1158
+ reset = AuthContext.set(cast("Auth.types.BaseAuthContext | None", ctx))
1159
+ try:
1160
+ await api(scope, receive, send)
1161
+ finally:
1162
+ AuthContext.reset(reset)
1163
+
1164
+ return app
992
1165
 
993
1166
 
994
1167
  class DisableHttpxLoggingContextManager(contextlib.AbstractContextManager):
@@ -1023,11 +1196,7 @@ async def wait_until_js_ready(
1023
1196
  limits=httpx.Limits(max_connections=1),
1024
1197
  transport=httpx.AsyncHTTPTransport(verify=SSL),
1025
1198
  ) as graph_client,
1026
- httpx.AsyncClient(
1027
- base_url=f"http://localhost:{REMOTE_PORT}",
1028
- limits=httpx.Limits(max_connections=1),
1029
- transport=httpx.AsyncHTTPTransport(verify=SSL),
1030
- ) as checkpointer_client,
1199
+ _checkpointer_httpx_client() as checkpointer_client,
1031
1200
  ):
1032
1201
  attempt = 0
1033
1202
  graph_ready = False
@@ -1088,11 +1257,7 @@ async def js_healthcheck():
1088
1257
  limits=httpx.Limits(max_connections=1),
1089
1258
  transport=httpx.AsyncHTTPTransport(verify=SSL),
1090
1259
  ) as graph_client,
1091
- httpx.AsyncClient(
1092
- base_url=f"http://localhost:{REMOTE_PORT}",
1093
- limits=httpx.Limits(max_connections=1),
1094
- transport=httpx.AsyncHTTPTransport(verify=SSL),
1095
- ) as checkpointer_client,
1260
+ _checkpointer_httpx_client() as checkpointer_client,
1096
1261
  ):
1097
1262
  graph_passed = False
1098
1263
  try:
langgraph_api/metadata.py CHANGED
@@ -155,6 +155,12 @@ def _lang_usage_metadata() -> tuple[dict[str, str], dict[str, int]]:
155
155
  return tags, measures
156
156
 
157
157
 
158
+ def _billing_channel() -> str:
159
+ # A self-hosted LangSmith bills API-key usage under its own license unless the
160
+ # server already reports it under a license key.
161
+ return "license_key" if LANGGRAPH_CLOUD_LICENSE_KEY else "api_key"
162
+
163
+
158
164
  def incr_runs(*, incr: int = 1) -> None:
159
165
  global RUN_COUNTER
160
166
  RUN_COUNTER += incr
@@ -283,6 +289,7 @@ async def metadata_loop() -> None:
283
289
  langchain_payload = {
284
290
  **base_payload,
285
291
  "api_key": LANGSMITH_CONTROL_PLANE_API_KEY,
292
+ "billing_channel": _billing_channel(),
286
293
  }
287
294
  submissions_attempted.append("langchain")
288
295
  try:
@@ -233,15 +233,12 @@ async def create_valid_run(
233
233
  detail="Cannot specify both configurable and context. Prefer setting context alone. Context was introduced in LangGraph 0.6.0 and is the long term planned replacement for configurable.",
234
234
  )
235
235
 
236
- # ``tools`` and ``lifecycle`` are emitted only by the protocol v2
237
- # streaming pipeline (``stream_v2.py`` / ``StreamMessagesHandlerV2``
238
- # plus the registered stream transformers). Accepting them on legacy
239
- # streaming routes would either no-op (``lifecycle``, never emitted
240
- # without a transformer) or leak v2-shaped events into v1 consumers
241
- # (``tools``). Reject up front so callers get a clear 422 instead of
242
- # a silently empty stream.
236
+ # ``lifecycle`` is emitted only by the protocol v2 streaming pipeline
237
+ # (``stream_v2.py`` plus the registered stream transformers), so legacy
238
+ # routes reject it up front instead of returning a silently empty stream.
239
+ # ``tools`` is accepted on legacy routes and dropped in ``stream.py``.
243
240
  if not configurable.get(EVENT_STREAMING_V2_CONFIG_KEY):
244
- v2_only = {"tools", "lifecycle"}.intersection(stream_mode)
241
+ v2_only = {"lifecycle"}.intersection(stream_mode)
245
242
  if v2_only:
246
243
  raise HTTPException(
247
244
  status_code=422,
langgraph_api/stream.py CHANGED
@@ -265,6 +265,11 @@ async def astream_state(
265
265
  # ``capabilities.py`` documents this behavior — keep them in sync.
266
266
  stream_modes_set.discard("tools")
267
267
  stream_modes_set.discard("lifecycle")
268
+ elif not event_streaming_v2_run:
269
+ # The v1 React ``useStream`` hook asks for ``tools`` whenever an app
270
+ # listens for tool events, but it cannot parse the events graphs emit
271
+ # for this mode and throws on the first one.
272
+ stream_modes_set.discard("tools")
268
273
  if "debug" not in stream_modes_set:
269
274
  stream_modes_set.add("debug")
270
275
  if "messages-tuple" in stream_modes_set and not is_remote_pregel:
@@ -738,23 +743,42 @@ async def _consume_inline(
738
743
  This preserves natural backpressure: each event must be published before the
739
744
  next one is consumed.
740
745
  """
746
+ publishing = True
741
747
  async with aclosing(stream):
742
748
  try:
743
749
  async for mode, payload in stream:
750
+ if not publishing:
751
+ continue
744
752
  event_type = get_stream_event_type(mode)
745
753
  payload_bytes = await run_in_executor(None, json_dumpb, payload)
746
- is_resumable = resumable and mode.split("|")[0] in stream_modes
747
- with reporter.track_latency_ms(
748
- LATENCY_STREAM_PUBLISH,
749
- attributes={"stream_event_type": event_type},
750
- ):
751
- await Runs.Stream.publish(
752
- run_id,
753
- mode,
754
- payload_bytes,
755
- thread_id=thread_id,
756
- resumable=is_resumable,
754
+ is_resumable = resumable and event_type.split("/")[0] in stream_modes
755
+ try:
756
+ with reporter.track_latency_ms(
757
+ LATENCY_STREAM_PUBLISH,
758
+ attributes={"stream_event_type": event_type},
759
+ ):
760
+ await Runs.Stream.publish(
761
+ run_id,
762
+ mode,
763
+ payload_bytes,
764
+ thread_id=thread_id,
765
+ resumable=is_resumable,
766
+ )
767
+ except StreamPublishException as e:
768
+ # Stop publishing for the rest of the run: transient errors
769
+ # were already retried for up to a minute, and retrying every
770
+ # later event would stall the graph again each time. The run
771
+ # still finishes, and the worker records its real outcome.
772
+ publishing = False
773
+ await logger.aerror(
774
+ "Stream publish failed, finishing the run without streaming",
775
+ run_id=str(run_id),
776
+ thread_id=str(thread_id),
777
+ error_type=type(e).__name__,
778
+ error=str(e),
757
779
  )
780
+ reporter.inc_counter(COUNTER_STREAMING_DATA_LOSS)
781
+ continue
758
782
  reporter.record_histogram(
759
783
  HISTOGRAM_STREAM_DATA_SIZE,
760
784
  float(len(payload_bytes)),
@@ -763,26 +787,6 @@ async def _consume_inline(
763
787
  except Exception as e:
764
788
  if isinstance(e, ExceptionGroup):
765
789
  e = e.exceptions[0]
766
- if isinstance(e, StreamPublishException):
767
- # We swallow this and report the run as complete, so the worker
768
- # never sees it and cannot surface it. Emit it to the client here.
769
- with suppress(Exception):
770
- await Runs.Stream.publish(
771
- run_id,
772
- "error",
773
- await run_in_executor(None, json_dumpb, e),
774
- thread_id=thread_id,
775
- resumable=resumable,
776
- )
777
- await logger.aerror(
778
- "Suppressing stream consume exception",
779
- run_id=str(run_id),
780
- thread_id=str(thread_id),
781
- error_type=type(e).__name__,
782
- error=str(e),
783
- )
784
- reporter.inc_counter(COUNTER_STREAMING_DATA_LOSS)
785
- return
786
790
  raise e
787
791
 
788
792
 
@@ -858,7 +862,7 @@ async def _consume_async_queue(
858
862
  async for mode, payload in stream:
859
863
  event_type = get_stream_event_type(mode)
860
864
  payload_bytes = await run_in_executor(None, json_dumpb, payload)
861
- is_resumable = resumable and mode.split("|")[0] in stream_modes
865
+ is_resumable = resumable and event_type.split("/")[0] in stream_modes
862
866
  await _enqueue((mode, payload_bytes, event_type, is_resumable))
863
867
 
864
868
  except Exception as e:
langgraph_api/worker.py CHANGED
@@ -296,6 +296,15 @@ async def worker(
296
296
  "Background run timed out. To increase the timeout, set the BG_JOB_TIMEOUT_SECS environment variable (integer, defaults to 86400).",
297
297
  **log_info,
298
298
  )
299
+ # Without it, every stream shows a timed-out run as a success.
300
+ with suppress(Exception):
301
+ await Runs.Stream.publish(
302
+ run_id,
303
+ "error",
304
+ json_dumpb(exception),
305
+ thread_id=run["thread_id"],
306
+ resumable=resumable,
307
+ )
299
308
  if not temporary:
300
309
  await Threads.set_joint_status(
301
310
  conn,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-api
3
- Version: 0.18.0.dev10
3
+ Version: 0.18.0.dev12
4
4
  Author-email: Will Fu-Hinthorn <will@langchain.dev>, Josh Rogers <josh@langchain.dev>, Parker Rule <parker@langchain.dev>
5
5
  License: Elastic-2.0
6
6
  License-File: LICENSE
@@ -24,7 +24,7 @@ Requires-Dist: langsmith[otel]>=0.6.3
24
24
  Requires-Dist: mcp<3,>=2.3.0
25
25
  Requires-Dist: opentelemetry-api>=0.0.1
26
26
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=0.0.1
27
- Requires-Dist: opentelemetry-exporter-prometheus<0.64,>=0.63b1
27
+ Requires-Dist: opentelemetry-exporter-prometheus<0.67,>=0.63b1
28
28
  Requires-Dist: opentelemetry-sdk>=0.0.1
29
29
  Requires-Dist: orjson>=3.9.7
30
30
  Requires-Dist: packaging>=23.2
@@ -1,4 +1,4 @@
1
- langgraph_api/__init__.py,sha256=-bIIVjTdPEAZN8A072q6cjv1tdFUQmeHn9FYvYC-Q14,29
1
+ langgraph_api/__init__.py,sha256=4-U3UVsECE8ZPJ6VZOCr58LOX8rIvYtmAXI_APYqlGo,29
2
2
  langgraph_api/_factory_utils.py,sha256=_FqW0olMu2PGLsnRq0ylt3a4jDCe12YIFHExMXI6Nyo,5976
3
3
  langgraph_api/asgi_transport.py,sha256=XApY3lIWBZTMbbsl8dDJzl0cLGirmAGE0SifqZUnXvs,11896
4
4
  langgraph_api/asyncio.py,sha256=-p-Vx89TnErypdPcWb-2EdbsSvhdIdHfriCFG7UP7ls,12444
@@ -10,12 +10,12 @@ langgraph_api/cron_scheduler.py,sha256=H1jPymC68A1MYyjfdApX4-hELiGBlJpGgSOsN8qhc
10
10
  langgraph_api/drain.py,sha256=MLUm3fDq3R8yxrZ-1W3TeCfhNzgNSzUA7TvB-D6K2_s,6108
11
11
  langgraph_api/errors.py,sha256=x6yhf0JrgSNweFRJw7l3ZK8De4T76d2v6MRvoDU_C-U,3312
12
12
  langgraph_api/feature_flags.py,sha256=NB1X5jO19c40hI3WDxGk9qwD6EZUROO0ooFSd1QTniM,3985
13
- langgraph_api/graph.py,sha256=uQaC_yOWRFZIZwQt5WGdjnVW5iB0WaaBniUcUV82Y5Q,40594
13
+ langgraph_api/graph.py,sha256=aWj1o4bYU2prIWpAVAWOH7CHbjvZKYfWV2CtF-hzPko,40697
14
14
  langgraph_api/http.py,sha256=7hPxKbj-xoAKcm7iucBpT5nM_hXOgGVCPbBsCD693Cw,6977
15
15
  langgraph_api/http_metrics.py,sha256=etxbZNmYxdb58DVLNkHP7S-N6njXPTiQh2OWKMaIZi8,5336
16
16
  langgraph_api/http_metrics_utils.py,sha256=sjxF7SYGTzY0Wz_G0dzatsYNnWr31S6ujej4JmBG2yo,866
17
17
  langgraph_api/logging.py,sha256=PeApafcPxox03rcpjEU5rlSzDkxJ3gff52XC34uZjl0,6972
18
- langgraph_api/metadata.py,sha256=OjFF_N4eoGjzCwsq7SS8aUVF39K1Jw2N1UBnpQ9f-X4,11681
18
+ langgraph_api/metadata.py,sha256=j4FJvZRJP5tPi6uBcmLPaI4LrrhIZ7Df1ccoTYY_no4,11976
19
19
  langgraph_api/metrics_collector.py,sha256=ALUyirKew3uqtXVrCwpBc7wCM_viNhT13wZVMUVyva0,8495
20
20
  langgraph_api/metrics_otlp.py,sha256=JucxN4i4qn79jP4td0A_T6ycFpLP1G7GThvQTczfpAA,30850
21
21
  langgraph_api/operator_routes.py,sha256=s4HfkNUdMvgyYPb8L8yM3Z8UJQbGkJrVFMiH5HJp_S4,8389
@@ -32,13 +32,13 @@ langgraph_api/server.py,sha256=_DG7dCL2wtlaXdppTce2e0k8PunCcSRj79ncQfqmK_Q,10305
32
32
  langgraph_api/sse.py,sha256=cChZ7raQUHp8p5BreE_5wMBR8lFO0n7746sV8_HQOrc,4822
33
33
  langgraph_api/state.py,sha256=Py8HG8k7u0FRAj4Ok89_uR9GerkyeFm90gSMTbWWQX0,8356
34
34
  langgraph_api/store.py,sha256=OXcdQSVCDKOzfS8S2aJHXbfuI5iYRoQBiW9WYKnCyx8,7122
35
- langgraph_api/stream.py,sha256=SMrditrbTJ7QtKW-ThpYAPiVsQD-ot_tQeGJIp9PKGI,36519
35
+ langgraph_api/stream.py,sha256=XnXqtf628r5EUltaV9JtKlka_40YO38kkYUNUTz4hcQ,36891
36
36
  langgraph_api/stream_v2.py,sha256=AieeCAXQsthy9n6crCibMB7unbN1W_o1xkpJ2v11RSY,10822
37
37
  langgraph_api/traceblock.py,sha256=GhgAtLhAQ8Z8LHtRQq3par-rfPpRBQp2Q7WBACJq1NI,677
38
38
  langgraph_api/tracing_session.py,sha256=gmfv-BW1XRar6suVur1P4rTVKWkjElDEb2hhkBduWOs,728
39
39
  langgraph_api/validation.py,sha256=_fmqjbETfsrc4KKJLtfhVjELD-1WzEVrK6G1pfDrmtk,14930
40
40
  langgraph_api/webhook.py,sha256=qXEtkE6orek2POeOQmPRsEarJgXIYp-LBrZB-OwITxc,9572
41
- langgraph_api/worker.py,sha256=o-gpQDRnAlNXnH5F69DOVQgI57nCPwt5oL2B22vrj_k,22058
41
+ langgraph_api/worker.py,sha256=GjHJ1ig--xdCBg065W1JAfXSzgt3Ec-0iX7lbU31CIM,22457
42
42
  langgraph_api/worker_runtime.py,sha256=zzVW7A4vHyJE6DRxdjRZIa2U334Eh5oO157SyWmQ3G8,11861
43
43
  langgraph_api/_checkpointer/__init__.py,sha256=ofJTJLGy7Hsuzhj-2dpfDvrDloM0BzlhTzvZOdR9K8U,2223
44
44
  langgraph_api/_checkpointer/_adapter.py,sha256=idGYL-FwyVtUXcmhvfYboARfQAkoj0aFD40L5h9N7F8,22289
@@ -53,7 +53,7 @@ langgraph_api/api/openapi.py,sha256=hS4yhfJfWAYilm0Pv8iC08bK7CMDG4PIVc-77y9Y7Ts,
53
53
  langgraph_api/api/profile.py,sha256=CA1ZkHALOuP8orYTICnEhcG_JnnA2wnyjbWyeb117jA,3455
54
54
  langgraph_api/api/runs.py,sha256=0ogQnEQyKeVTuEUY6UD4KMShUA6Qq7_Zyp0Z70zGSzs,32577
55
55
  langgraph_api/api/store.py,sha256=5d1GOxsVDK-8PPngYG2rjYF_B2qNevDvPos-nB8YPCI,7710
56
- langgraph_api/api/threads.py,sha256=OQSoveEmKE-mErF4AbvivHpTexLE_BJ_raGDgH8YZ44,23031
56
+ langgraph_api/api/threads.py,sha256=JT7TpAs7zJBokxzGl_DmUNEnfAVfTRdJRiLwuP4D1EM,22916
57
57
  langgraph_api/api/ui.py,sha256=A1cRxaMK_4pb192mrZ_LMxLSFb8NriYwwvjeDdNDD7k,2803
58
58
  langgraph_api/api/a2ui_schemas/README.md,sha256=fZCc9rSK-qeUHsbJ17EYxMZjS3yUrYQdoQLX7ltgP8M,298
59
59
  langgraph_api/api/a2ui_schemas/client_capabilities.json,sha256=3YAHlRfeOn9dUH6uAek1loCL4caeSUuQzhrtWKWW9nU,1906
@@ -100,15 +100,15 @@ langgraph_api/event_streaming/types.py,sha256=RyZqfqgH-jmmmmAFQj5f6nH9M1rGK93zVG
100
100
  langgraph_api/executor/__init__.py,sha256=mZBT8HSqe9pMlBpE7sw6DMI4ULB4xwNPWGkN9zp1B7k,302
101
101
  langgraph_api/executor/__main__.py,sha256=fKZs36-aLdXMZrRrXOneBJNV88maiNwuToRCIoyHzmQ,12091
102
102
  langgraph_api/grpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
103
- langgraph_api/grpc/client.py,sha256=ypSgZWclK7OCKLGW8gD_LZwwgsgQEahJqc8mBWF8Rug,15578
103
+ langgraph_api/grpc/client.py,sha256=L-FM_NrfO-pJHIk71g-rbIcR10tzCzJPv9VOszZoIkc,16100
104
104
  langgraph_api/grpc/server.py,sha256=Fknr6BJbdSK38wF7B1vPDplK25KB_UfMB9Fp1er6DPU,8366
105
105
  langgraph_api/grpc/generated/core_api_pb2.pyi,sha256=0PEzVh8s6rqHq52ekhqPaMI_TOsP4_xuUUv9_XS7r1w,54758
106
106
  langgraph_api/grpc/ops/__init__.py,sha256=KQL4w0xjvkCVz4j770QQVuP7MQh6qPaTZnxlO3KmoiA,18754
107
107
  langgraph_api/grpc/ops/assistants.py,sha256=hLDZrAahpGSjzWNzr8nQidTW_4MoCR6sZy5VWB7oRxo,14080
108
108
  langgraph_api/grpc/ops/cache.py,sha256=MoPAmSuOwbt2Cud1YZJm_YJeaDHPFy1IiV4m0PDImgg,1399
109
109
  langgraph_api/grpc/ops/crons.py,sha256=aDlvNVYIfSP8gtr5NptLiAj-bfMtJOuMWYIA2NifWm0,19713
110
- langgraph_api/grpc/ops/runs.py,sha256=GPTNBqW97zX00MDeS1QtRaSB8bz8GZ_vfYRZxY7LEIg,44500
111
- langgraph_api/grpc/ops/threads.py,sha256=J0QUNZqHW-7ZHVgay1IbNUj5vTDv9iFcnJbZajvbqd4,51950
110
+ langgraph_api/grpc/ops/runs.py,sha256=LM4tbxTUZM_HvTuBQ7AkhWa95Zza4jWw2ULcnNgeUoY,44747
111
+ langgraph_api/grpc/ops/threads.py,sha256=4XPd4jzfKp77KY5BY6W0nbgXj2cK7wKY7uhON12zMUg,52153
112
112
  langgraph_api/grpc/servicers/__init__.py,sha256=l8yqTA_gMbIj0xHw5-RZYo0JbjL-EpFD8RxPMgW_-bA,422
113
113
  langgraph_api/grpc/servicers/checkpointer.py,sha256=kn9J3HQEmqggtOGiDxpEAeBVkVi4qDCJikj2bXo5k4g,10543
114
114
  langgraph_api/grpc/servicers/encryption.py,sha256=ddAjRZ9btWBf4MPa7yd793japdCEi-DfEPi1Ynt8gzk,10563
@@ -119,11 +119,11 @@ langgraph_api/js/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
119
119
  langgraph_api/js/base.py,sha256=CJihwc51MwOVkis80f8zudRa1fQz_5jrom4rY8trww8,1133
120
120
  langgraph_api/js/build.mts,sha256=--Kfm4NKDhMljmv8fVvRR0VeUsBKiWch6qXpBoKmrvU,3445
121
121
  langgraph_api/js/client.http.mts,sha256=ARlTJURLJamgvyaPlBCYEAHFnXQnPpzzUXWKwXiebnw,6011
122
- langgraph_api/js/client.mts,sha256=vEXetaza6XmCH2lFyXueb-iYbrHhWrETf8UpZp5vdwM,40316
122
+ langgraph_api/js/client.mts,sha256=oM3ctxVJ8YoxEDmjWAlZqVDv28k9TVUSD0dezsA58y0,41978
123
123
  langgraph_api/js/errors.py,sha256=Cm1TKWlUCwZReDC5AQ6SgNIVGD27Qov2xcgHyf8-GXo,361
124
- langgraph_api/js/global.d.ts,sha256=j4GhgtQSZ5_cHzjSPcHgMJ8tfBThxrH-pUOrrJGteOU,196
124
+ langgraph_api/js/global.d.ts,sha256=LHCvS5NFvr0KKJUFzqIGL-CdBC4CgFIqif1im1r9xAg,237
125
125
  langgraph_api/js/package.json,sha256=e2FzqGICzYOtz3CsncKR8o4trBP6GMCZUU8RdbWMvSI,1372
126
- langgraph_api/js/remote.py,sha256=eYYbTmZprmNc5cF_qCrwQZ2LpTWUGAMVn84hS2--w60,53492
126
+ langgraph_api/js/remote.py,sha256=WR2hss2HL2j3FMWqfUTqleK_LXGSCNets1LH6ub8xMQ,59394
127
127
  langgraph_api/js/schema.py,sha256=M4fLtr50O1jck8H1hm_0W4cZOGYGdkrB7riLyCes4oY,438
128
128
  langgraph_api/js/sse.py,sha256=IfcEpknyZeqqjYTOWRdijc6o2EBfatbWCZarGQQ0SQI,4088
129
129
  langgraph_api/js/traceblock.mts,sha256=QtGSN5VpzmGqDfbArrGXkMiONY94pMQ5CgzetT_bKYg,761
@@ -149,7 +149,7 @@ langgraph_api/middleware/http_logger.py,sha256=79iMlqKdSP5sS6G-iuEAHl2h0Z2ygYDxs
149
149
  langgraph_api/middleware/private_network.py,sha256=eQEzWI8epBNUCiNsMu9O27ofHBQ45M0p2OZy5YdUYos,2097
150
150
  langgraph_api/middleware/request_id.py,sha256=-p230Q5jDJAJLmSZRqQvB4dFFkJS9B4Vwg6pUgQtI24,1259
151
151
  langgraph_api/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
152
- langgraph_api/models/run.py,sha256=MW7Q17G3veh3WCPwwyDVA9qkk-ih8MPo3QKqY-_Wyho,15795
152
+ langgraph_api/models/run.py,sha256=XHlx9m3Odr61CGuHjNdAqJEU_B325rxRn-98AeAgV4M,15625
153
153
  langgraph_api/timing/__init__.py,sha256=UDMuNBmc5NqX0m5T-9PUFioCa1lNvd4Wv4juPIBRb-w,877
154
154
  langgraph_api/timing/profiler.py,sha256=_n0qJYJMjCGo95RDuH5z8FIzePGNSNbnm3cvGmcgn6A,6186
155
155
  langgraph_api/timing/startup.py,sha256=1Jp4-tquBhiE5glWQhXGX5XS2_006myJXHHZMOi15ng,6161
@@ -182,11 +182,11 @@ langgraph_runtime/store.py,sha256=7mowndlsIroGHv3NpTSOZDJR0lCuaYMBoTnTrewjslw,11
182
182
  LICENSE,sha256=SCVQGLQfwOllsRFa9-Z3m8IYu4pnR9Vh2oANUCJiKqI,3860
183
183
  hatch_build.py,sha256=Nn9N-EFfVqmH5q3iCUrhqX6zc7qT1jLI3gWSBN2xgYQ,1738
184
184
  logging.json,sha256=sBy8HDDPuucpLbLUD6e8t5fsiQjJoklZsbVHi2qQ7aQ,367
185
- openapi.json,sha256=-3ATf5OgeAsoctzGUcZRXFyPBAHz49VCNqKaWt8yvQc,228738
185
+ openapi.json,sha256=AX8OyPP9M3JCJ4J09DU0KdmPxTTlHl12YLm8e-u7Fg4,229573
186
186
  langgraph_grpc_common/__init__.py,sha256=sO7jFHekQxt6zZPhTPZAWLYLB_S8w7-VuJ7NyWFlFNo,97
187
187
  langgraph_grpc_common/checkpointer.py,sha256=FwY4a4zIdRFy_Fw2jd_dxIhBpWBIMi5kJqye2VkzzUg,16505
188
188
  langgraph_grpc_common/serde.py,sha256=8q290nZ6TdxbEhCTzwEs44XGi2b-3pfrUeMYY5WGlTY,6179
189
- langgraph_grpc_common/store.py,sha256=ae34gPQS260oBh2hcNLzI7-c1xKMs6FVvsJExqFRbhk,4743
189
+ langgraph_grpc_common/store.py,sha256=yyckUNGX9-2ZoU1XiuhPHpK600MNPUWV-NYoK1BUzHA,5063
190
190
  langgraph_grpc_common/conversion/__init__.py,sha256=pLsOrRtmF9YkgmCPhcfym6lYOB7PCJTRItyo1YK_AT8,208
191
191
  langgraph_grpc_common/conversion/_compat.py,sha256=MIEo69I7tfaPskunPGwBCKheksLwxpw-z-4O3hvO9og,4010
192
192
  langgraph_grpc_common/conversion/checkpoint.py,sha256=aBiQ_73PA4HUFFzgnJ9FKhWuqUhPHjyN9aPKrqd7LNs,13760
@@ -260,8 +260,8 @@ langgraph_grpc_common/proto/store_pb2.py,sha256=VWJ7wtzL9pVmZZKbxMzOzCoiFn58bB2g
260
260
  langgraph_grpc_common/proto/store_pb2.pyi,sha256=atXejijuYEb9tE0stGQdZcfA9ucMFj-ytTMwvkxwviM,29322
261
261
  langgraph_grpc_common/proto/store_pb2_grpc.py,sha256=Kyt8eV2NXkjh3be_BYytGdD19kNf74961cruonMvCrw,3300
262
262
  langgraph_grpc_common/proto/store_pb2_grpc.pyi,sha256=oS1h2cDoq2OjyM8xazcmc8LwRDC5oCAD0zto2QUmPQw,2027
263
- langgraph_api-0.18.0.dev10.dist-info/METADATA,sha256=p4UUfXDSWLBDQK7btVt4IGOL7teL42UEJs6_uJ2Hxpo,5940
264
- langgraph_api-0.18.0.dev10.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
265
- langgraph_api-0.18.0.dev10.dist-info/entry_points.txt,sha256=hGedv8n7cgi41PypMfinwS_HfCwA7xJIfS0jAp8htV8,78
266
- langgraph_api-0.18.0.dev10.dist-info/licenses/LICENSE,sha256=SCVQGLQfwOllsRFa9-Z3m8IYu4pnR9Vh2oANUCJiKqI,3860
267
- langgraph_api-0.18.0.dev10.dist-info/RECORD,,
263
+ langgraph_api-0.18.0.dev12.dist-info/METADATA,sha256=R4amunHvob9GwNDTv0iaNt8y_qlFejxLKP_JLQDCL0s,5940
264
+ langgraph_api-0.18.0.dev12.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
265
+ langgraph_api-0.18.0.dev12.dist-info/entry_points.txt,sha256=hGedv8n7cgi41PypMfinwS_HfCwA7xJIfS0jAp8htV8,78
266
+ langgraph_api-0.18.0.dev12.dist-info/licenses/LICENSE,sha256=SCVQGLQfwOllsRFa9-Z3m8IYu4pnR9Vh2oANUCJiKqI,3860
267
+ langgraph_api-0.18.0.dev12.dist-info/RECORD,,
@@ -62,8 +62,15 @@ class GrpcStore(AsyncBatchedBaseStore):
62
62
  if self._stub is None:
63
63
  with self._channel_lock:
64
64
  if self._stub is None:
65
- # Internal loopback to core-server; matches langgraph_api.grpc.client.
66
- self._channel = grpc.aio.insecure_channel(self._address)
65
+ # Internal loopback to core-server, reconnecting as fast as
66
+ # langgraph_api.grpc.client does (which explains the values).
67
+ self._channel = grpc.aio.insecure_channel(
68
+ self._address,
69
+ options=[
70
+ ("grpc.initial_reconnect_backoff_ms", 100),
71
+ ("grpc.max_reconnect_backoff_ms", 1000),
72
+ ],
73
+ )
67
74
  self._stub = StoreStub(self._channel)
68
75
  return self._stub
69
76
 
openapi.json CHANGED
@@ -1666,6 +1666,28 @@
1666
1666
  },
1667
1667
  "name": "stream_modes",
1668
1668
  "in": "query"
1669
+ },
1670
+ {
1671
+ "required": false,
1672
+ "schema": {
1673
+ "anyOf": [
1674
+ {
1675
+ "type": "string",
1676
+ "enum": ["lifecycle", "run_modes", "state_update"]
1677
+ },
1678
+ {
1679
+ "type": "array",
1680
+ "items": {
1681
+ "type": "string",
1682
+ "enum": ["lifecycle", "run_modes", "state_update"]
1683
+ }
1684
+ }
1685
+ ],
1686
+ "title": "Stream Mode",
1687
+ "description": "Alias of `stream_modes`, the name the SDKs send. Ignored when `stream_modes` is set."
1688
+ },
1689
+ "name": "stream_mode",
1690
+ "in": "query"
1669
1691
  }
1670
1692
  ],
1671
1693
  "responses": {
@@ -5308,7 +5330,8 @@
5308
5330
  "updates",
5309
5331
  "events",
5310
5332
  "debug",
5311
- "custom"
5333
+ "custom",
5334
+ "tools"
5312
5335
  ]
5313
5336
  },
5314
5337
  "type": "array"
@@ -5324,7 +5347,8 @@
5324
5347
  "updates",
5325
5348
  "events",
5326
5349
  "debug",
5327
- "custom"
5350
+ "custom",
5351
+ "tools"
5328
5352
  ]
5329
5353
  }
5330
5354
  ],
@@ -5506,7 +5530,8 @@
5506
5530
  "updates",
5507
5531
  "events",
5508
5532
  "debug",
5509
- "custom"
5533
+ "custom",
5534
+ "tools"
5510
5535
  ]
5511
5536
  },
5512
5537
  "type": "array"
@@ -5522,7 +5547,8 @@
5522
5547
  "updates",
5523
5548
  "events",
5524
5549
  "debug",
5525
- "custom"
5550
+ "custom",
5551
+ "tools"
5526
5552
  ]
5527
5553
  }
5528
5554
  ],