langgraph-api 0.4.1__py3-none-any.whl → 0.7.3__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.
Files changed (135) hide show
  1. langgraph_api/__init__.py +1 -1
  2. langgraph_api/api/__init__.py +111 -51
  3. langgraph_api/api/a2a.py +1610 -0
  4. langgraph_api/api/assistants.py +212 -89
  5. langgraph_api/api/mcp.py +3 -3
  6. langgraph_api/api/meta.py +52 -28
  7. langgraph_api/api/openapi.py +27 -17
  8. langgraph_api/api/profile.py +108 -0
  9. langgraph_api/api/runs.py +342 -195
  10. langgraph_api/api/store.py +19 -2
  11. langgraph_api/api/threads.py +209 -27
  12. langgraph_api/asgi_transport.py +14 -9
  13. langgraph_api/asyncio.py +14 -4
  14. langgraph_api/auth/custom.py +52 -37
  15. langgraph_api/auth/langsmith/backend.py +4 -3
  16. langgraph_api/auth/langsmith/client.py +13 -8
  17. langgraph_api/cli.py +230 -133
  18. langgraph_api/command.py +5 -3
  19. langgraph_api/config/__init__.py +532 -0
  20. langgraph_api/config/_parse.py +58 -0
  21. langgraph_api/config/schemas.py +431 -0
  22. langgraph_api/cron_scheduler.py +17 -1
  23. langgraph_api/encryption/__init__.py +15 -0
  24. langgraph_api/encryption/aes_json.py +158 -0
  25. langgraph_api/encryption/context.py +35 -0
  26. langgraph_api/encryption/custom.py +280 -0
  27. langgraph_api/encryption/middleware.py +632 -0
  28. langgraph_api/encryption/shared.py +63 -0
  29. langgraph_api/errors.py +12 -1
  30. langgraph_api/executor_entrypoint.py +11 -6
  31. langgraph_api/feature_flags.py +29 -0
  32. langgraph_api/graph.py +176 -76
  33. langgraph_api/grpc/client.py +313 -0
  34. langgraph_api/grpc/config_conversion.py +231 -0
  35. langgraph_api/grpc/generated/__init__.py +29 -0
  36. langgraph_api/grpc/generated/checkpointer_pb2.py +63 -0
  37. langgraph_api/grpc/generated/checkpointer_pb2.pyi +99 -0
  38. langgraph_api/grpc/generated/checkpointer_pb2_grpc.py +329 -0
  39. langgraph_api/grpc/generated/core_api_pb2.py +216 -0
  40. langgraph_api/grpc/generated/core_api_pb2.pyi +905 -0
  41. langgraph_api/grpc/generated/core_api_pb2_grpc.py +1621 -0
  42. langgraph_api/grpc/generated/engine_common_pb2.py +219 -0
  43. langgraph_api/grpc/generated/engine_common_pb2.pyi +722 -0
  44. langgraph_api/grpc/generated/engine_common_pb2_grpc.py +24 -0
  45. langgraph_api/grpc/generated/enum_cancel_run_action_pb2.py +37 -0
  46. langgraph_api/grpc/generated/enum_cancel_run_action_pb2.pyi +12 -0
  47. langgraph_api/grpc/generated/enum_cancel_run_action_pb2_grpc.py +24 -0
  48. langgraph_api/grpc/generated/enum_control_signal_pb2.py +37 -0
  49. langgraph_api/grpc/generated/enum_control_signal_pb2.pyi +16 -0
  50. langgraph_api/grpc/generated/enum_control_signal_pb2_grpc.py +24 -0
  51. langgraph_api/grpc/generated/enum_durability_pb2.py +37 -0
  52. langgraph_api/grpc/generated/enum_durability_pb2.pyi +16 -0
  53. langgraph_api/grpc/generated/enum_durability_pb2_grpc.py +24 -0
  54. langgraph_api/grpc/generated/enum_multitask_strategy_pb2.py +37 -0
  55. langgraph_api/grpc/generated/enum_multitask_strategy_pb2.pyi +16 -0
  56. langgraph_api/grpc/generated/enum_multitask_strategy_pb2_grpc.py +24 -0
  57. langgraph_api/grpc/generated/enum_run_status_pb2.py +37 -0
  58. langgraph_api/grpc/generated/enum_run_status_pb2.pyi +22 -0
  59. langgraph_api/grpc/generated/enum_run_status_pb2_grpc.py +24 -0
  60. langgraph_api/grpc/generated/enum_stream_mode_pb2.py +37 -0
  61. langgraph_api/grpc/generated/enum_stream_mode_pb2.pyi +28 -0
  62. langgraph_api/grpc/generated/enum_stream_mode_pb2_grpc.py +24 -0
  63. langgraph_api/grpc/generated/enum_thread_status_pb2.py +37 -0
  64. langgraph_api/grpc/generated/enum_thread_status_pb2.pyi +16 -0
  65. langgraph_api/grpc/generated/enum_thread_status_pb2_grpc.py +24 -0
  66. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.py +37 -0
  67. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.pyi +16 -0
  68. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2_grpc.py +24 -0
  69. langgraph_api/grpc/generated/errors_pb2.py +39 -0
  70. langgraph_api/grpc/generated/errors_pb2.pyi +21 -0
  71. langgraph_api/grpc/generated/errors_pb2_grpc.py +24 -0
  72. langgraph_api/grpc/ops/__init__.py +370 -0
  73. langgraph_api/grpc/ops/assistants.py +424 -0
  74. langgraph_api/grpc/ops/runs.py +792 -0
  75. langgraph_api/grpc/ops/threads.py +1013 -0
  76. langgraph_api/http.py +16 -5
  77. langgraph_api/http_metrics.py +15 -35
  78. langgraph_api/http_metrics_utils.py +38 -0
  79. langgraph_api/js/build.mts +1 -1
  80. langgraph_api/js/client.http.mts +13 -7
  81. langgraph_api/js/client.mts +2 -5
  82. langgraph_api/js/package.json +29 -28
  83. langgraph_api/js/remote.py +56 -30
  84. langgraph_api/js/src/graph.mts +20 -0
  85. langgraph_api/js/sse.py +2 -2
  86. langgraph_api/js/ui.py +1 -1
  87. langgraph_api/js/yarn.lock +1204 -1006
  88. langgraph_api/logging.py +29 -2
  89. langgraph_api/metadata.py +99 -28
  90. langgraph_api/middleware/http_logger.py +7 -2
  91. langgraph_api/middleware/private_network.py +7 -7
  92. langgraph_api/models/run.py +54 -93
  93. langgraph_api/otel_context.py +205 -0
  94. langgraph_api/patch.py +5 -3
  95. langgraph_api/queue_entrypoint.py +154 -65
  96. langgraph_api/route.py +47 -5
  97. langgraph_api/schema.py +88 -10
  98. langgraph_api/self_hosted_logs.py +124 -0
  99. langgraph_api/self_hosted_metrics.py +450 -0
  100. langgraph_api/serde.py +79 -37
  101. langgraph_api/server.py +138 -60
  102. langgraph_api/state.py +4 -3
  103. langgraph_api/store.py +25 -16
  104. langgraph_api/stream.py +80 -29
  105. langgraph_api/thread_ttl.py +31 -13
  106. langgraph_api/timing/__init__.py +25 -0
  107. langgraph_api/timing/profiler.py +200 -0
  108. langgraph_api/timing/timer.py +318 -0
  109. langgraph_api/utils/__init__.py +53 -8
  110. langgraph_api/utils/cache.py +47 -10
  111. langgraph_api/utils/config.py +2 -1
  112. langgraph_api/utils/errors.py +77 -0
  113. langgraph_api/utils/future.py +10 -6
  114. langgraph_api/utils/headers.py +76 -2
  115. langgraph_api/utils/retriable_client.py +74 -0
  116. langgraph_api/utils/stream_codec.py +315 -0
  117. langgraph_api/utils/uuids.py +29 -62
  118. langgraph_api/validation.py +9 -0
  119. langgraph_api/webhook.py +120 -6
  120. langgraph_api/worker.py +55 -24
  121. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/METADATA +16 -8
  122. langgraph_api-0.7.3.dist-info/RECORD +168 -0
  123. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/WHEEL +1 -1
  124. langgraph_runtime/__init__.py +1 -0
  125. langgraph_runtime/routes.py +11 -0
  126. logging.json +1 -3
  127. openapi.json +839 -478
  128. langgraph_api/config.py +0 -387
  129. langgraph_api/js/isolate-0x130008000-46649-46649-v8.log +0 -4430
  130. langgraph_api/js/isolate-0x138008000-44681-44681-v8.log +0 -4430
  131. langgraph_api/js/package-lock.json +0 -3308
  132. langgraph_api-0.4.1.dist-info/RECORD +0 -107
  133. /langgraph_api/{utils.py → grpc/__init__.py} +0 -0
  134. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/entry_points.txt +0 -0
  135. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/licenses/LICENSE +0 -0
