langgraph-runtime-inmem 0.28.1__tar.gz → 0.29.0.dev1__tar.gz

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 (18) hide show
  1. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/PKG-INFO +1 -1
  2. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/__init__.py +1 -1
  3. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/ops.py +119 -42
  4. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/uv.lock +3 -3
  5. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/.gitignore +0 -0
  6. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/Makefile +0 -0
  7. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/README.md +0 -0
  8. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/_persistence.py +0 -0
  9. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/checkpoint.py +0 -0
  10. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/database.py +0 -0
  11. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/inmem_stream.py +0 -0
  12. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/lifespan.py +0 -0
  13. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/metrics.py +0 -0
  14. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/queue.py +0 -0
  15. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/retry.py +0 -0
  16. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/routes.py +0 -0
  17. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/store.py +0 -0
  18. {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-runtime-inmem
3
- Version: 0.28.1
3
+ Version: 0.29.0.dev1
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
@@ -10,7 +10,7 @@ from langgraph_runtime_inmem import (
10
10
  store,
11
11
  )
12
12
 
13
- __version__ = "0.28.1"
13
+ __version__ = "0.29.0.dev1"
14
14
  __all__ = [
15
15
  "ops",
16
16
  "database",
@@ -1949,6 +1949,27 @@ class Threads(Authenticated):
1949
1949
  stream_modes: list[ThreadStreamMode],
1950
1950
  ctx: Auth.types.BaseAuthContext | None = None,
1951
1951
  ) -> AsyncIterator[tuple[bytes, bytes, bytes | None]]:
1952
+ async for (
1953
+ event,
1954
+ payload,
1955
+ stream_id,
1956
+ _run_id,
1957
+ ) in Threads.Stream.join_event_streaming(
1958
+ thread_id,
1959
+ last_event_id=last_event_id,
1960
+ stream_modes=stream_modes,
1961
+ ctx=ctx,
1962
+ ):
1963
+ yield event, payload, stream_id
1964
+
1965
+ @staticmethod
1966
+ async def join_event_streaming(
1967
+ thread_id: UUID,
1968
+ *,
1969
+ last_event_id: str | None = None,
1970
+ stream_modes: list[ThreadStreamMode],
1971
+ ctx: Auth.types.BaseAuthContext | None = None,
1972
+ ) -> AsyncIterator[tuple[bytes, bytes, bytes | None, str | None]]:
1952
1973
  """Stream the thread output."""
1953
1974
  await Threads.Stream.check_thread_stream_auth(thread_id, ctx)
1954
1975
 
@@ -1986,11 +2007,14 @@ class Threads(Authenticated):
1986
2007
 
1987
2008
  # Restore messages if resuming from a specific event
1988
2009
  if last_event_id is not None:
1989
- # Collect all events from all message stores for this thread
2010
+ # ``message_stores`` is keyed by ``UUID`` (see
2011
+ # :meth:`StreamManager.put`). Callers can hand us
2012
+ # ``thread_id`` as either ``str`` or ``UUID``, so
2013
+ # normalize before the lookup — otherwise replay
2014
+ # always misses and yields nothing.
2015
+ store_key = _ensure_uuid(thread_id)
1990
2016
  all_events = []
1991
- for run_id in stream_manager.message_stores.get(
1992
- str(thread_id), []
1993
- ):
2017
+ for run_id in stream_manager.message_stores.get(store_key, []):
1994
2018
  for message in stream_manager.restore_messages(
1995
2019
  run_id, thread_id, last_event_id
1996
2020
  ):
@@ -2020,9 +2044,20 @@ class Threads(Authenticated):
2020
2044
  event_bytes,
2021
2045
  message_bytes,
2022
2046
  message.id,
2047
+ str(run_id),
2023
2048
  )
2024
2049
 
2025
- # Listen for live messages from all queues
2050
+ # Listen for live messages from all queues.
2051
+ #
2052
+ # Hot loop is non-blocking: a burst of N events drains in
2053
+ # one outer-loop iteration via ``get_nowait``, instead of
2054
+ # the previous "one event per queue per 200ms timeout"
2055
+ # pattern that throttled fast-publishing runs (the empty
2056
+ # thread-stream queue alone forced every iteration to
2057
+ # wait the full timeout). When everything is idle we fall
2058
+ # back to a short ``asyncio.sleep`` so new runs joining
2059
+ # the thread get picked up by the next ``subscribe``
2060
+ # without burning CPU.
2026
2061
  while True:
