langgraph-runtime-inmem 0.20.1__py3-none-any.whl → 0.21.0__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.
@@ -9,7 +9,7 @@ from langgraph_runtime_inmem import (
9
9
  store,
10
10
  )
11
11
 
12
- __version__ = "0.20.1"
12
+ __version__ = "0.21.0"
13
13
  __all__ = [
14
14
  "ops",
15
15
  "database",
@@ -4,6 +4,8 @@ import logging
4
4
  import os
5
5
  import typing
6
6
  import uuid
7
+ from collections.abc import AsyncIterator
8
+ from typing import Any
7
9
 
8
10
  from langgraph.checkpoint.memory import MemorySaver, PersistentDict
9
11
 
@@ -116,6 +118,64 @@ class InMemorySaver(MemorySaver):
116
118
  if os.path.exists(file_path):
117
119
  os.remove(file_path)
118
120
 
121
+ async def _decrypt_json(self, data: dict[str, Any]) -> dict[str, Any]:
122
+ """Decrypt a dict if custom encryption is configured."""
123
+ from langgraph_api import config as api_config
124
+
125
+ if not api_config.LANGGRAPH_ENCRYPTION:
126
+ return data
127
+ from langgraph_api.api.encryption_middleware import decrypt_json_if_needed
128
+ from langgraph_api.encryption import get_encryption_instance
129
+
130
+ result = await decrypt_json_if_needed(
131
+ data, get_encryption_instance(), "checkpoint"
132
+ )
133
+ if result is None:
134
+ raise ValueError("decrypt_json_if_needed returned None for non-None input")
135
+ return result
136
+
137
+ async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
138
+ """Get checkpoint tuple with decrypted metadata."""
139
+ tuple_ = self.get_tuple(config)
140
+ if tuple_ is None:
141
+ return None
142
+
143
+ # Decrypt metadata if encryption is enabled
144
+ decrypted_metadata = await self._decrypt_json(tuple_.metadata)
145
+
146
+ from langgraph.checkpoint.base import CheckpointTuple as CPTuple
147
+
148
+ return CPTuple(
149
+ config=tuple_.config,
150
+ checkpoint=tuple_.checkpoint,
151
+ metadata=decrypted_metadata,
152
+ parent_config=tuple_.parent_config,
153
+ pending_writes=tuple_.pending_writes,
154
+ )
155
+
156
+ async def alist(
157
+ self,
158
+ config: RunnableConfig | None,
159
+ *,
160
+ filter: dict[str, Any] | None = None,
161
+ before: RunnableConfig | None = None,
162
+ limit: int | None = None,
163
+ ) -> AsyncIterator[CheckpointTuple]:
164
+ """List checkpoints with decrypted metadata."""
165
+ from langgraph.checkpoint.base import CheckpointTuple as CPTuple
166
+
167
+ for tuple_ in self.list(config, filter=filter, before=before, limit=limit):
168
+ # Decrypt metadata if encryption is enabled
169
+ decrypted_metadata = await self._decrypt_json(tuple_.metadata)
170
+
171
+ yield CPTuple(
172
+ config=tuple_.config,
173
+ checkpoint=tuple_.checkpoint,
174
+ metadata=decrypted_metadata,
175
+ parent_config=tuple_.parent_config,
176
+ pending_writes=tuple_.pending_writes,
177
+ )
178
+
119
179
 
120
180
  MEMORY = None
121
181
 
@@ -489,24 +489,23 @@ class Assistants(Authenticated):
489
489
  status_code=404, detail=f"Assistant with ID {assistant_id} not found"
490
490
  )
491
491
 
492
+ # Cancel all in-flight runs for this assistant before deletion
493
+ await Runs.cancel(
494
+ conn,
495
+ assistant_id=assistant_id,
496
+ action="interrupt",
497
+ ctx=ctx,
498
+ )
499
+
492
500
  conn.store["assistants"] = [
493
501
  a for a in conn.store["assistants"] if a["assistant_id"] != assistant_id
494
502
  ]
495
- # Cascade delete assistant versions, crons, & runs on this assistant
503
+ # Cascade delete assistant versions
496
504
  conn.store["assistant_versions"] = [
497
505
  v
498
506
  for v in conn.store["assistant_versions"]
499
507
  if v["assistant_id"] != assistant_id
500
508
  ]
501
- retained = []
502
- for run in conn.store["runs"]:
503
- if run["assistant_id"] == assistant_id:
504
- res = await Runs.delete(
505
- conn, run["run_id"], thread_id=run["thread_id"], ctx=ctx
506
- )
507
- await anext(res)
508
- else:
509
- retained.append(run)
510
509
 
