langgraph-runtime-inmem 0.37.0.dev5__tar.gz → 0.38.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.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/PKG-INFO +1 -1
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/langgraph_runtime_inmem/__init__.py +1 -1
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/langgraph_runtime_inmem/database.py +8 -21
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/langgraph_runtime_inmem/ops.py +108 -34
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/.gitignore +0 -0
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/Makefile +0 -0
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/README.md +0 -0
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/langgraph_runtime_inmem/_persistence.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/langgraph_runtime_inmem/checkpoint.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/langgraph_runtime_inmem/encryption.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/langgraph_runtime_inmem/inmem_stream.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/langgraph_runtime_inmem/lifespan.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/langgraph_runtime_inmem/metrics.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/langgraph_runtime_inmem/queue.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/langgraph_runtime_inmem/retry.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/langgraph_runtime_inmem/routes.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/langgraph_runtime_inmem/store.py +0 -0
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/pyproject.toml +0 -0
- {langgraph_runtime_inmem-0.37.0.dev5 → langgraph_runtime_inmem-0.38.0.dev1}/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(
|
|
@@ -948,15 +978,24 @@ def is_jsonb_contained(superset: dict[str, Any], subset: dict[str, Any]) -> bool
|
|
|
948
978
|
"""
|
|
949
979
|
if not isinstance(superset, dict):
|
|
950
980
|
return False
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
|
960
999
|
|
|
961
1000
|
|
|
962
1001
|
def bytes_decoder(obj):
|
|
@@ -1060,6 +1099,7 @@ class Threads(Authenticated):
|
|
|
1060
1099
|
sort_order: str | None = None,
|
|
1061
1100
|
select: list[ThreadSelectField] | None = None,
|
|
1062
1101
|
extract: dict[str, str] | None = None,
|
|
1102
|
+
exclude_metadata: list[dict[str, Any]] | None = None,
|
|
1063
1103
|
ctx: Auth.types.BaseAuthContext | None = None,
|
|
1064
1104
|
) -> tuple[AsyncIterator[Thread], int]:
|
|
1065
1105
|
threads = conn.store["threads"]
|
|
@@ -1102,6 +1142,12 @@ class Threads(Authenticated):
|
|
|
1102
1142
|
if metadata and not is_jsonb_contained(thread["metadata"], metadata):
|
|
1103
1143
|
continue
|
|
1104
1144
|
|
|
1145
|
+
if exclude_metadata and any(
|
|
1146
|
+
is_jsonb_contained(thread["metadata"], excluded)
|
|
1147
|
+
for excluded in exclude_metadata
|
|
1148
|
+
):
|
|
1149
|
+
continue
|
|
1150
|
+
|
|
1105
1151
|
if (
|
|
1106
1152
|
values
|
|
1107
1153
|
and "values" in thread
|
|
@@ -1683,19 +1729,30 @@ class Threads(Authenticated):
|
|
|
1683
1729
|
# The copy gets the original's checkpoints below, so it starts where the
|
|
1684
1730
|
# original stands: same values, interrupts and error, and the same fact
|
|
1685
1731
|
# about whether its graph has next steps. Its status comes from those,
|
|
1686
|
-
# 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).
|
|
1687
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
|
+
)
|
|
1688
1745
|
new_thread: Thread = {
|
|
1689
1746
|
"thread_id": new_thread_id,
|
|
1690
1747
|
"created_at": now,
|
|
1691
1748
|
"updated_at": now,
|
|
1692
1749
|
"state_updated_at": now,
|
|
1693
1750
|
"metadata": copy.deepcopy(original_thread["metadata"]),
|
|
1694
|
-
"status":
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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"],
|
|
1699
1756
|
),
|
|
1700
1757
|
"config": {},
|
|
1701
1758
|
"values": copy.deepcopy(original_thread.get("values")),
|
|
@@ -2462,6 +2519,7 @@ class Threads(Authenticated):
|
|
|
2462
2519
|
metadata: MetadataInput = None,
|
|
2463
2520
|
values: MetadataInput = None,
|
|
2464
2521
|
status: ThreadStatus | None = None,
|
|
2522
|
+
exclude_metadata: list[dict[str, Any]] | None = None,
|
|
2465
2523
|
ctx: Auth.types.BaseAuthContext | None = None,
|
|
2466
2524
|
) -> int:
|
|
2467
2525
|
"""Get count of threads."""
|
|
@@ -2490,6 +2548,12 @@ class Threads(Authenticated):
|
|
|
2490
2548
|
if metadata and not is_jsonb_contained(thread["metadata"], metadata):
|
|
2491
2549
|
continue
|
|
2492
2550
|
|
|
2551
|
+
if exclude_metadata and any(
|
|
2552
|
+
is_jsonb_contained(thread["metadata"], excluded)
|
|
2553
|
+
for excluded in exclude_metadata
|
|
2554
|
+
):
|
|
2555
|
+
continue
|
|
2556
|
+
|
|
2493
2557
|
if (
|
|
2494
2558
|
values
|
|
2495
2559
|
and "values" in thread
|
|
@@ -2813,6 +2877,8 @@ class Runs(Authenticated):
|
|
|
2813
2877
|
if not _check_filter_match(existing_thread["metadata"], filters):
|
|
2814
2878
|
return _empty_generator()
|
|
2815
2879
|
|
|
2880
|
+
_reject_existing_run(conn.store["runs"], run_id)
|
|
2881
|
+
|
|
2816
2882
|
if not existing_thread and (thread_id is None or if_not_exists == "create"):
|
|
2817
2883
|
# Create new thread
|
|
2818
2884
|
if thread_id is None:
|
|
@@ -2951,6 +3017,7 @@ class Runs(Authenticated):
|
|
|
2951
3017
|
cancel_reason=None,
|
|
2952
3018
|
)
|
|
2953
3019
|
stored_run = new_run
|
|
3020
|
+
encrypted_thread = None
|
|
2954
3021
|
if encryption_enabled:
|
|
2955
3022
|
# Unlike Go core, in-memory workers read nested run kwargs and thread
|
|
2956
3023
|
# config directly from the store, so those containers must stay readable.
|
|
@@ -2958,14 +3025,16 @@ class Runs(Authenticated):
|
|
|
2958
3025
|
thread if not existing_thread else existing_thread,
|
|
2959
3026
|
"thread",
|
|
2960
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:
|
|
2961
3032
|
stored_thread = next(
|
|
2962
3033
|
t
|
|
2963
3034
|
for t in conn.store["threads"]
|
|
2964
3035
|
if t["thread_id"] == encrypted_thread["thread_id"]
|
|
2965
3036
|
)
|
|
2966
3037
|
stored_thread.update(encrypted_thread)
|
|
2967
|
-
stored_run = copy.deepcopy(new_run)
|
|
2968
|
-
stored_run = Run(**await _encrypt_for_storage(stored_run, "run"))
|
|
2969
3038
|
conn.store["runs"].append(stored_run)
|
|
2970
3039
|
|
|
2971
3040
|
async def _yield_new():
|
|
@@ -4433,6 +4502,11 @@ def _check_filter_match(
|
|
|
4433
4502
|
return True
|
|
4434
4503
|
|
|
4435
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
|
+
|
|
4436
4510
|
async def _empty_generator():
|
|
4437
4511
|
if False:
|
|
4438
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
|