2027
2062
  # Refresh queues to pick up any new runs that joined this thread
2028
2063
  new_queue_tuples = await Threads.Stream.subscribe(
@@ -2032,40 +2067,69 @@ class Threads(Authenticated):
2032
2067
  for run_id, queue in new_queue_tuples:
2033
2068
  created_queues.append((run_id, queue))
2034
2069
 
2070
+ drained_any = False
2035
2071
  for run_id, queue in created_queues:
2036
- try:
2037
- message = await asyncio.wait_for(
2038
- queue.get(), timeout=0.2
2039
- )
2040
- decoded = decode_stream_message(
2041
- message.data, channel=message.topic
2042
- )
2072
+ while True:
2073
+ try:
2074
+ message = queue.get_nowait()
2075
+ except asyncio.QueueEmpty:
2076
+ break
2077
+ try:
2078
+ decoded = decode_stream_message(
2079
+ message.data, channel=message.topic
2080
+ )
2081
+ except (ValueError, KeyError):
2082
+ continue
2043
2083
  event = decoded.event_bytes
2044
2084
  event_name = event.decode("utf-8")
2045
2085
  payload = decoded.message_bytes
2046
2086
 
2047
2087
  if event == b"control" and payload == b"done":
2088
+ # Don't shadow the queue-iteration
2089
+ # ``run_id`` with the topic-extracted
2090
+ # string — non-control events later in
2091
+ # this drain pass would yield the
2092
+ # rebound value. Wire output is
2093
+ # identical (``str(UUID)`` matches the
2094
+ # topic suffix), but the rebinding is
2095
+ # fragile if the topic format moves.
2048
2096
  topic = message.topic.decode()
2049
- run_id = topic.split("run:")[1].split(":")[0]
2097
+ done_run_id = topic.split("run:")[1].split(":")[0]
2050
2098
  meta_event = b"metadata"
2051
2099
  meta_payload = orjson.dumps(
2052
- {"status": "run_done", "run_id": run_id}
2100
+ {"status": "run_done", "run_id": done_run_id}
2053
2101
  )
2054
2102
  if not should_filter_event(
2055
2103
  "metadata", meta_payload
2056
2104
  ):
2057
- yield (meta_event, meta_payload, message.id)
2105
+ yield (
2106
+ meta_event,
2107
+ meta_payload,
2108
+ message.id,
2109
+ done_run_id,
2110
+ )
2111
+ drained_any = True
2058
2112
  else:
2059
2113
  if not should_filter_event(event_name, payload):
2060
- yield (event, payload, message.id)
2061
-
2062
- except TimeoutError:
2063
- continue
2064
- except (ValueError, KeyError):
2065
- continue
2066
-
2067
- # Yield execution to other tasks to prevent event loop starvation
2068
- await asyncio.sleep(0)
2114
+ yield (
2115
+ event,
2116
+ payload,
2117
+ message.id,
2118
+ str(run_id),
2119
+ )
2120
+ drained_any = True
2121
+
2122
+ if drained_any:
2123
+ # Yield once so other tasks (worker, send) can
2124
+ # advance, then loop immediately to drain any
2125
+ # follow-up burst.
2126
+ await asyncio.sleep(0)
2127
+ else:
2128
+ # All queues empty — short poll interval keeps
2129
+ # ``subscribe`` rechecking for newly-spawned
2130
+ # runs and lets the worker emit without a
2131
+ # >5-events-per-second delivery cap.
2132
+ await asyncio.sleep(0.02)
2069
2133
 
2070
2134
  except WrappedHTTPException as e:
2071
2135
  raise e.http_exception from None
@@ -2913,12 +2977,18 @@ class Runs(Authenticated):
2913
2977
  run_id
2914
2978
  ):
2915
2979
  for control_queue in control_queues:
