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/cli.py CHANGED
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import contextlib
2
3
  import json
3
4
  import logging
@@ -8,15 +9,18 @@ import typing
8
9
  from collections.abc import Mapping, Sequence
9
10
  from typing import Literal
10
11
 
11
- from typing_extensions import TypedDict
12
-
13
12
  if typing.TYPE_CHECKING:
14
- from langgraph_api.config import HttpConfig, StoreConfig
13
+ from packaging.version import Version
14
+
15
+ from langgraph_api.config import AuthConfig, HttpConfig, StoreConfig
15
16
 
16
17
  logging.basicConfig(level=logging.INFO)
17
18
  logger = logging.getLogger(__name__)
18
19
 
19
20
 
21
+ SUPPORT_STATUS = Literal["active", "critical", "eol"]
22
+
23
+
20
24
  def _get_ls_origin() -> str | None:
21
25
  from langsmith.client import Client
22
26
  from langsmith.utils import tracing_is_enabled
@@ -76,91 +80,6 @@ def patch_environment(**kwargs):
76
80
  os.environ[key] = value
77
81
 
78
82
 
79
- class SecurityConfig(TypedDict, total=False):
80
- securitySchemes: dict
81
- security: list
82
- # path => {method => security}
83
- paths: dict[str, dict[str, list]]
84
-
85
-
86
- class CacheConfig(TypedDict, total=False):
87
- cache_keys: list[str]
88
- ttl_seconds: int
89
- max_size: int
90
-
91
-
92
- class AuthConfig(TypedDict, total=False):
93
- path: str
94
- """Path to the authentication function in a Python file."""
95
- disable_studio_auth: bool
96
- """Whether to disable auth when connecting from the LangSmith Studio."""
97
- openapi: SecurityConfig
98
- """The schema to use for updating the openapi spec.
99
-
100
- Example:
101
- {
102
- "securitySchemes": {
103
- "OAuth2": {
104
- "type": "oauth2",
105
- "flows": {
106
- "password": {
107
- "tokenUrl": "/token",
108
- "scopes": {
109
- "me": "Read information about the current user",
110
- "items": "Access to create and manage items"
111
- }
112
- }
113
- }
114
- }
115
- },
116
- "security": [
117
- {"OAuth2": ["me"]} # Default security requirement for all endpoints
118
- ]
119
- }
120
- """
121
- cache: CacheConfig | None
122
-
123
-
124
- def _check_newer_version(pkg: str, timeout: float = 0.2) -> None:
125
- """Log a notice if PyPI reports a newer version."""
126
- import importlib.metadata as md
127
- import json
128
- import urllib.request
129
-
130
- from packaging.version import Version
131
-
132
- thread_logger = logging.getLogger("check_version")
133
- if not thread_logger.handlers:
134
- handler = logging.StreamHandler()
135
- handler.setFormatter(logging.Formatter("%(message)s"))
136
- thread_logger.addHandler(handler)
137
-
138
- try:
139
- current = Version(md.version(pkg))
140
- with urllib.request.urlopen(
141
- f"https://pypi.org/pypi/{pkg}/json", timeout=timeout
142
- ) as resp:
143
- latest_str = json.load(resp)["info"]["version"]
144
- latest = Version(latest_str)
145
- if latest > current:
146
- thread_logger.info(
147
- "🔔 A newer version of %s is available: %s → %s (pip install -U %s)",
148
- pkg,
149
- current,
150
- latest,
151
- pkg,
152
- )
153
-
154
- except Exception:
155
- pass
156
-
157
- except RuntimeError:
158
- thread_logger.info(
159
- f"Failed to check for newer version of {pkg}."
160
- " To disable version checks, set LANGGRAPH_NO_VERSION_CHECK=true"
161
- )
162
-
163
-
164
83
  def run_server(
165
84
  host: str = "127.0.0.1",
166
85
  port: int = 2024,
@@ -176,15 +95,20 @@ def run_server(
176
95
  reload_includes: Sequence[str] | None = None,
177
96
  reload_excludes: Sequence[str] | None = None,
178
97
  store: typing.Optional["StoreConfig"] = None,
179
- auth: AuthConfig | None = None,
98
+ auth: typing.Optional["AuthConfig"] = None,
180
99
  http: typing.Optional["HttpConfig"] = None,
181
100
  ui: dict | None = None,
101
+ webhooks: dict | None = None,
182
102
  ui_config: dict | None = None,
183
103
  studio_url: str | None = None,
184
104
  disable_persistence: bool = False,
185
105
  allow_blocking: bool = False,
186
106
  runtime_edition: Literal["inmem", "community", "postgres"] = "inmem",
187
107
  server_level: str = "WARNING",
108
+ __redis_uri__: str | None = "fake",
109
+ __database_uri__: str | None = ":memory:",
110
+ __migrations_path__: str | None = "__inmem",
111
+ __entrypoint__: Literal["server", "python-executor"] = "server",
188
112
  **kwargs: typing.Any,
189
113
  ):
190
114
  """Run the LangGraph API server."""
@@ -200,6 +124,8 @@ def run_server(
200
124
  mount_prefix = None
201
125
  if http is not None and http.get("mount_prefix") is not None:
202
126
  mount_prefix = http.get("mount_prefix")
127
+ if os.environ.get("MOUNT_PREFIX"):
128
+ mount_prefix = os.environ.get("MOUNT_PREFIX")
203
129
  if os.environ.get("LANGGRAPH_MOUNT_PREFIX"):
204
130
  mount_prefix = os.environ.get("LANGGRAPH_MOUNT_PREFIX")
205
131
  if isinstance(env, str | pathlib.Path):
@@ -265,16 +191,19 @@ def run_server(
265
191
  else:
266
192
  local_url = upstream_url
267
193
  to_patch = dict(
268
- MIGRATIONS_PATH="__inmem",
269
- DATABASE_URI=":memory:",
270
- REDIS_URI="fake",
271
- N_JOBS_PER_WORKER=str(n_jobs_per_worker if n_jobs_per_worker else 1),
194
+ MIGRATIONS_PATH=__migrations_path__,
195
+ DATABASE_URI=__database_uri__,
196
+ REDIS_URI=__redis_uri__,
197
+ N_JOBS_PER_WORKER=str(
198
+ n_jobs_per_worker if n_jobs_per_worker is not None else 1
199
+ ),
272
200
  LANGGRAPH_STORE=json.dumps(store) if store else None,
273
201
  LANGSERVE_GRAPHS=json.dumps(graphs) if graphs else None,
274
202
  LANGSMITH_LANGGRAPH_API_VARIANT="local_dev",
275
203
  LANGGRAPH_AUTH=json.dumps(auth) if auth else None,
276
204
  LANGGRAPH_HTTP=json.dumps(http) if http else None,
277
205
  LANGGRAPH_UI=json.dumps(ui) if ui else None,
206
+ LANGGRAPH_WEBHOOKS=json.dumps(webhooks) if webhooks else None,
278
207
  LANGGRAPH_UI_CONFIG=json.dumps(ui_config) if ui_config else None,
279
208
  LANGGRAPH_UI_BUNDLER="true",
280
209
  LANGGRAPH_API_URL=local_url,
@@ -291,7 +220,7 @@ def run_server(
291
220
  if k in to_patch:
292
221
  logger.debug(f"Skipping loaded env var {k}={v}")
293
222
  continue
294
- to_patch[k] = v
223
+ to_patch[k] = v # type: ignore[invalid-assignment]
295
224
  with patch_environment(
296
225
  **to_patch,
297
226
  ):
@@ -325,7 +254,7 @@ def run_server(
325
254
  full_studio_url = f"{studio_origin}/studio/?baseUrl={local_url}&organizationId={org_id}"
326
255
  except TimeoutError as e:
327
256
  thread_logger.debug(
328
- f"Failed to get organization ID: {str(e)}"
257
+ f"Failed to get organization ID: {e!s}"
329
258
  )
330
259
  pass
331
260
  thread_logger.info(
@@ -354,7 +283,7 @@ def run_server(
354
283
  - 📚 API Docs: \033[36m{local_url}/docs\033[0m
355
284
 
356
285
  This in-memory server is designed for development and testing.
357
- For production use, please use LangGraph Platform.
286
+ For production use, please use LangSmith Deployment.
358
287
 
359
288
  """
360
289
  logger.info(welcome)
@@ -362,8 +291,12 @@ For production use, please use LangGraph Platform.
362
291
  threading.Thread(target=_open_browser, daemon=True).start()
363
292
  nvc = os.getenv("LANGGRAPH_NO_VERSION_CHECK")
364
293
  if nvc is None or nvc.lower() not in ("true", "1"):
294
+ from langgraph_api import __version__
295
+
365
296
  threading.Thread(
366
- target=_check_newer_version, args=("langgraph-api",), daemon=True
297
+ target=_check_newer_version,
298
+ args=("langgraph-api", __version__),
299
+ daemon=True,
367
300
  ).start()
368
301
  supported_kwargs = {
369
302
  k: v
@@ -371,40 +304,53 @@ For production use, please use LangGraph Platform.
371
304
  if k in inspect.signature(uvicorn.run).parameters
372
305
  }
373
306
  server_level = server_level.upper()
374
- uvicorn.run(
375
- "langgraph_api.server:app",
376
- host=host,
377
- port=port,
378
- reload=reload,
379
- env_file=env_file,
380
- access_log=False,
381
- reload_includes=list(reload_includes) if reload_includes else None,
382
- reload_excludes=list(reload_excludes) if reload_excludes else None,
383
- log_config={
384
- "version": 1,
385
- "incremental": False,
386
- "disable_existing_loggers": False,
387
- "formatters": {
388
- "simple": {
389
- "class": "langgraph_api.logging.Formatter",
390
- }
391
- },
392
- "handlers": {
393
- "console": {
394
- "class": "logging.StreamHandler",
395
- "formatter": "simple",
396
- "stream": "ext://sys.stdout",
397
- }
398
- },
399
- "loggers": {
400
- "uvicorn": {"level": server_level},
401
- "uvicorn.error": {"level": server_level},
402
- "langgraph_api.server": {"level": server_level},
307
+ if __entrypoint__ == "server":
308
+ uvicorn.run(
309
+ "langgraph_api.server:app",
310
+ host=host,
311
+ port=port,
312
+ reload=reload,
313
+ env_file=env_file,
314
+ access_log=False,
315
+ reload_includes=list(reload_includes) if reload_includes else None,
316
+ reload_excludes=list(reload_excludes) if reload_excludes else None,
317
+ log_config={
318
+ "version": 1,
319
+ "incremental": False,
320
+ "disable_existing_loggers": False,
321
+ "formatters": {
322
+ "simple": {
323
+ "class": "langgraph_api.logging.Formatter",
324
+ }
325
+ },
326
+ "handlers": {
327
+ "console": {
328
+ "class": "logging.StreamHandler",
329
+ "formatter": "simple",
330
+ "stream": "ext://sys.stdout",
331
+ }
332
+ },
333
+ "loggers": {
334
+ "uvicorn": {"level": server_level},
335
+ "uvicorn.error": {"level": server_level},
336
+ "langgraph_api.server": {"level": server_level},
337
+ },
338
+ "root": {"handlers": ["console"]},
403
339
  },
404
- "root": {"handlers": ["console"]},
405
- },
406
- **supported_kwargs,
407
- )
340
+ **supported_kwargs,
341
+ )
342
+ elif __entrypoint__ == "python-executor":
343
+ from langgraph_api.executor_entrypoint import (
344
+ main as executor_entrypoint_main,
345
+ )
346
+
347
+ asyncio.run(
348
+ executor_entrypoint_main(
349
+ grpc_port=8188,
350
+ )
351
+ )
352
+ else:
353
+ raise ValueError(f"Unknown entrypoint: {__entrypoint__}")
408
354
 
409
355
 
410
356
  def main():
@@ -429,7 +375,7 @@ def main():
429
375
  help="Number of jobs per worker. Default is None (meaning 10)",
430
376
  )
431
377
  parser.add_argument(
432
- "--no-browser", action="store_true", help="Disable automatic browser opening"
378
+ "--open-browser", action="store_true", help="Open browser automatically"
433
379
  )
434
380
  parser.add_argument(
435
381
  "--debug-port", type=int, help="Port for debugger to listen on (default: none)"
@@ -444,7 +390,19 @@ def main():
444
390
  action="store_true",
445
391
  help="Expose the server via Cloudflare Tunnel",
446
392
  )
447
-
393
+ parser.add_argument(
394
+ "--runtime-edition",
395
+ type=str,
396
+ default="inmem",
397
+ help="Runtime edition to use",
398
+ )
399
+ parser.add_argument(
400
+ "--entrypoint",
401
+ type=str,
402
+ default="server",
403
+ choices=["server", "python-executor"],
404
+ help="Entry point to use",
405
+ )
448
406
  args = parser.parse_args()
449
407
 
450
408
  with open(args.config, encoding="utf-8") as f:
@@ -453,23 +411,162 @@ def main():
453
411
  graphs = config_data.get("graphs", {})
454
412
  auth = config_data.get("auth")
455
413
  ui = config_data.get("ui")
414
+ webhooks = config_data.get("webhooks")
456
415
  ui_config = config_data.get("ui_config")
416
+ kwargs = {}
417
+ if args.runtime_edition == "postgres":
418
+ kwargs["__redis_uri__"] = os.getenv("REDIS_URI")
419
+ kwargs["__database_uri__"] = os.getenv("DATABASE_URI")
420
+ kwargs["__migrations_path__"] = os.getenv("MIGRATIONS_PATH")
421
+ if args.entrypoint == "python-executor":
422
+ kwargs["__entrypoint__"] = "python-executor"
457
423
  run_server(
458
424
  args.host,
459
425
  args.port,
460
426
  not args.no_reload,
461
427
  graphs,
462
428
  n_jobs_per_worker=args.n_jobs_per_worker,
463
- open_browser=not args.no_browser,
429
+ open_browser=args.open_browser,
464
430
  tunnel=args.tunnel,
465
431
  debug_port=args.debug_port,
466
432
  wait_for_client=args.wait_for_client,
467
433
  env=config_data.get("env", None),
468
434
  auth=auth,
469
435
  ui=ui,
436
+ webhooks=webhooks,
470
437
  ui_config=ui_config,
438
+ runtime_edition=args.runtime_edition,
439
+ **kwargs,
440
+ )
441
+
442
+
443
+ def _check_newer_version(pkg: str, current_version: str, timeout: float = 0.5) -> None:
444
+ """Check PyPI for newer versions and log support status.
445
+
446
+ Critical = one minor behind on same major, OR latest minor of previous major while latest is X.0.*
447
+ EOL = two+ minors behind on same major, OR any previous major after X.1.*
448
+ """
449
+ import json
450
+ import urllib.request
451
+
452
+ from packaging.version import InvalidVersion, Version
453
+
454
+ log = logging.getLogger("version_check")
455
+ if not log.handlers:
456
+ h = logging.StreamHandler()
457
+ h.setFormatter(logging.Formatter("%(message)s"))
458
+ log.addHandler(h)
459
+
460
+ if os.getenv("LANGGRAPH_NO_VERSION_CHECK", "").lower() in ("true", "1"):
461
+ return
462
+
463
+ def _parse(v: str) -> Version | None:
464
+ try:
465
+ return Version(v)
466
+ except InvalidVersion:
467
+ return None
468
+
469
+ try:
470
+ current = Version(current_version)
471
+ except InvalidVersion:
472
+ log.info(
473
+ f"[version] Could not parse installed version {current_version!r}. Skipping support check."
474
+ )
475
+ return
476
+
477
+ try:
478
+ with urllib.request.urlopen(
479
+ f"https://pypi.org/pypi/{pkg}/json", timeout=timeout
480
+ ) as resp:
481
+ payload = json.load(resp)
482
+ latest_str = payload["info"]["version"]
483
+ latest = Version(latest_str)
484
+ releases: dict[str, list[dict]] = payload.get("releases", {})
485
+ except Exception:
486
+ log.debug("Failed to retrieve latest version info for %s", pkg)
487
+ return
488
+ prev_major_latest_minor: Version | None = None
489
+ if latest.major > 0:
490
+ pm = latest.major - 1
491
+ prev_major_versions = [
492
+ v
493
+ for s in releases
494
+ if (v := _parse(s)) is not None and not v.is_prerelease and v.major == pm
495
+ ]
496
+ if prev_major_versions:
497
+ prev_major_latest_minor = max(
498
+ prev_major_versions, key=lambda v: (v.major, v.minor, v.micro)
499
+ )
500
+
501
+ if latest > current and not current.is_prerelease:
502
+ log.info(
503
+ "[version] A newer version of %s is available: %s → %s (pip install -U %s)",
504
+ pkg,
505
+ current,
506
+ latest,
507
+ pkg,
508
+ )
509
+
510
+ level = _support_level(current, latest, prev_major_latest_minor)
511
+ changelog = (
512
+ "https://docs.langchain.com/langgraph-platform/langgraph-server-changelog"
471
513
  )
472
514
 
515
+ if level == "critical":
516
+ # Distinguish same-major vs cross-major grace in the wording
517
+ if current.major == latest.major and current.minor == latest.minor - 1:
518
+ tail = "You are one minor version behind the latest (%d.%d.x).\n"
519
+ else:
520
+ tail = "You are on the latest minor of the previous major while a new major (%d.%d.x) just released.\n"
521
+ log.info(
522
+ "⚠️ [support] %s %s is in Critical support.\n"
523
+ "Only critical security and installation fixes are provided.\n"
524
+ + tail
525
+ + "Please plan an upgrade soon. See changelog: %s",
526
+ pkg,
527
+ current,
528
+ latest.major,
529
+ latest.minor,
530
+ changelog,
531
+ )
532
+ elif level == "eol":
533
+ log.info(
534
+ "⚠️ [support] %s %s is End of Life.\n"
535
+ "No bug fixes or security updates will be provided.\n"
536
+ "You are two or more minor versions behind the latest (%d.%d.x).\n"
537
+ "You should upgrade immediately. See changelog: %s",
538
+ pkg,
539
+ current,
540
+ latest.major,
541
+ latest.minor,
542
+ changelog,
543
+ )
544
+
545
+
546
+ def _support_level(
547
+ cur: "Version", lat: "Version", prev_major_latest_minor: "Version | None"
548
+ ) -> SUPPORT_STATUS:
549
+ if cur.major > lat.major:
550
+ return "active"
551
+ if cur.major == lat.major:
552
+ if cur.minor == lat.minor:
553
+ return "active"
554
+ if cur.minor == lat.minor - 1:
555
+ return "critical"
556
+ if cur.minor <= lat.minor - 2:
557
+ return "eol"
558
+ return "active"
559
+
560
+ if cur.major == lat.major - 1 and lat.minor == 0:
561
+ if (
562
+ prev_major_latest_minor is not None
563
+ and cur.minor == prev_major_latest_minor.minor
564
+ ):
565
+ return "critical"
566
+ return "eol"
567
+
568
+ return "eol"
569
+
473
570
 
474
571
  if __name__ == "__main__":
475
572
  main()
langgraph_api/command.py CHANGED
@@ -1,3 +1,5 @@
1
+ from typing import cast
2
+
1
3
  from langgraph.types import Command, Send
2
4
 
3
5
  from langgraph_api.schema import RunCommand
@@ -11,15 +13,15 @@ def map_cmd(cmd: RunCommand) -> Command:
11
13
  update = cmd.get("update")
12
14
  if isinstance(update, tuple | list) and all(
13
15
  isinstance(t, tuple | list) and len(t) == 2 and isinstance(t[0], str)
14
- for t in update
16
+ for t in cast("list", update)
15
17
  ):
16
- update = [tuple(t) for t in update]
18
+ update = [tuple(t) for t in cast("list", update)]
17
19
 
18
20
  return Command(
19
21
  update=update,
20
22
  goto=(
21
23
  [
22
- it if isinstance(it, str) else Send(it["node"], it["input"])
24
+ it if isinstance(it, str) else Send(it["node"], it["input"]) # type: ignore[non-subscriptable]
23
25
  for it in goto
24
26
  ]
25
27
  if goto