511
510
  async def _yield_deleted():
512
511
  yield assistant_id
@@ -883,8 +882,16 @@ class Threads(Authenticated):
883
882
  conn: InMemConnectionProto,
884
883
  thread_id: UUID,
885
884
  ctx: Auth.types.BaseAuthContext | None = None,
885
+ include_ttl: bool = False,
886
886
  ) -> AsyncIterator[Thread]:
887
- """Get a thread by ID."""
887
+ """Get a thread by ID.
888
+
889
+ Args:
890
+ conn: In-memory connection
891
+ thread_id: Thread ID
892
+ ctx: Auth context
893
+ include_ttl: Not supported in inmem - parameter ignored.
894
+ """
888
895
  matching_thread = await Threads._get(conn, thread_id, ctx)
889
896
 
890
897
  if not matching_thread:
@@ -1222,6 +1229,48 @@ class Threads(Authenticated):
1222
1229
 
1223
1230
  return empty_iterator()
1224
1231
 
1232
+ @staticmethod
1233
+ async def prune(
1234
+ thread_ids: Sequence[str] | Sequence[UUID],
1235
+ strategy: Literal["delete", "keep_latest"] = "delete",
1236
+ batch_size: int = 100,
1237
+ ctx: Auth.types.BaseAuthContext | None = None,
1238
+ ) -> int:
1239
+ """Prune threads by ID (inmem implementation).
1240
+
1241
+ Args:
1242
+ thread_ids: List of thread IDs to prune
1243
+ strategy: Prune strategy ("delete" supported, "keep_latest" not supported)
1244
+ batch_size: Not used in inmem implementation
1245
+ ctx: Auth context for permission checks
1246
+
1247
+ Returns:
1248
+ Number of threads successfully pruned
1249
+ """
1250
+ if not thread_ids:
1251
+ return 0
1252
+
1253
+ if strategy == "keep_latest":
1254
+ raise HTTPException(
1255
+ status_code=422,
1256
+ detail="keep_latest strategy is not supported in in-memory runtime",
1257
+ )
1258
+
1259
+ pruned = 0
1260
+ async with connect() as conn:
1261
+ for tid in thread_ids:
1262
+ try:
1263
+ tid_uuid = _ensure_uuid(tid)
1264
+ iter_result = await Threads.delete(conn, tid_uuid, ctx)
1265
+ # Consume the iterator to ensure deletion
1266
+ async for _ in iter_result:
1267
+ pruned += 1
1268
+ except HTTPException:
1269
+ # Thread not found or no permission - skip silently
1270
+ pass
1271
+
1272
+ return pruned
1273
+
1225
1274
  @staticmethod
1226
1275
  async def _delete_with_run(
1227
1276
  conn: InMemConnectionProto,
@@ -1981,6 +2030,7 @@ class Runs(Authenticated):
1981
2030
  "n_pending": 0,
1982
2031
  "pending_runs_wait_time_max_secs": None,
1983
2032
  "pending_runs_wait_time_med_secs": None,
2033
+ "pending_unblocked_runs_wait_time_max_secs": None,
1984
2034
  "n_running": 0,
1985
2035
  }
1986
2036
 
@@ -2005,11 +2055,34 @@ class Runs(Authenticated):
2005
2055
  else:
2006
2056
  med_pending_wait = None
2007
2057
 
2058
+ # Calculate max wait time for unblocked runs (runs not blocked by another run on the same thread)
2059
+ pending_unblocked_waits: list[float] = []
2060
+ for run in pending_runs:
2061
+ thread_id = run.get("thread_id")
2062
+ # Check if there's a running run on the same thread
2063
+ has_running_on_thread = any(
2064
+ r.get("thread_id") == thread_id and r.get("status") == "running"
2065
+ for r in conn.store["runs"]
2066
+ )
2067
+ if not has_running_on_thread:
2068
+ created_at = run.get("created_at")
2069
+ if not isinstance(created_at, datetime):
2070
+ continue
2071
+ if created_at.tzinfo is None:
2072
+ created_at = created_at.replace(tzinfo=UTC)
2073
+ if created_at < now:
2074
+ pending_unblocked_waits.append((now - created_at).total_seconds())
2075
+
2076
+ max_unblocked_wait = (
2077
+ max(pending_unblocked_waits) if pending_unblocked_waits else None
2078
+ )
2079
+
2008
2080
  return {
2009
2081
  "n_pending": len(pending_runs),
2010
2082
  "n_running": len(running_runs),
2011
2083
  "pending_runs_wait_time_max_secs": max_pending_wait,
2012
2084
  "pending_runs_wait_time_med_secs": med_pending_wait,
2085
+ "pending_unblocked_runs_wait_time_max_secs": max_unblocked_wait,
2013
2086
  }