langgraph_api/http.py CHANGED
@@ -70,11 +70,11 @@ class JsonHttpClient:
70
70
  await res.aclose()
71
71
 
72
72
 
73
- _http_client: JsonHttpClient
73
+ _http_client: JsonHttpClient | None = None
74
74
  _loopback_client: JsonHttpClient | None = None
75
75
 
76
76
 
77
- async def start_http_client() -> None:
77
+ async def start_http_client() -> JsonHttpClient:
78
78
  global _http_client
79
79
  _http_client = JsonHttpClient(
80
80
  client=httpx.AsyncClient(
@@ -86,15 +86,26 @@ async def start_http_client() -> None:
86
86
  ),
87
87
  ),
88
88
  )
89
+ return _http_client
89
90
 
90
91
 
91
92
  async def stop_http_client() -> None:
92
93
  global _http_client
94
+ if _http_client is None:
95
+ return
93
96
  await _http_client.client.aclose()
94
- del _http_client
97
+ _http_client = None
98
+
99
+
100
+ def get_http_client() -> JsonHttpClient | None:
101
+ global _http_client
102
+ return _http_client
95
103
 
96
104
 
97
- def get_http_client() -> JsonHttpClient:
105
+ async def ensure_http_client() -> JsonHttpClient:
106
+ global _http_client
107
+ if _http_client is None:
108
+ return await start_http_client()
98
109
  return _http_client
