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.
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/PKG-INFO +1 -1
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/__init__.py +1 -1
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/ops.py +119 -42
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/uv.lock +3 -3
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/.gitignore +0 -0
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/Makefile +0 -0
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/README.md +0 -0
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/_persistence.py +0 -0
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/checkpoint.py +0 -0
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/database.py +0 -0
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/inmem_stream.py +0 -0
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/lifespan.py +0 -0
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/metrics.py +0 -0
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/queue.py +0 -0
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/retry.py +0 -0
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/routes.py +0 -0
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/langgraph_runtime_inmem/store.py +0 -0
- {langgraph_runtime_inmem-0.28.1 → langgraph_runtime_inmem-0.29.0.dev1}/pyproject.toml +0 -0
|
@@ -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
|
-
#
|
|
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
|
-
|
|
2037
|
-
|
|
2038
|
-
queue.
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
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
|
-
|
|
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":
|
|
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 (
|
|
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 (
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
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
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
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
|
-
|
|
3510
|
-
assistant_id
|
|
3511
|
-
thread_id
|
|
3512
|
-
limit
|
|
3513
|
-
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
|
-
|
|
3625
|
-
assistant_id
|
|
3626
|
-
thread_id
|
|
3627
|
-
limit
|
|
3628
|
-
offset
|
|
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.
|
|
1033
|
+
version = "2.7.0"
|
|
1034
1034
|
source = { registry = "https://pypi.org/simple" }
|
|
1035
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
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/
|
|
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]]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|