langgraph-runtime-inmem 0.37.0.dev4__tar.gz → 0.37.0rc1__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.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/PKG-INFO +1 -1
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/langgraph_runtime_inmem/__init__.py +1 -1
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/langgraph_runtime_inmem/database.py +8 -21
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/langgraph_runtime_inmem/ops.py +117 -39
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/.gitignore +0 -0
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/Makefile +0 -0
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/README.md +0 -0
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/langgraph_runtime_inmem/_persistence.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/langgraph_runtime_inmem/checkpoint.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/langgraph_runtime_inmem/encryption.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/langgraph_runtime_inmem/inmem_stream.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/langgraph_runtime_inmem/lifespan.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/langgraph_runtime_inmem/metrics.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/langgraph_runtime_inmem/queue.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/langgraph_runtime_inmem/retry.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/langgraph_runtime_inmem/routes.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/langgraph_runtime_inmem/store.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/pyproject.toml +0 -0
- {langgraph_runtime_inmem-0.37.0.dev4 → langgraph_runtime_inmem-0.37.0rc1}/uv.lock +0 -0
|
@@ -195,28 +195,15 @@ def _load_ops_file() -> None:
|
|
|
195
195
|
|
|
196
196
|
|
|
197
197
|
def upgrade_thread_status_facts(store: dict[str, Any]) -> None:
|
|
198
|
-
"""
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
error, are cleared when the error reads as exactly one; anything else, encrypted
|
|
205
|
-
errors included, stays.
|
|
198
|
+
"""Ensure fact maps exist for ops files saved by older versions.
|
|
199
|
+
|
|
200
|
+
Migration 000066 only adds nullable has_next / failed columns; it does not
|
|
201
|
+
backfill. Writers fill the facts on the next status write, and status
|
|
202
|
+
re-derivation keeps the stored status while either fact is still unknown.
|
|
203
|
+
Cancels that older versions left in ``error`` are left as-is.
|
|
206
204
|
"""
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
for thread in store["threads"]:
|
|
210
|
-
if thread.get("status") == "interrupted":
|
|
211
|
-
has_next.setdefault(thread["thread_id"], True)
|
|
212
|
-
failed.setdefault(thread["thread_id"], thread.get("status") == "error")
|
|
213
|
-
error = thread.get("error")
|
|
214
|
-
if (
|
|
215
|
-
isinstance(error, dict)
|
|
216
|
-
and "__encryption_context__" not in error
|
|
217
|
-
and error.get("error") in ("UserInterrupt", "UserRollback")
|
|
218
|
-
):
|
|
219
|
-
thread["error"] = None
|
|
205
|
+
store.setdefault("thread_has_next", {})
|
|
206
|
+
store.setdefault("thread_failed", {})
|
|
220
207
|
|
|
221
208
|
|
|
222
209
|
def _set_aside_ops_file(reason: Exception) -> None:
|
|
@@ -122,6 +122,34 @@ def _thread_failure(exception: BaseException | None) -> BaseException | None:
|
|
|
122
122
|
return None if isinstance(exception, UserInterrupt) else exception
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
def _derive_thread_status(
|
|
126
|
+
*,
|
|
127
|
+
has_active_runs: bool,
|
|
128
|
+
failed: bool | None,
|
|
129
|
+
has_next: bool | None,
|
|
130
|
+
existing_status: ThreadStatus,
|
|
131
|
+
) -> ThreadStatus:
|
|
132
|
+
"""The one rule for a thread's status from known facts.
|
|
133
|
+
|
|
134
|
+
busy if a run is active; otherwise error if the last run failed; otherwise
|
|
135
|
+
interrupted if the graph has next steps; otherwise keep ``existing_status``
|
|
136
|
+
when ``failed`` or ``has_next`` is still unknown (None); otherwise idle.
|
|
137
|
+
|
|
138
|
+
A leftover busy with no active runs becomes idle.
|
|
139
|
+
"""
|
|
140
|
+
if has_active_runs:
|
|
141
|
+
return "busy"
|
|
142
|
+
if failed:
|
|
143
|
+
return "error"
|
|
144
|
+
if has_next:
|
|
145
|
+
return "interrupted"
|
|
146
|
+
if failed is None or has_next is None:
|
|
147
|
+
if existing_status == "busy":
|
|
148
|
+
return "idle"
|
|
149
|
+
return existing_status
|
|
150
|
+
return "idle"
|
|
151
|
+
|
|
152
|
+
|
|
125
153
|
def _thread_status(
|
|
126
154
|
conn: InMemConnectionProto,
|
|
127
155
|
thread_id: UUID,
|
|
@@ -131,27 +159,29 @@ def _thread_status(
|
|
|
131
159
|
) -> ThreadStatus:
|
|
132
160
|
"""The one rule for a thread's status, used by every write that changes it.
|
|
133
161
|
|
|
134
|
-
busy if a run on the thread is pending or running; otherwise error if the last
|
|
135
|
-
run failed; otherwise interrupted if the graph has next steps (an interrupt or a
|
|
136
|
-
breakpoint); otherwise idle.
|
|
137
|
-
|
|
138
162
|
A nil ``failed`` or ``has_next`` means the caller doesn't know the fact, so it is
|
|
139
|
-
read from what is stored on the thread;
|
|
163
|
+
read from what is stored on the thread; a fact still missing from the store is
|
|
164
|
+
unknown and keeps the thread's current status (see ``_derive_thread_status``).
|
|
140
165
|
"""
|
|
141
|
-
|
|
166
|
+
has_active_runs = any(
|
|
142
167
|
r["thread_id"] == thread_id and r["status"] in ("pending", "running")
|
|
143
168
|
for r in conn.store["runs"]
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return
|
|
169
|
+
)
|
|
170
|
+
if failed is None and thread_id in _thread_failed(conn):
|
|
171
|
+
failed = _thread_failed(conn)[thread_id]
|
|
172
|
+
if has_next is None and thread_id in _thread_has_next(conn):
|
|
173
|
+
has_next = _thread_has_next(conn)[thread_id]
|
|
174
|
+
existing_status: ThreadStatus = "idle"
|
|
175
|
+
for thread in conn.store["threads"]:
|
|
176
|
+
if thread["thread_id"] == thread_id:
|
|
177
|
+
existing_status = thread["status"]
|
|
178
|
+
break
|
|
179
|
+
return _derive_thread_status(
|
|
180
|
+
has_active_runs=has_active_runs,
|
|
181
|
+
failed=failed,
|
|
182
|
+
has_next=has_next,
|
|
183
|
+
existing_status=existing_status,
|
|
184
|
+
)
|
|
155
185
|
|
|
156
186
|
|
|
157
187
|
async def _save_thread_status(
|
|
@@ -209,7 +239,8 @@ async def _save_thread_status(
|
|
|
209
239
|
|
|
210
240
|
def _run_stream_mode_matches(event_mode: str, stream_mode: list[str] | None) -> bool:
|
|
211
241
|
"""Return True if a published run-stream event matches the join filter."""
|
|
212
|
-
|
|
242
|
+
# A run's error is its outcome, not a selectable mode, so no filter drops it.
|
|
243
|
+
if not stream_mode or event_mode == "error":
|
|
213
244
|
return True
|
|
214
245
|
if event_mode in stream_mode:
|
|
215
246
|
return True
|
|
@@ -947,15 +978,24 @@ def is_jsonb_contained(superset: dict[str, Any], subset: dict[str, Any]) -> bool
|
|
|
947
978
|
"""
|
|
948
979
|
if not isinstance(superset, dict):
|
|
949
980
|
return False
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
981
|
+
return all(
|
|
982
|
+
key in superset and _is_jsonb_value_contained(superset[key], value)
|
|
983
|
+
for key, value in subset.items()
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def _is_jsonb_value_contained(superset: Any, subset: Any) -> bool:
|
|
988
|
+
"""Implements Postgres' @> semantics for arbitrary JSON values."""
|
|
989
|
+
if isinstance(subset, dict):
|
|
990
|
+
return isinstance(superset, dict) and is_jsonb_contained(superset, subset)
|
|
991
|
+
if isinstance(subset, list):
|
|
992
|
+
return isinstance(superset, list) and all(
|
|
993
|
+
any(_is_jsonb_value_contained(item, sub_item) for item in superset)
|
|
994
|
+
for sub_item in subset
|
|
995
|
+
)
|
|
996
|
+
if isinstance(superset, bool) or isinstance(subset, bool):
|
|
997
|
+
return superset is subset
|
|
998
|
+
return superset == subset
|
|
959
999
|
|
|
960
1000
|
|
|
961
1001
|
def bytes_decoder(obj):
|
|
@@ -1059,6 +1099,7 @@ class Threads(Authenticated):
|
|
|
1059
1099
|
sort_order: str | None = None,
|
|
1060
1100
|
select: list[ThreadSelectField] | None = None,
|
|
1061
1101
|
extract: dict[str, str] | None = None,
|
|
1102
|
+
exclude_metadata: list[dict[str, Any]] | None = None,
|
|
1062
1103
|
ctx: Auth.types.BaseAuthContext | None = None,
|
|
1063
1104
|
) -> tuple[AsyncIterator[Thread], int]:
|
|
1064
1105
|
threads = conn.store["threads"]
|
|
@@ -1101,6 +1142,12 @@ class Threads(Authenticated):
|
|
|
1101
1142
|
if metadata and not is_jsonb_contained(thread["metadata"], metadata):
|
|
1102
1143
|
continue
|
|
1103
1144
|
|
|
1145
|
+
if exclude_metadata and any(
|
|
1146
|
+
is_jsonb_contained(thread["metadata"], excluded)
|
|
1147
|
+
for excluded in exclude_metadata
|
|
1148
|
+
):
|
|
1149
|
+
continue
|
|
1150
|
+
|
|
1104
1151
|
if (
|
|
1105
1152
|
values
|
|
1106
1153
|
and "values" in thread
|
|
@@ -1682,19 +1729,30 @@ class Threads(Authenticated):
|
|
|
1682
1729
|
# The copy gets the original's checkpoints below, so it starts where the
|
|
1683
1730
|
# original stands: same values, interrupts and error, and the same fact
|
|
1684
1731
|
# about whether its graph has next steps. Its status comes from those,
|
|
1685
|
-
# by the usual rule: the original's runs are not the copy's.
|
|
1732
|
+
# by the usual rule: the original's runs are not the copy's. Missing
|
|
1733
|
+
# facts keep the original's status (except a stale busy → idle).
|
|
1686
1734
|
now = datetime.now(tz=UTC)
|
|
1735
|
+
src_failed = (
|
|
1736
|
+
_thread_failed(conn)[thread_id]
|
|
1737
|
+
if thread_id in _thread_failed(conn)
|
|
1738
|
+
else None
|
|
1739
|
+
)
|
|
1740
|
+
src_has_next = (
|
|
1741
|
+
_thread_has_next(conn)[thread_id]
|
|
1742
|
+
if thread_id in _thread_has_next(conn)
|
|
1743
|
+
else None
|
|
1744
|
+
)
|
|
1687
1745
|
new_thread: Thread = {
|
|
1688
1746
|
"thread_id": new_thread_id,
|
|
1689
1747
|
"created_at": now,
|
|
1690
1748
|
"updated_at": now,
|
|
1691
1749
|
"state_updated_at": now,
|
|
1692
1750
|
"metadata": copy.deepcopy(original_thread["metadata"]),
|
|
1693
|
-
"status":
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1751
|
+
"status": _derive_thread_status(
|
|
1752
|
+
has_active_runs=False,
|
|
1753
|
+
failed=src_failed,
|
|
1754
|
+
has_next=src_has_next,
|
|
1755
|
+
existing_status=original_thread["status"],
|
|
1698
1756
|
),
|
|
1699
1757
|
"config": {},
|
|
1700
1758
|
"values": copy.deepcopy(original_thread.get("values")),
|
|
@@ -2461,6 +2519,7 @@ class Threads(Authenticated):
|
|
|
2461
2519
|
metadata: MetadataInput = None,
|
|
2462
2520
|
values: MetadataInput = None,
|
|
2463
2521
|
status: ThreadStatus | None = None,
|
|
2522
|
+
exclude_metadata: list[dict[str, Any]] | None = None,
|
|
2464
2523
|
ctx: Auth.types.BaseAuthContext | None = None,
|
|
2465
2524
|
) -> int:
|
|
2466
2525
|
"""Get count of threads."""
|
|
@@ -2489,6 +2548,12 @@ class Threads(Authenticated):
|
|
|
2489
2548
|
if metadata and not is_jsonb_contained(thread["metadata"], metadata):
|
|
2490
2549
|
continue
|
|
2491
2550
|
|
|
2551
|
+
if exclude_metadata and any(
|
|
2552
|
+
is_jsonb_contained(thread["metadata"], excluded)
|
|
2553
|
+
for excluded in exclude_metadata
|
|
2554
|
+
):
|
|
2555
|
+
continue
|
|
2556
|
+
|
|
2492
2557
|
if (
|
|
2493
2558
|
values
|
|
2494
2559
|
and "values" in thread
|
|
@@ -2812,6 +2877,8 @@ class Runs(Authenticated):
|
|
|
2812
2877
|
if not _check_filter_match(existing_thread["metadata"], filters):
|
|
2813
2878
|
return _empty_generator()
|
|
2814
2879
|
|
|
2880
|
+
_reject_existing_run(conn.store["runs"], run_id)
|
|
2881
|
+
|
|
2815
2882
|
if not existing_thread and (thread_id is None or if_not_exists == "create"):
|
|
2816
2883
|
# Create new thread
|
|
2817
2884
|
if thread_id is None:
|
|
@@ -2950,6 +3017,7 @@ class Runs(Authenticated):
|
|
|
2950
3017
|
cancel_reason=None,
|
|
2951
3018
|
)
|
|
2952
3019
|
stored_run = new_run
|
|
3020
|
+
encrypted_thread = None
|
|
2953
3021
|
if encryption_enabled:
|
|
2954
3022
|
# Unlike Go core, in-memory workers read nested run kwargs and thread
|
|
2955
3023
|
# config directly from the store, so those containers must stay readable.
|
|
@@ -2957,14 +3025,16 @@ class Runs(Authenticated):
|
|
|
2957
3025
|
thread if not existing_thread else existing_thread,
|
|
2958
3026
|
"thread",
|
|
2959
3027
|
)
|
|
3028
|
+
stored_run = copy.deepcopy(new_run)
|
|
3029
|
+
stored_run = Run(**await _encrypt_for_storage(stored_run, "run"))
|
|
3030
|
+
_reject_existing_run(conn.store["runs"], run_id)
|
|
3031
|
+
if encrypted_thread is not None:
|
|
2960
3032
|
stored_thread = next(
|
|
2961
3033
|
t
|
|
2962
3034
|
for t in conn.store["threads"]
|
|
2963
3035
|
if t["thread_id"] == encrypted_thread["thread_id"]
|
|
2964
3036
|
)
|
|
2965
3037
|
stored_thread.update(encrypted_thread)
|
|
2966
|
-
stored_run = copy.deepcopy(new_run)
|
|
2967
|
-
stored_run = Run(**await _encrypt_for_storage(stored_run, "run"))
|
|
2968
3038
|
conn.store["runs"].append(stored_run)
|
|
2969
3039
|
|
|
2970
3040
|
async def _yield_new():
|
|
@@ -3178,15 +3248,19 @@ class Runs(Authenticated):
|
|
|
3178
3248
|
data=action.encode(),
|
|
3179
3249
|
reason=reason,
|
|
3180
3250
|
)
|
|
3181
|
-
|
|
3251
|
+
# Cancels by status or assistant have no thread_id; the worker listens on the run's own.
|
|
3252
|
+
coros.append(stream_manager.put(run_id, run["thread_id"], control_message))
|
|
3182
3253
|
|
|
3183
|
-
queues = stream_manager.get_queues(run_id, thread_id)
|
|
3254
|
+
queues = stream_manager.get_queues(run_id, run["thread_id"])
|
|
3184
3255
|
|
|
3185
3256
|
if run["status"] in ("pending", "running"):
|
|
3186
3257
|
cancelable_runs.append(run)
|
|
3187
3258
|
if run["status"] == "pending":
|
|
3188
3259
|
pending_thread_ids.add(run["thread_id"])
|
|
3189
|
-
if
|
|
3260
|
+
if action == "rollback" and run["status"] == "running":
|
|
3261
|
+
# Its worker finalizes the rollback; staying "running" holds the thread's next run until then.
|
|
3262
|
+
pass
|
|
3263
|
+
elif queues or action != "rollback":
|
|
3190
3264
|
run["status"] = "interrupted"
|
|
3191
3265
|
run["cancel_reason"] = reason
|
|
3192
3266
|
run["updated_at"] = datetime.now(tz=UTC)
|
|
@@ -3194,7 +3268,6 @@ class Runs(Authenticated):
|
|
|
3194
3268
|
await logger.ainfo(
|
|
3195
3269
|
"Eagerly deleting pending run with rollback action",
|
|
3196
3270
|
run_id=str(run_id),
|
|
3197
|
-
status=run["status"],
|
|
3198
3271
|
)
|
|
3199
3272
|
coros.append(Runs.delete(conn, run_id, thread_id=run["thread_id"]))
|
|
3200
3273
|
else:
|
|
@@ -4429,6 +4502,11 @@ def _check_filter_match(
|
|
|
4429
4502
|
return True
|
|
4430
4503
|
|
|
4431
4504
|
|
|
4505
|
+
def _reject_existing_run(runs: Sequence[Run], run_id: UUID | None) -> None:
|
|
4506
|
+
if any(run["run_id"] == run_id for run in runs):
|
|
4507
|
+
raise HTTPException(status_code=409, detail=f"Run {run_id} already exists")
|
|
4508
|
+
|
|
4509
|
+
|
|
4432
4510
|
async def _empty_generator():
|
|
4433
4511
|
if False:
|
|
4434
4512
|
yield
|
|
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
|
|
File without changes
|