2916
- try:
2917
- while True:
2918
- control_msg = control_queue.get()
2919
- await queue.put(control_msg)
2920
- except asyncio.QueueEmpty:
2921
- pass
2980
+ # NOTE: must use ``get_nowait``. ``asyncio.Queue.get`` is a
2981
+ # coroutine — calling it without ``await`` returns a
2982
+ # coroutine object and never raises ``QueueEmpty``, which
2983
+ # turns this drain into an infinite loop that blocks the
2984
+ # event loop (the coroutine objects get pushed straight
2985
+ # into ``queue`` via ``put_nowait`` with no yield point).
2986
+ while True:
2987
+ try:
2988
+ control_msg = control_queue.get_nowait()
2989
+ except asyncio.QueueEmpty:
2990
+ break
2991
+ await queue.put(control_msg)
2922
2992
  return queue
2923
2993
 
2924
2994
  @staticmethod
@@ -3502,16 +3572,18 @@ class Crons(Authenticated):
3502
3572
  ctx: Auth.types.BaseAuthContext | None = None,
3503
3573
  sort_by: str | None = None,
3504
3574
  sort_order: Literal["asc", "desc"] | None = None,
3575
+ metadata: dict | None = None,
3505
3576
  ) -> tuple[AsyncIterator[Cron], int | None]:
3506
3577
  filters = await Crons.handle_event(
3507
3578
  ctx,
3508
3579
  "search",
3509
- Auth.types.CronsSearch(
3510
- assistant_id=assistant_id,
3511
- thread_id=thread_id,
3512
- limit=limit,
3513
- offset=offset,
3514
- ),
3580
+ {
3581
+ "assistant_id": assistant_id,
3582
+ "thread_id": thread_id,
3583
+ "limit": limit,
3584
+ "offset": offset,
3585
+ "metadata": metadata or {},
3586
+ },
3515
3587
  )
3516
3588
 
3517
3589
  if thread_id:
@@ -3535,6 +3607,7 @@ class Crons(Authenticated):
3535
3607
  if (assistant_id is None or str(c["assistant_id"]) == str(assistant_id))
3536
3608
  and (thread_id is None or str(c.get("thread_id")) == str(thread_id))
3537
3609
  and (enabled is None or c.get("enabled") == enabled)
3610
+ and (not metadata or is_jsonb_contained(c.get("metadata", {}), metadata))
3538
3611
  and (not filters or _check_filter_match(c.get("metadata", {}), filters))
3539
3612
  ]
3540
3613
 
@@ -3616,17 +3689,19 @@ class Crons(Authenticated):
3616
3689
  assistant_id: UUID | None = None,
3617
3690
  thread_id: UUID | None = None,
3618
3691
  ctx: Auth.types.BaseAuthContext | None = None,
3692
+ metadata: dict | None = None,
3619
3693
  ) -> int:
3620
3694
  """Get count of crons."""
3621
3695
  filters = await Crons.handle_event(
3622
3696
  ctx,
3623
3697
  "search",
3624
- Auth.types.CronsSearch(
3625
- assistant_id=assistant_id,
3626
- thread_id=thread_id,
3627
- limit=0,
3628
- offset=0,
3629
- ),
3698
+ {
3699
+ "assistant_id": assistant_id,
3700
+ "thread_id": thread_id,
3701
+ "limit": 0,
3702
+ "offset": 0,
3703
+ "metadata": metadata or {},
3704
+ },
3630
3705
  )
3631
3706
 
3632
3707
  if thread_id:
@@ -3649,6 +3724,8 @@ class Crons(Authenticated):
3649
3724
  continue
3650
3725
  if thread_id is not None and str(c.get("thread_id")) != str(thread_id):
3651
3726
  continue
3727
+ if metadata and not is_jsonb_contained(c.get("metadata", {}), metadata):
3728
+ continue
3652
3729
  if filters and not _check_filter_match(c.get("metadata", {}), filters):
3653
3730
  continue
3654
3731
  filtered_crons.append(c)
@@ -1030,11 +1030,11 @@ wheels = [
1030
1030
 
1031
1031
  [[package]]
1032
1032
  name = "urllib3"
1033
- version = "2.6.3"
1033
+ version = "2.7.0"
1034
1034
  source = { registry = "https://pypi.org/simple" }
1035
- sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
1035
+ sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
1036
1036
  wheels = [
1037
- { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
1037
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
1038
1038
  ]
1039
1039
 
1040
1040
  [[package]]