99
110
 
100
111
 
@@ -168,7 +179,7 @@ async def http_request(
168
179
  if not path.startswith(("http://", "https://", "/")):
169
180
  raise ValueError("path must start with / or http")
170
181
 
171
- client = client or get_http_client()
182
+ client = client or (await ensure_http_client())
172
183
 
173
184
  content = None
174
185
  if body is not None:
@@ -1,51 +1,23 @@
1
1
  from collections import defaultdict
2
2
  from typing import Any
3
3
 
4
- FILTERED_ROUTES = {"/ok", "/info", "/metrics", "/docs", "/openapi.json"}
4
+ from langgraph_api import config
5
+ from langgraph_api.http_metrics_utils import (
6
+ HTTP_LATENCY_BUCKETS,
7
+ get_route,
8
+ should_filter_route,
9
+ )
5
10
 
6
11
  MAX_REQUEST_COUNT_ENTRIES = 5000
7
12
  MAX_HISTOGRAM_ENTRIES = 1000
8
13
 
9
14
 
10
- def get_route(route: Any) -> str | None:
11
- try:
12
- # default lg api routes use the custom APIRoute where scope["route"] is set to a string
13
- if isinstance(route, str):
14
- return route
15
- else:
16
- # custom FastAPI routes provided by user_router attach an object to scope["route"]
17
- route_path = getattr(route, "path", None)
18
- return route_path
19
- except Exception:
20
- return None
21
-
22
-
23
- def should_filter_route(route_path: str) -> bool:
24
- # use endswith to honor MOUNT_PREFIX
25
- return any(route_path.endswith(suffix) for suffix in FILTERED_ROUTES)
26
-
27
-
28
15
  class HTTPMetricsCollector:
29
16
  def __init__(self):
30
17
  # Counter: Key: (method, route, status), Value: count
31
18
  self._request_counts: dict[tuple[str, str, int], int] = defaultdict(int)
32
19
 
33
- self._histogram_buckets = [
34
- 0.01,
35
- 0.1,
36
- 0.5,
37
- 1,
38
- 5,
39
- 15,
40
- 30,
41
- 60,
42
- 120,
43
- 300,
44
- 600,
45
- 1800,
46
- 3600,
47
- float("inf"),
48
- ]
20
+ self._histogram_buckets = HTTP_LATENCY_BUCKETS
49
21
  self._histogram_bucket_labels = [
50
22
  "+Inf" if value == float("inf") else str(value)
51
23
  for value in self._histogram_buckets
@@ -97,6 +69,14 @@ class HTTPMetricsCollector:
97
69
  hist_data["sum"] += latency_seconds
98
70
  hist_data["count"] += 1
99
71
 
72
+ try:
73
+ if config.LANGGRAPH_METRICS_ENABLED:
74
+ from langgraph_api.self_hosted_metrics import record_http_request
75
+
76
+ record_http_request(method, route_path, status, latency_seconds)
77
+ except Exception:
78
+ pass
79
+
100
80
  def get_metrics(
101
81
  self,
102
82
  project_id: str | None,
@@ -0,0 +1,38 @@
1
+ from typing import Any
2
+
3
+ FILTERED_ROUTES = {"/ok", "/info", "/metrics", "/docs", "/openapi.json"}
4
+
5
+ HTTP_LATENCY_BUCKETS = [
6
+ 0.01,
7
+ 0.1,
8
+ 0.5,
9
+ 1,
10
+ 5,
11
+ 15,
12
+ 30,
13
+ 60,
14
+ 120,
15
+ 300,
16
+ 600,
17
+ 1800,
18
+ 3600,
19
+ float("inf"),
20
+ ]
21
+
22
+
23
+ def get_route(route: Any) -> str | None:
24
+ try:
25
+ # default lg api routes use the custom APIRoute where scope["route"] is set to a string
26
+ if isinstance(route, str):
27
+ return route
28
+ else:
29
+ # custom FastAPI routes provided by user_router attach an object to scope["route"]
30
+ route_path = getattr(route, "path", None)
31
+ return route_path
32
+ except Exception:
33
+ return None
34
+
35
+
36
+ def should_filter_route(route_path: str) -> bool:
37
+ # use endswith to honor MOUNT_PREFIX
38
+ return any(route_path.endswith(suffix) for suffix in FILTERED_ROUTES)
@@ -1,7 +1,7 @@
1
1
  /// <reference types="./global.d.ts" />
2
2
  import "./src/preload.mjs";
3
3
 
4
- import { z } from "zod";
4
+ import { z } from "zod/v3";
5
5
  import * as fs from "node:fs/promises";
6
6
  import * as path from "node:path";
7
7
  import { type GraphSchema, resolveGraph } from "./src/graph.mts";
@@ -6,7 +6,7 @@ import * as path from "node:path";
6
6
  import * as url from "node:url";
7
7
  import { createLogger, format, transports } from "winston";
8
8
  import { gracefulExit } from "exit-hook";
9
- import { z } from "zod";
9
+ import { z } from "zod/v3";
10
10
 
11
11
  const logger = createLogger({
12
12
  level: "debug",
@@ -47,7 +47,9 @@ const wrapHonoApp = (app: Hono) => {
47
47
  // b/c the user's Hono version might be different than ours.
48
48
  // See warning here: https://hono.dev/docs/guides/middleware#built-in-middleware
49
49
  const newApp = new (Object.getPrototypeOf(app).constructor)() as Hono<{
50
- Variables: { body: string | ArrayBuffer | ReadableStream | null };
50
+ Variables: {
51
+ "langgraph:body": string | ArrayBuffer | ReadableStream | null;
52
+ };
51
53
  }>;
52
54
 
53
55
  // This endpoint is used to check if we can yield the routing to the Python server early.
@@ -71,11 +73,15 @@ const wrapHonoApp = (app: Hono) => {
71
73
  newApp.notFound(async (c) => {
72
74
  // Send the request body back to the Python server
73
75
  // Use the cached body in-case the user mutated the body
74
- let payload: any = null;
75
- try {
76
- payload = JSON.stringify(await c.req.json()) ?? null;
77
- } catch {
78
- // pass
76
+ let payload: string | ArrayBuffer | ReadableStream | null =
77
+ c.get("langgraph:body");
78
+
79
+ if (payload == null) {
80
+ try {
81
+ payload = JSON.stringify(await c.req.json()) ?? null;
82
+ } catch {
83
+ // pass
84
+ }
79
85
  }
80
86
 
81
87
  return c.body(payload, {
@@ -1,6 +1,6 @@
1
1
  /// <reference types="./global.d.ts" />
2
2
 
3
- import { z } from "zod";
3
+ import { z } from "zod/v3";
4
4
  import { Context, Hono } from "hono";
5
5
  import { serve } from "@hono/node-server";
6
6
  import { zValidator } from "@hono/zod-validator";
@@ -527,10 +527,7 @@ export class RemoteStore extends BaseStore {
527
527
  }
528
528
 
529
529
  async get(namespace: string[], key: string): Promise<Item | null> {
530
- return await sendRecv<Item | null>("store_get", {
531
- namespace: namespace.join("."),
532
- key,
533
- });
530
+ return await sendRecv<Item | null>("store_get", { namespace, key });
534
531
  }
535
532
 
536
533
  async search(
@@ -7,41 +7,42 @@
7
7
  "format": "prettier --write ."
8
8
  },
9
9
  "dependencies": {
10
- "@hono/node-server": "^1.12.0",
11
- "@hono/zod-validator": "^0.2.2",
12
- "@langchain/core": "^0.3.59",
13
- "@langchain/langgraph": "^0.2.65",
14
- "@langchain/langgraph-api": "~0.0.59",
15
- "@langchain/langgraph-ui": "~0.0.59",
16
- "@langchain/langgraph-checkpoint": "~0.0.18",
10
+ "@hono/node-server": "^1.19.8",
11
+ "@hono/zod-validator": "^0.7.6",
12
+ "@langchain/core": "^1.1.8",
13
+ "@langchain/langgraph": "^1.1.0",
14
+ "@langchain/langgraph-api": "^1.0.3",
15
+ "@langchain/langgraph-checkpoint": "^1.0.0",
16
+ "@langchain/langgraph-ui": "^1.1.11",
17
17
  "@types/json-schema": "^7.0.15",
18
- "@typescript/vfs": "^1.6.0",
19
- "dedent": "^1.5.3",
20
- "exit-hook": "^4.0.0",
21
- "hono": "^4.5.4",
22
- "p-queue": "^8.0.1",
23
- "p-retry": "^6.2.0",
24
- "tsx": "^4.19.3",
25
- "typescript": "^5.5.4",
26
- "undici": "^6.21.2",
27
- "uuid": "^10.0.0",
28
- "vite": "^6.1.6",
29
- "winston": "^3.17.0",
30
- "zod": "^3.25.32"
18
+ "@typescript/vfs": "^1.6.2",
19
+ "dedent": "^1.7.1",
20
+ "esbuild": "^0.27.2",
21
+ "exit-hook": "^5.0.1",
22
+ "hono": "^4.11.4",
23
+ "p-queue": "^9.0.1",
24
+ "p-retry": "^7.1.1",
25
+ "tsx": "^4.21.0",
26
+ "typescript": "^5.9.3",
27
+ "undici": "^7.18.2",
28
+ "uuid": "^13.0.0",
29
+ "vite": "^7.3.0",
30
+ "winston": "^3.19.0",
31
+ "zod": "^4.2.1"
31
32
  },
32
33
  "resolutions": {
33
34
  "esbuild": "^0.25.0",
34
35
  "vite": "^6.1.6"
35
36
  },
36
37
  "devDependencies": {
37
- "@langchain/langgraph-sdk": "^0.0.104",
38
- "@types/node": "^22.2.0",
39
- "@types/react": "^19.0.8",
40
- "@types/react-dom": "^19.0.3",
41
- "jose": "^6.0.10",
42
- "postgres": "^3.4.4",
43
- "prettier": "^3.3.3",
44
- "vitest": "^3.0.5"
38
+ "@langchain/langgraph-sdk": "^1.5.4",
39
+ "@types/node": "^25.0.9",
40
+ "@types/react": "^19.2.7",
41
+ "@types/react-dom": "^19.2.3",
42
+ "jose": "^6.1.3",
43
+ "postgres": "^3.4.7",
44
+ "prettier": "^3.7.4",
45
+ "vitest": "^4.0.16"
45
46
  },
46
47
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
47
48
  }
@@ -15,7 +15,7 @@ import httpx
15
15
  import orjson
16
16
  import structlog
17
17
  import uvicorn
18
- from langchain_core.runnables.config import RunnableConfig
18
+ from langchain_core.runnables.config import RunnableConfig, merge_configs
19
19
  from langchain_core.runnables.graph import Edge, Node
20
20
  from langchain_core.runnables.graph import Graph as DrawableGraph
21
21
  from langchain_core.runnables.schema import (
@@ -49,6 +49,7 @@ from langgraph_api.js.sse import SSEDecoder, aiter_lines_raw
49
49
  from langgraph_api.route import ApiResponse
50
50
  from langgraph_api.schema import Config
51
51
  from langgraph_api.serde import json_dumpb
52
+ from langgraph_api.utils import get_auth_ctx, get_user_id
52
53
 
53
54
  logger = structlog.stdlib.get_logger(__name__)
54
55
 
@@ -153,9 +154,9 @@ class RemotePregel(BaseRemotePregel):
153
154
 
154
155
  async for event in _client_stream("streamEvents", data):
155
156
  if event["event"] == "on_custom_event":
156
- yield CustomStreamEvent(**event)
157
+ yield CustomStreamEvent(**event) # type: ignore[missing-typed-dict-key]
157
158
  else:
158
- yield StandardStreamEvent(**event)
159
+ yield StandardStreamEvent(**event) # type: ignore[missing-typed-dict-key]
159
160
 
160
161
  async def fetch_state_schema(self):
161
162
  return await _client_invoke("getSchema", {"graph_id": self.graph_id})
@@ -170,9 +171,9 @@ class RemotePregel(BaseRemotePregel):
170
171
  "getGraph",
171
172
  {
172
173
  "graph_id": self.graph_id,
173
- "graph_config": self.config,
174
+ "graph_config": self._inject_auth_to_config(self.config),
174
175
  "graph_name": self.name,
175
- "config": config,
176
+ "config": self._inject_auth_to_config(config),
176
177
  "xray": xray,
177
178
  },
178
179
  )
@@ -187,15 +188,17 @@ class RemotePregel(BaseRemotePregel):
187
188
  )
188
189
  for data in nodes
189
190
  },
190
- {
191
- Edge(
192
- data["source"],
193
- data["target"],
194
- data.get("data"),
195
- data.get("conditional", False),
196
- )
197
- for data in edges
198
- },
191
+ list(
192
+ {
193
+ Edge(
194
+ data["source"],
195
+ data["target"],
196
+ data.get("data"),
197
+ data.get("conditional", False),
198
+ )
199
+ for data in edges
200
+ }
201
+ ),
199
202
  )
200
203
 
201
204
  async def fetch_subgraphs(
@@ -209,11 +212,11 @@ class RemotePregel(BaseRemotePregel):
209
212
  "getSubgraphs",
210
213
  {
211
214
  "graph_id": self.graph_id,
212
- "graph_config": self.config,
215
+ "graph_config": self._inject_auth_to_config(self.config),
213
216
  "graph_name": self.name,
214
217
  "namespace": namespace,
215
218
  "recurse": recurse,
216
- "config": config,
219
+ "config": self._inject_auth_to_config(config),
217
220
  },
218
221
  )
219
222
 
@@ -237,7 +240,7 @@ class RemotePregel(BaseRemotePregel):
237
240
  tuple(task["path"]) if task.get("path") else tuple(),
238
241
  # TODO: figure out how to properly deserialise errors
239
242
  task.get("error"),
240
- tuple(interrupts),
243
+ tuple(interrupts), # type: ignore[arg-type]
241
244
  state,
242
245
  task.get("result"),
243
246
  )
@@ -246,7 +249,7 @@ class RemotePregel(BaseRemotePregel):
246
249
 
247
250
  return StateSnapshot( # type: ignore[missing-argument]
248
251
  item.get("values"),
249
- cast(tuple, item.get("next", ())),
252
+ cast("tuple", item.get("next", ())),
250
253
  item.get("config"),
251
254
  item.get("metadata"),
252
255
  item.get("createdAt"),
@@ -264,9 +267,9 @@ class RemotePregel(BaseRemotePregel):
264
267
  "getState",
265
268
  {
266
269
  "graph_id": self.graph_id,
267
- "graph_config": self.config,
270
+ "graph_config": self._inject_auth_to_config(self.config),
268
271
  "graph_name": self.name,
269
- "config": config,
272
+ "config": self._inject_auth_to_config(config),
270
273
  "subgraphs": subgraphs,
271
274
  },
272
275
  )
@@ -282,9 +285,9 @@ class RemotePregel(BaseRemotePregel):
282
285
  "updateState",
283
286
  {
284
287
  "graph_id": self.graph_id,
285
- "graph_config": self.config,
288
+ "graph_config": self._inject_auth_to_config(self.config),
286
289
  "graph_name": self.name,
287
- "config": config,
290
+ "config": self._inject_auth_to_config(config),
288
291
  "values": values,
289
292
  "as_node": as_node,
290
293
  },
@@ -303,9 +306,9 @@ class RemotePregel(BaseRemotePregel):
303
306
  "getStateHistory",
304
307
  {
305
308
  "graph_id": self.graph_id,
306
- "graph_config": self.config,
309
+ "graph_config": self._inject_auth_to_config(self.config),
307
310
  "graph_name": self.name,
308
- "config": config,
311
+ "config": self._inject_auth_to_config(config),
309
312
  "limit": limit,
310
313
  "filter": filter,
311
314
  "before": before,
@@ -351,6 +354,27 @@ class RemotePregel(BaseRemotePregel):
351
354
  )
352
355
  return result["nodesExecuted"]
353
356
 
357
+ def _inject_auth_to_config(self, config: RunnableConfig | Config):
358
+ if ctx := get_auth_ctx():
359
+ user_id = get_user_id(cast("BaseUser | None", ctx.user))
360
+
361
+ # Skip if cannot serialize the user to JSON
362
+ if not hasattr(ctx.user, "model_dump"):
363
+ return config
364
+
365
+ return merge_configs(
366
+ config,
367
+ {
368
+ "configurable": {
369
+ "langgraph_auth_user": ctx.user,
370
+ "langgraph_auth_user_id": user_id,
371
+ "langgraph_auth_permissions": list(ctx.permissions),
372
+ }
373
+ },
374
+ )
375
+
376
+ return config
377
+
354
378
 
355
379
  async def run_js_process(paths_str: str | None, watch: bool = False):
356
380
  # check if tsx is available
@@ -630,13 +654,13 @@ async def run_remote_checkpointer():
630
654
 
631
655
  async def store_get(payload: dict):
632
656
  """Get store data"""
633
- namespaces_str = payload.get("namespace")
657
+ namespaces = payload.get("namespace")
634
658
  key = payload.get("key")
635
659
 
636
- if not namespaces_str or not key:
660
+ if not namespaces or not key:
637
661
  raise ValueError("Both namespaces and key are required")
638
662
 
639
- namespaces = namespaces_str.split(".")
663
+ namespaces = tuple(namespaces)
640
664
 
641
665
  store = await _get_passthrough_store()
642
666
  result = await store.aget(namespaces, key)
@@ -646,7 +670,7 @@ async def run_remote_checkpointer():
646
670
  async def store_put(payload: dict):
647
671
  """Put the new store data"""
648
672
 
649
- namespace = tuple(payload["namespace"].split("."))
673
+ namespace = tuple(payload["namespace"])
650
674
  key = payload["key"]
651
675
  value = payload["value"]
652
676
  index = payload.get("index")
@@ -861,6 +885,8 @@ class CustomJsAuthBackend(AuthenticationBackend):
861
885
  self.ls_auth = LangsmithAuthBackend()
862
886
  self.ttl_cache: LRUCache | None = None
863
887
  self.cache_keys: list[str] | None = None
888
+ if LANGGRAPH_AUTH is None:
889
+ raise ValueError("LANGGRAPH_AUTH is not set")
864
890
  if cache := LANGGRAPH_AUTH.get("cache"):
865
891
  keys = cache.get("cache_keys", [])
866
892
  if not isinstance(keys, list):
@@ -891,7 +917,7 @@ class CustomJsAuthBackend(AuthenticationBackend):
891
917
  if self.cache_keys:
892
918
  cache_key = tuple((k, headers[k]) for k in self.cache_keys if k in headers)
893
919
  if cache_key and self.ttl_cache is not None:
894
- cached = self.ttl_cache.get(cache_key)
920
+ cached = await self.ttl_cache.get(cache_key)
895
921
  if cached:
896
922
  return cached
897
923
 
@@ -954,7 +980,7 @@ async def handle_js_auth_event(
954
980
 
955
981
  raise HTTPException(status_code=status, detail=message, headers=headers)
956
982
 
957
- filters = cast(Auth.types.FilterType | None, response.get("filters"))
983
+ filters = cast("Auth.types.FilterType | None", response.get("filters"))
958
984
 
959
985
  # mutate metadata in value if applicable
960
986
  # we need to preserve the identity of the object, so cannot create a new
@@ -62,6 +62,17 @@ export async function resolveGraph(
62
62
  return "compile" in graph && typeof graph.compile === "function";
63
63
  };
64
64
 
65
+ const isCompiledGraph = (
66
+ graph: GraphLike,
67
+ ): graph is CompiledGraph<string> => {
68
+ if (typeof graph !== "object" || graph == null) return false;
69
+ return (
70
+ "builder" in graph &&
71
+ typeof graph.builder === "object" &&
72
+ graph.builder != null
73
+ );
74
+ };
75
+
65
76
  const graph: GraphUnknown = await import(sourceFile).then(
66
77
  (module) => module[exportSymbol || "default"],
67
78
  );
@@ -73,6 +84,15 @@ export async function resolveGraph(
73
84
 
74
85
  const afterResolve = (graphLike: GraphLike): CompiledGraph<string> => {
75
86
  const graph = isGraph(graphLike) ? graphLike.compile() : graphLike;
87
+
88
+ // TODO: hack, remove once LangChain 1.x createAgent is fixed
89
+ // LangGraph API will assign it's checkpointer by setting it
90
+ // via `graph.checkpointer = ...` and `graph.store = ...`, and the 1.x `createAgent`
91
+ // hides the underlying `StateGraph` instance, so we need to access it directly.
92
+ if (!isCompiledGraph(graph) && "graph" in graph) {
93
+ return (graph as { graph: CompiledGraph<string> }).graph;
94
+ }
95
+
76
96
  return graph;
77
97
  };
78
98
 
langgraph_api/js/sse.py CHANGED
@@ -51,7 +51,7 @@ class BytesLineDecoder:
51
51
  # Include any existing buffer in the first portion of the
52
52
  # splitlines result.
53
53
  self.buffer.extend(lines[0])
54
- lines = [self.buffer] + lines[1:]
54
+ lines = [self.buffer, *lines[1:]]
55
55
  self.buffer = bytearray()
56
56
 
57
57
  if not trailing_newline:
@@ -79,7 +79,7 @@ class SSEDecoder:
79
79
  self._retry: int | None = None
80
80
 
81
81
  def decode(self, line: bytes) -> StreamPart | None:
82
- # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501
82
+ # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
83
83
 
84
84
  if not line:
85
85
  if (
langgraph_api/js/ui.py CHANGED
@@ -66,7 +66,7 @@ async def _start_ui_bundler_process():
66
66
  "watch",
67
67
  "-o",
68
68
  UI_ROOT_DIR,
69
- env=os.environ,
69
+ env=dict(os.environ),
70
70
  )
71
71
  pid = process.pid
72
72
  logger.info("Started UI bundler process [%d]", pid)