2014
2087
 
2015
2088
  @staticmethod
@@ -2410,12 +2483,14 @@ class Runs(Authenticated):
2410
2483
  action: Literal["interrupt", "rollback"] = "interrupt",
2411
2484
  thread_id: UUID | None = None,
2412
2485
  status: Literal["pending", "running", "all"] | None = None,
2486
+ assistant_id: UUID | None = None,
2413
2487
  ctx: Auth.types.BaseAuthContext | None = None,
2414
2488
  ) -> None:
2415
2489
  """
2416
2490
  Cancel runs in memory. Must provide either:
2417
2491
  1) thread_id + run_ids, or
2418
- 2) status in {"pending", "running", "all"}.
2492
+ 2) status in {"pending", "running", "all"}, or
2493
+ 3) assistant_id (cancels all in-flight runs for that assistant).
2419
2494
 
2420
2495
  Steps:
2421
2496
  - Validate arguments (one usage pattern or the other).
@@ -2426,10 +2501,18 @@ class Runs(Authenticated):
2426
2501
  * If 'pending', set to 'interrupted' or delete (if action='rollback' and not actively queued).
2427
2502
  * If 'running', the worker will pick up the message.
2428
2503
  * Otherwise, log a warning for non-cancelable states.
2429
- - 404 if no runs are found or authorized.
2504
+ - 404 if no runs are found or authorized (unless assistant_id is provided).
2430
2505
  """
2431
2506
  # 1. Validate arguments
2432
- if status is not None:
2507
+ if assistant_id is not None:
2508
+ # If assistant_id is set, user must NOT specify other filters
2509
+ if thread_id is not None or run_ids is not None or status is not None:
2510
+ raise HTTPException(
2511
+ status_code=422,
2512
+ detail="Cannot specify 'thread_id', 'run_ids', or 'status' when using 'assistant_id'",
2513
+ )
2514
+ assistant_id = _ensure_uuid(assistant_id)
2515
+ elif status is not None:
2433
2516
  # If status is set, user must NOT specify thread_id or run_ids
2434
2517
  if thread_id is not None or run_ids is not None:
2435
2518
  raise HTTPException(
@@ -2441,7 +2524,7 @@ class Runs(Authenticated):
2441
2524
  if thread_id is None or run_ids is None:
2442
2525
  raise HTTPException(
2443
2526
  status_code=422,
2444
- detail="Must provide either a status or both 'thread_id' and 'run_ids'",
2527
+ detail="Must provide either a status, an assistant_id, or both 'thread_id' and 'run_ids'",
2445
2528
  )
2446
2529
 
2447
2530
  # Convert and normalize inputs
@@ -2476,7 +2559,12 @@ class Runs(Authenticated):
2476
2559
  """
2477
2560
  Check whether a run in `conn.store["runs"]` meets the selection criteria.
2478
2561
  """
2479
- if status_list:
2562
+ if assistant_id is not None:
2563
+ return r["assistant_id"] == assistant_id and r["status"] in (
2564
+ "pending",
2565
+ "running",
2566
+ )
2567
+ elif status_list:
2480
2568
  return r["status"] in status_list
2481
2569
  else:
2482
2570
  return r["thread_id"] == thread_id and r["run_id"] in run_ids # type: ignore
@@ -2497,6 +2585,9 @@ class Runs(Authenticated):
2497
2585
  # on thread. If your filters also apply to runs, you might do more checks here.
2498
2586
 
2499
2587
  if not candidate_runs:
2588
+ # When cancelling by assistant_id, it's valid to have no runs
2589
+ if assistant_id is not None:
2590
+ return
2500
2591
  raise HTTPException(status_code=404, detail="No runs found to cancel.")
2501
2592
 
2502
2593
  stream_manager = get_stream_manager()
@@ -2545,6 +2636,9 @@ class Runs(Authenticated):
2545
2636
  )
2546
2637
 
2547
2638
  if not cancelable_runs:
2639
+ # When cancelling by assistant_id, it's valid to have no cancelable runs
2640
+ if assistant_id is not None:
2641
+ return
2548
2642
  raise HTTPException(
2549
2643
  status_code=404,
2550
2644
  detail="No matching runs to cancel. Please verify the thread ID and run IDs are correct, and the runs haven't been deleted or completed.",
@@ -2671,7 +2765,10 @@ class Runs(Authenticated):
2671
2765
  await Runs.Stream.check_run_stream_auth(run_id, thread_id, ctx)
2672
2766
  except HTTPException as e:
2673
2767
  raise WrappedHTTPException(e) from None
2674
- run = await Runs.get(conn, run_id, thread_id=thread_id, ctx=ctx)
2768
+ run_iter = await Runs.get(
2769
+ conn, run_id, thread_id=thread_id, ctx=ctx
2770
+ )
2771
+ run = await anext(run_iter, None)
2675
2772
 
2676
2773
  for message in get_stream_manager().restore_messages(
2677
2774
  run_id, thread_id, last_event_id
@@ -2727,7 +2824,13 @@ class Runs(Authenticated):
2727
2824
  and mode.startswith("messages")
2728
2825
  )
2729
2826
  ):
2730
- yield mode.encode(), payload, id
2827
+ # We only return a stream ID if the run is resumable
2828
+ stream_id = (
2829
+ id
2830
+ if run.get("kwargs", {}).get("resumable")
2831
+ else None
2832
+ )
2833
+ yield mode.encode(), payload, stream_id
2731
2834
  logger.debug(
2732
2835
  "Streamed run event",
2733
2836
  run_id=str(run_id),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-runtime-inmem
3
- Version: 0.20.1
3
+ Version: 0.21.0
4
4
  Summary: Inmem implementation for the LangGraph API server.
5
5
  Author-email: Will Fu-Hinthorn <will@langchain.dev>
6
6
  License: Elastic-2.0
@@ -1,13 +1,13 @@
1
- langgraph_runtime_inmem/__init__.py,sha256=e4M7aySmFiZUS_yTkC-q-Qi2j5olmmHDojVLRwD-HMU,311
2
- langgraph_runtime_inmem/checkpoint.py,sha256=nc1G8DqVdIu-ibjKTqXfbPfMbAsKjPObKqegrSzo6Po,4432
1
+ langgraph_runtime_inmem/__init__.py,sha256=oQHrzUbBlmokOOOztcFB8u7eCJvS0aPyNTrKSpqJ0to,311
2
+ langgraph_runtime_inmem/checkpoint.py,sha256=TOuvWt3sqQPCmLjr1HZr6JAsinqUmmFr1hhhCbp2Lzo,6728
3
3
  langgraph_runtime_inmem/database.py,sha256=g2XYa5KN-T8MbDeFH9sfUApDG62Wp4BACumVnDtxYhI,6403
4
4
  langgraph_runtime_inmem/inmem_stream.py,sha256=PFLWbsxU8RqbT5mYJgNk6v5q6TWJRIY1hkZWhJF8nkI,9094
5
5
  langgraph_runtime_inmem/lifespan.py,sha256=fCoYcN_h0cxmj6-muC-f0csPdSpyepZuGRD1yBrq4XM,4755
6
6
  langgraph_runtime_inmem/metrics.py,sha256=_YiSkLnhQvHpMktk38SZo0abyL-5GihfVAtBo0-lFIc,403
7
- langgraph_runtime_inmem/ops.py,sha256=qNK_-isTB92kQi3vVSt84__gMG02JUqmkNh-9LURexo,114772
7
+ langgraph_runtime_inmem/ops.py,sha256=Im4G26eN7vLWELvj7WZQhm9x6UxzpP48-Bytye1v2pU,119002
8
8
  langgraph_runtime_inmem/queue.py,sha256=WM6ZJu25QPVjFXeJYW06GALLUgRsnRrA4YdypR0oG0U,9584
9
9
  langgraph_runtime_inmem/retry.py,sha256=XmldOP4e_H5s264CagJRVnQMDFcEJR_dldVR1Hm5XvM,763
10
10
  langgraph_runtime_inmem/store.py,sha256=rTfL1JJvd-j4xjTrL8qDcynaWF6gUJ9-GDVwH0NBD_I,3506
11
- langgraph_runtime_inmem-0.20.1.dist-info/METADATA,sha256=nMzYXp5OXuPOROcMYfi8EEAa-8ENdpX5t5H35xV1ww4,570
12
- langgraph_runtime_inmem-0.20.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
- langgraph_runtime_inmem-0.20.1.dist-info/RECORD,,
11
+ langgraph_runtime_inmem-0.21.0.dist-info/METADATA,sha256=CO0H61LlKVGta_vsiopcmGNdV8hbEGuXTp-_nZXhtbA,570
12
+ langgraph_runtime_inmem-0.21.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
+ langgraph_runtime_inmem-0.21.0.dist-info/RECORD,,