langgraph-runtime-inmem 0.38.0.dev13__tar.gz → 0.38.0.dev14__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.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/PKG-INFO +1 -1
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/__init__.py +1 -1
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/database.py +33 -3
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/inmem_stream.py +38 -21
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/lifespan.py +14 -0
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/ops.py +131 -16
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/pyproject.toml +5 -1
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/.gitignore +0 -0
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/Makefile +0 -0
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/README.md +0 -0
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/_persistence.py +0 -0
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/checkpoint.py +0 -0
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/encryption.py +0 -0
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/metrics.py +0 -0
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/queue.py +0 -0
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/retry.py +0 -0
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/routes.py +0 -0
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/store.py +0 -0
- {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/uv.lock +0 -0
|
@@ -185,12 +185,27 @@ def _load_ops_file() -> None:
|
|
|
185
185
|
orphaned = [r for r in GLOBAL_STORE.get("runs") or [] if r["status"] == "running"]
|
|
186
186
|
now = datetime.now(UTC)
|
|
187
187
|
for run in orphaned:
|
|
188
|
-
run
|
|
188
|
+
# An interrupt recorded before the crash stands, so that run is not retried. A
|
|
189
|
+
# rollback needs the checkpointer, which starts later: the worker that picks the
|
|
190
|
+
# run up applies it before the graph runs.
|
|
191
|
+
run["status"] = (
|
|
192
|
+
"interrupted" if run.get("cancel_action") == "interrupt" else "pending"
|
|
193
|
+
)
|
|
189
194
|
run["updated_at"] = now
|
|
190
|
-
|
|
195
|
+
# No worker will finish those runs, so nothing else moves their threads off busy.
|
|
196
|
+
stopped = {r["thread_id"] for r in orphaned if r["status"] == "interrupted"}
|
|
197
|
+
if stopped:
|
|
198
|
+
from langgraph_runtime_inmem.ops import _thread_status # noqa: PLC0415
|
|
199
|
+
|
|
200
|
+
conn = InMemConnectionProto()
|
|
201
|
+
for thread in GLOBAL_STORE.get("threads") or []:
|
|
202
|
+
if thread["thread_id"] in stopped:
|
|
203
|
+
thread["status"] = _thread_status(conn, thread["thread_id"])
|
|
204
|
+
retried = [str(r["run_id"]) for r in orphaned if r["status"] == "pending"]
|
|
205
|
+
if retried:
|
|
191
206
|
logger.warning(
|
|
192
207
|
"Retrying runs left running by a previous process that did not shut down cleanly",
|
|
193
|
-
run_ids=
|
|
208
|
+
run_ids=retried,
|
|
194
209
|
)
|
|
195
210
|
|
|
196
211
|
|
|
@@ -206,6 +221,19 @@ def upgrade_thread_status_facts(store: dict[str, Any]) -> None:
|
|
|
206
221
|
store.setdefault("thread_failed", {})
|
|
207
222
|
|
|
208
223
|
|
|
224
|
+
def upgrade_thread_priority_settings(store: dict[str, Any]) -> None:
|
|
225
|
+
"""Give threads saved by older versions, before priority settings, none yet."""
|
|
226
|
+
for thread in store.get("threads") or ():
|
|
227
|
+
for key in ("run_priority", "fairness_group"):
|
|
228
|
+
thread.setdefault(key, None)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def upgrade_run_priorities(store: dict[str, Any]) -> None:
|
|
232
|
+
"""Give runs saved by older versions, before runs had a priority, the default level."""
|
|
233
|
+
for run in store.get("runs") or ():
|
|
234
|
+
run.setdefault("priority", 0)
|
|
235
|
+
|
|
236
|
+
|
|
209
237
|
def _set_aside_ops_file(reason: Exception) -> None:
|
|
210
238
|
"""Rename an unloadable ops file to ``<file>.bak-<UTC timestamp>`` and log why."""
|
|
211
239
|
from langgraph_api.graph import graph_file_module_names # noqa: PLC0415
|
|
@@ -293,6 +321,8 @@ async def start_pool() -> None:
|
|
|
293
321
|
if not GLOBAL_STORE.get(k):
|
|
294
322
|
GLOBAL_STORE[k] = []
|
|
295
323
|
upgrade_thread_status_facts(GLOBAL_STORE)
|
|
324
|
+
upgrade_run_priorities(GLOBAL_STORE)
|
|
325
|
+
upgrade_thread_priority_settings(GLOBAL_STORE)
|
|
296
326
|
# Remove agents that were created by the system to avoid removed graphs
|
|
297
327
|
if GLOBAL_STORE.get("assistants"):
|
|
298
328
|
for a in GLOBAL_STORE["assistants"]:
|
|
@@ -13,12 +13,19 @@ def _ensure_uuid(id: str | UUID) -> UUID:
|
|
|
13
13
|
return UUID(id) if isinstance(id, str) else id
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
def stream_id_key(stream_id: str | bytes) -> tuple[int, int]:
|
|
17
|
+
"""Numeric order of a `<ms>-<seq>` stream id, for comparing resume cursors.
|
|
18
|
+
|
|
19
|
+
Clients send `-` or a negative id such as `-1` to replay everything, so
|
|
20
|
+
those sort first, and `<ms>` or `<ms>-*` to replay that whole millisecond.
|
|
21
|
+
Raises ValueError for an id without a numeric `<ms>`.
|
|
22
|
+
"""
|
|
23
|
+
if isinstance(stream_id, bytes):
|
|
24
|
+
stream_id = stream_id.decode()
|
|
25
|
+
if stream_id.startswith("-"):
|
|
26
|
+
return (-1, -1)
|
|
27
|
+
ms, _, seq = stream_id.partition("-")
|
|
28
|
+
return int(ms), int(seq) if seq.isdigit() else -1
|
|
22
29
|
|
|
23
30
|
|
|
24
31
|
@dataclass
|
|
@@ -65,6 +72,14 @@ class StreamManager:
|
|
|
65
72
|
self.message_stores = defaultdict(
|
|
66
73
|
lambda: defaultdict(list[Message])
|
|
67
74
|
) # Dict[str, List[Message]]
|
|
75
|
+
self._last_id = (0, -1)
|
|
76
|
+
|
|
77
|
+
def _next_id(self) -> bytes:
|
|
78
|
+
"""Strictly increasing, as a resume replays only the ids above its cursor."""
|
|
79
|
+
ms = int(time.time() * 1000)
|
|
80
|
+
last_ms, last_seq = self._last_id
|
|
81
|
+
self._last_id = (ms, 0) if ms > last_ms else (last_ms, last_seq + 1)
|
|
82
|
+
return f"{self._last_id[0]}-{self._last_id[1]}".encode()
|
|
68
83
|
|
|
69
84
|
def get_queues(
|
|
70
85
|
self, run_id: UUID | str, thread_id: UUID | str | None
|
|
@@ -109,8 +124,7 @@ class StreamManager:
|
|
|
109
124
|
else:
|
|
110
125
|
thread_id = _ensure_uuid(thread_id)
|
|
111
126
|
|
|
112
|
-
message.id =
|
|
113
|
-
# For resumable run streams, embed the generated message ID into the frame
|
|
127
|
+
message.id = self._next_id()
|
|
114
128
|
topic = message.topic.decode()
|
|
115
129
|
if resumable:
|
|
116
130
|
self.message_stores[thread_id][run_id].append(message)
|
|
@@ -131,8 +145,8 @@ class StreamManager:
|
|
|
131
145
|
message: Message,
|
|
132
146
|
) -> None:
|
|
133
147
|
thread_id = _ensure_uuid(thread_id)
|
|
134
|
-
message.id =
|
|
135
|
-
queues = self.thread_streams
|
|
148
|
+
message.id = self._next_id()
|
|
149
|
+
queues = self.thread_streams.get(thread_id, ())
|
|
136
150
|
coros = [queue.put(message) for queue in queues]
|
|
137
151
|
results = await asyncio.gather(*coros, return_exceptions=True)
|
|
138
152
|
for result in results:
|
|
@@ -182,6 +196,13 @@ class StreamManager:
|
|
|
182
196
|
if not self.queues[thread_id][run_id]:
|
|
183
197
|
del self.queues[thread_id][run_id]
|
|
184
198
|
|
|
199
|
+
async def remove_thread_stream(self, thread_id: UUID | str, queue: asyncio.Queue):
|
|
200
|
+
thread_id = _ensure_uuid(thread_id)
|
|
201
|
+
if thread_id in self.thread_streams:
|
|
202
|
+
self.thread_streams[thread_id].remove(queue)
|
|
203
|
+
if not self.thread_streams[thread_id]:
|
|
204
|
+
del self.thread_streams[thread_id]
|
|
205
|
+
|
|
185
206
|
async def remove_control_queue(
|
|
186
207
|
self, run_id: UUID | str, thread_id: UUID | str | None, queue: asyncio.Queue
|
|
187
208
|
):
|
|
@@ -207,19 +228,15 @@ class StreamManager:
|
|
|
207
228
|
thread_id = THREADLESS_KEY
|
|
208
229
|
else:
|
|
209
230
|
thread_id = _ensure_uuid(thread_id)
|
|
210
|
-
if message_id is None:
|
|
231
|
+
if message_id is None or thread_id not in self.message_stores:
|
|
211
232
|
return
|
|
212
233
|
try:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
# Try integer format if ms-seq fails
|
|
220
|
-
message_idx = int(message_id) + 1
|
|
221
|
-
if run_id in self.message_stores:
|
|
222
|
-
yield from self.message_stores[thread_id][run_id][message_idx:]
|
|
234
|
+
after = stream_id_key(message_id)
|
|
235
|
+
except ValueError:
|
|
236
|
+
return
|
|
237
|
+
for message in self.message_stores[thread_id][run_id]:
|
|
238
|
+
if stream_id_key(message.id) > after:
|
|
239
|
+
yield message
|
|
223
240
|
|
|
224
241
|
def get_queues_by_thread_id(self, thread_id: UUID | str) -> list[asyncio.Queue]:
|
|
225
242
|
"""Get all queues for a specific thread_id across all runs."""
|
|
@@ -136,6 +136,20 @@ async def lifespan(
|
|
|
136
136
|
_LAST_LIFESPAN_ERROR = exc
|
|
137
137
|
await _log_graph_load_failure(exc)
|
|
138
138
|
raise
|
|
139
|
+
try:
|
|
140
|
+
from langgraph_api.priority_settings import ( # noqa: PLC0415
|
|
141
|
+
get_priority_callback,
|
|
142
|
+
)
|
|
143
|
+
except ImportError:
|
|
144
|
+
# An older langgraph-api has no priority callback.
|
|
145
|
+
pass
|
|
146
|
+
else:
|
|
147
|
+
# Loaded now so a bad path fails startup, not every run create.
|
|
148
|
+
if get_priority_callback() is not None:
|
|
149
|
+
await logger.awarning(
|
|
150
|
+
"The in-memory runtime stores each thread's priority settings"
|
|
151
|
+
" but starts runs in the order they were created"
|
|
152
|
+
)
|
|
139
153
|
if config.N_JOBS_PER_WORKER > 0:
|
|
140
154
|
tg.create_task(queue_with_signal())
|
|
141
155
|
|
|
@@ -32,6 +32,7 @@ from langgraph_runtime_inmem.inmem_stream import (
|
|
|
32
32
|
ContextQueue,
|
|
33
33
|
Message,
|
|
34
34
|
get_stream_manager,
|
|
35
|
+
stream_id_key,
|
|
35
36
|
)
|
|
36
37
|
|
|
37
38
|
if typing.TYPE_CHECKING:
|
|
@@ -1404,6 +1405,8 @@ class Threads(Authenticated):
|
|
|
1404
1405
|
"status": "idle",
|
|
1405
1406
|
"config": {},
|
|
1406
1407
|
"values": None,
|
|
1408
|
+
"run_priority": None,
|
|
1409
|
+
"fairness_group": None,
|
|
1407
1410
|
}
|
|
1408
1411
|
|
|
1409
1412
|
# Add to store
|
|
@@ -1560,6 +1563,9 @@ class Threads(Authenticated):
|
|
|
1560
1563
|
|
|
1561
1564
|
now = datetime.now(UTC)
|
|
1562
1565
|
|
|
1566
|
+
if run.get("cancel_action") == "rollback":
|
|
1567
|
+
# A rollback recorded while the run ran wins over the signal its worker saw first, as in Go.
|
|
1568
|
+
run_status, checkpoint, exception = "rollback", None, None
|
|
1563
1569
|
if run_status == "rollback":
|
|
1564
1570
|
await Runs.delete(conn, run_id, thread_id=run["thread_id"])
|
|
1565
1571
|
else:
|
|
@@ -1771,6 +1777,9 @@ class Threads(Authenticated):
|
|
|
1771
1777
|
"values": copy.deepcopy(original_thread.get("values")),
|
|
1772
1778
|
"interrupts": copy.deepcopy(original_thread.get("interrupts", {})),
|
|
1773
1779
|
"error": copy.deepcopy(original_thread.get("error")),
|
|
1780
|
+
# A copy gets its own priority settings at its first run.
|
|
1781
|
+
"run_priority": None,
|
|
1782
|
+
"fairness_group": None,
|
|
1774
1783
|
}
|
|
1775
1784
|
# Add new thread to store
|
|
1776
1785
|
conn.store["threads"].append(
|
|
@@ -2412,8 +2421,7 @@ class Threads(Authenticated):
|
|
|
2412
2421
|
):
|
|
2413
2422
|
all_events.append((message, run_id))
|
|
2414
2423
|
|
|
2415
|
-
|
|
2416
|
-
all_events.sort(key=lambda x: x[0].id.decode())
|
|
2424
|
+
all_events.sort(key=lambda x: stream_id_key(x[0].id))
|
|
2417
2425
|
|
|
2418
2426
|
# Yield sorted events
|
|
2419
2427
|
for message, run_id in all_events:
|
|
@@ -2535,9 +2543,14 @@ class Threads(Authenticated):
|
|
|
2535
2543
|
raise
|
|
2536
2544
|
finally:
|
|
2537
2545
|
# Clean up all created queues
|
|
2546
|
+
thread_key = _ensure_uuid(thread_id)
|
|
2538
2547
|
for run_id, queue in created_queues:
|
|
2539
2548
|
try:
|
|
2540
|
-
|
|
2549
|
+
# subscribe() files the thread-level queue under the thread id.
|
|
2550
|
+
if run_id == thread_key:
|
|
2551
|
+
await stream_manager.remove_thread_stream(thread_id, queue)
|
|
2552
|
+
else:
|
|
2553
|
+
await stream_manager.remove_queue(run_id, thread_id, queue)
|
|
2541
2554
|
except Exception:
|
|
2542
2555
|
# Ignore cleanup errors
|
|
2543
2556
|
pass
|
|
@@ -2877,6 +2890,17 @@ class Runs(Authenticated):
|
|
|
2877
2890
|
from langgraph_api.schema import Run, Thread # noqa: PLC0415
|
|
2878
2891
|
from langgraph_api.utils import uuid7 # noqa: PLC0415
|
|
2879
2892
|
|
|
2893
|
+
try:
|
|
2894
|
+
from langgraph_api.priority_settings import ( # noqa: PLC0415
|
|
2895
|
+
assign_priority_settings,
|
|
2896
|
+
get_priority_callback,
|
|
2897
|
+
)
|
|
2898
|
+
except ImportError:
|
|
2899
|
+
# An older langgraph-api has no priority callback.
|
|
2900
|
+
priority_callback = None
|
|
2901
|
+
else:
|
|
2902
|
+
priority_callback = get_priority_callback()
|
|
2903
|
+
|
|
2880
2904
|
assistant_id = _ensure_uuid(assistant_id)
|
|
2881
2905
|
assistant = next(
|
|
2882
2906
|
(a for a in conn.store["assistants"] if a["assistant_id"] == assistant_id),
|
|
@@ -2949,6 +2973,53 @@ class Runs(Authenticated):
|
|
|
2949
2973
|
|
|
2950
2974
|
_reject_existing_run(conn.store["runs"], run_id)
|
|
2951
2975
|
|
|
2976
|
+
# A thread gets its priority settings at its first run, from the deployment's
|
|
2977
|
+
# priority callback, before the thread is created or updated. They're kept as
|
|
2978
|
+
# the callback returned them; inmem never orders runs by them.
|
|
2979
|
+
priority_settings = None
|
|
2980
|
+
creates_thread = not existing_thread and (
|
|
2981
|
+
thread_id is None or if_not_exists == "create"
|
|
2982
|
+
)
|
|
2983
|
+
if priority_callback is not None and (
|
|
2984
|
+
creates_thread
|
|
2985
|
+
or (
|
|
2986
|
+
existing_thread is not None
|
|
2987
|
+
and existing_thread.get("run_priority") is None
|
|
2988
|
+
)
|
|
2989
|
+
):
|
|
2990
|
+
ids = {"graph_id": assistant["graph_id"], "assistant_id": str(assistant_id)}
|
|
2991
|
+
priority_settings = await assign_priority_settings(
|
|
2992
|
+
priority_callback,
|
|
2993
|
+
thread_id=str(thread_id) if thread_id is not None else None,
|
|
2994
|
+
thread_metadata=(
|
|
2995
|
+
{**ids, **(config.get("metadata") or {}), **metadata}
|
|
2996
|
+
if creates_thread
|
|
2997
|
+
else Runs._merge_jsonb(existing_thread["metadata"], ids)
|
|
2998
|
+
),
|
|
2999
|
+
assistant_id=str(assistant_id),
|
|
3000
|
+
graph_id=assistant["graph_id"],
|
|
3001
|
+
assistant_metadata=assistant.get("metadata") or {},
|
|
3002
|
+
run_metadata=metadata,
|
|
3003
|
+
run_kwargs=kwargs,
|
|
3004
|
+
)
|
|
3005
|
+
# Another first run on the thread may have gone ahead while the callback
|
|
3006
|
+
# ran: read the thread again, and keep its settings if it has them now.
|
|
3007
|
+
if thread_id is not None:
|
|
3008
|
+
existing_thread = next(
|
|
3009
|
+
(t for t in conn.store["threads"] if t["thread_id"] == thread_id),
|
|
3010
|
+
None,
|
|
3011
|
+
)
|
|
3012
|
+
if encryption_enabled and existing_thread:
|
|
3013
|
+
existing_thread = await _decrypt_from_storage(
|
|
3014
|
+
existing_thread, "thread"
|
|
3015
|
+
)
|
|
3016
|
+
if (
|
|
3017
|
+
existing_thread
|
|
3018
|
+
and filters
|
|
3019
|
+
and not _check_filter_match(existing_thread["metadata"], filters)
|
|
3020
|
+
):
|
|
3021
|
+
return _empty_generator()
|
|
3022
|
+
|
|
2952
3023
|
if not existing_thread and (thread_id is None or if_not_exists == "create"):
|
|
2953
3024
|
# Create new thread
|
|
2954
3025
|
if thread_id is None:
|
|
@@ -2975,10 +3046,18 @@ class Runs(Authenticated):
|
|
|
2975
3046
|
created_at=datetime.now(UTC),
|
|
2976
3047
|
updated_at=datetime.now(UTC),
|
|
2977
3048
|
values=None,
|
|
3049
|
+
run_priority=priority_settings["priority"]
|
|
3050
|
+
if priority_settings
|
|
3051
|
+
else None,
|
|
3052
|
+
fairness_group=(
|
|
3053
|
+
priority_settings["fairness_group"] if priority_settings else None
|
|
3054
|
+
),
|
|
2978
3055
|
)
|
|
2979
3056
|
|
|
2980
|
-
|
|
3057
|
+
# Append before the first await, so a concurrent first run that read the
|
|
3058
|
+
# thread after its priority callback sees this one instead of adding another.
|
|
2981
3059
|
conn.store["threads"].append(thread)
|
|
3060
|
+
await logger.ainfo("Creating thread", thread_id=thread_id)
|
|
2982
3061
|
elif existing_thread:
|
|
2983
3062
|
# Update existing thread
|
|
2984
3063
|
if existing_thread["status"] != "busy":
|
|
@@ -3024,6 +3103,30 @@ class Runs(Authenticated):
|
|
|
3024
3103
|
|
|
3025
3104
|
return _return_inflight()
|
|
3026
3105
|
|
|
3106
|
+
# As in Go core, an existing thread gets the callback's result only once a run
|
|
3107
|
+
# is created on it, and only if it still has no settings; while the deployment
|
|
3108
|
+
# has a callback, every run takes the thread's priority. The check reads the
|
|
3109
|
+
# stored thread, not a decrypted copy, which a concurrent first run's settings
|
|
3110
|
+
# may have reached first.
|
|
3111
|
+
if existing_thread:
|
|
3112
|
+
stored = next(
|
|
3113
|
+
(t for t in conn.store["threads"] if t["thread_id"] == thread_id),
|
|
3114
|
+
existing_thread,
|
|
3115
|
+
)
|
|
3116
|
+
if priority_settings and stored.get("run_priority") is None:
|
|
3117
|
+
stored.update(
|
|
3118
|
+
run_priority=priority_settings["priority"],
|
|
3119
|
+
fairness_group=priority_settings["fairness_group"],
|
|
3120
|
+
)
|
|
3121
|
+
existing_thread.update(
|
|
3122
|
+
{key: stored.get(key) for key in ("run_priority", "fairness_group")}
|
|
3123
|
+
)
|
|
3124
|
+
thread_priority = (
|
|
3125
|
+
(existing_thread or thread).get("run_priority")
|
|
3126
|
+
if priority_callback is not None
|
|
3127
|
+
else None
|
|
3128
|
+
)
|
|
3129
|
+
|
|
3027
3130
|
# Create new run
|
|
3028
3131
|
configurable = Runs._merge_jsonb(
|
|
3029
3132
|
Runs._get_configurable(assistant["config"]),
|
|
@@ -3085,6 +3188,7 @@ class Runs(Authenticated):
|
|
|
3085
3188
|
updated_at=datetime.now(UTC),
|
|
3086
3189
|
langsmith_session_name=langsmith_session_name,
|
|
3087
3190
|
cancel_reason=None,
|
|
3191
|
+
priority=thread_priority if thread_priority is not None else 0,
|
|
3088
3192
|
)
|
|
3089
3193
|
stored_run = new_run
|
|
3090
3194
|
encrypted_thread = None
|
|
@@ -3341,8 +3445,8 @@ class Runs(Authenticated):
|
|
|
3341
3445
|
"""Interrupt every in-flight run accepted at or before `accepted_before`.
|
|
3342
3446
|
|
|
3343
3447
|
Applies no auth filters, so only the operator route may call it. As with
|
|
3344
|
-
`cancel`, running
|
|
3345
|
-
`finishing`
|
|
3448
|
+
`cancel`, a signalled running run stays running until its worker stops it,
|
|
3449
|
+
so `finishing` counts the running runs still being stopped.
|
|
3346
3450
|
"""
|
|
3347
3451
|
boundary_ms = (accepted_before - _EPOCH) // timedelta(milliseconds=1)
|
|
3348
3452
|
|
|
@@ -3403,9 +3507,13 @@ class Runs(Authenticated):
|
|
|
3403
3507
|
cancelable_runs.append(run)
|
|
3404
3508
|
if run["status"] == "pending":
|
|
3405
3509
|
pending_thread_ids.add(run["thread_id"])
|
|
3406
|
-
if
|
|
3407
|
-
# Its worker finalizes the
|
|
3408
|
-
|
|
3510
|
+
if run["status"] == "running":
|
|
3511
|
+
# Its worker finalizes the cancel; staying "running" holds the thread's next run until then.
|
|
3512
|
+
# A later interrupt keeps the first one's reason, which names who stopped the run.
|
|
3513
|
+
if action == "rollback" or not run.get("cancel_action"):
|
|
3514
|
+
run["cancel_reason"] = reason
|
|
3515
|
+
run["cancel_action"] = action
|
|
3516
|
+
run["updated_at"] = datetime.now(tz=UTC)
|
|
3409
3517
|
elif queues or action != "rollback":
|
|
3410
3518
|
run["status"] = "interrupted"
|
|
3411
3519
|
run["cancel_reason"] = reason
|
|
@@ -3717,15 +3825,22 @@ async def listen_for_cancellation(
|
|
|
3717
3825
|
|
|
3718
3826
|
if control_key := stream_manager.get_control_key(run_id, thread_id):
|
|
3719
3827
|
_handle_control_message(control_key)
|
|
3828
|
+
else:
|
|
3829
|
+
# Control messages do not survive a restart; a cancel recorded on the run does.
|
|
3830
|
+
runs = InMemConnectionProto().store["runs"]
|
|
3831
|
+
run = next((r for r in runs if r["run_id"] == run_id), None)
|
|
3832
|
+
if run and run.get("cancel_action"):
|
|
3833
|
+
_handle_control_message(
|
|
3834
|
+
Message(
|
|
3835
|
+
topic=f"run:{run_id}:control".encode(),
|
|
3836
|
+
data=run["cancel_action"].encode(),
|
|
3837
|
+
reason=run.get("cancel_reason"),
|
|
3838
|
+
)
|
|
3839
|
+
)
|
|
3720
3840
|
|
|
3841
|
+
# Runs.enter cancels this task when the run ends.
|
|
3721
3842
|
while not done.is_set():
|
|
3722
|
-
|
|
3723
|
-
# This task gets cancelled when Runs.enter exits anyway,
|
|
3724
|
-
# so we can have a pretty lengthy timeout here
|
|
3725
|
-
message = await asyncio.wait_for(queue.get(), timeout=240)
|
|
3726
|
-
if _handle_control_message(message):
|
|
3727
|
-
break
|
|
3728
|
-
except TimeoutError:
|
|
3843
|
+
if _handle_control_message(await queue.get()):
|
|
3729
3844
|
break
|
|
3730
3845
|
|
|
3731
3846
|
|
{langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/pyproject.toml
RENAMED
|
@@ -65,6 +65,10 @@ exclude = [
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
[tool.ruff]
|
|
68
|
-
lint.select = ["E", "F", "UP", "B", "I", "PLC0415"]
|
|
68
|
+
lint.select = ["E", "F", "UP", "B", "I", "PLC0415", "TID251"]
|
|
69
69
|
lint.ignore = ["E501", "B008"]
|
|
70
70
|
target-version = "py311"
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
|
73
|
+
"asyncio.set_event_loop_policy".msg = "Pass a loop_factory to asyncio.Runner instead."
|
|
74
|
+
"uvloop.install".msg = "Use langgraph_api.asyncio.run_main_loop instead."
|
|
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
|