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.
Files changed (19) hide show
  1. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/PKG-INFO +1 -1
  2. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/__init__.py +1 -1
  3. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/database.py +33 -3
  4. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/inmem_stream.py +38 -21
  5. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/lifespan.py +14 -0
  6. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/ops.py +131 -16
  7. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/pyproject.toml +5 -1
  8. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/.gitignore +0 -0
  9. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/Makefile +0 -0
  10. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/README.md +0 -0
  11. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/_persistence.py +0 -0
  12. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/checkpoint.py +0 -0
  13. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/encryption.py +0 -0
  14. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/metrics.py +0 -0
  15. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/queue.py +0 -0
  16. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/retry.py +0 -0
  17. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/routes.py +0 -0
  18. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/langgraph_runtime_inmem/store.py +0 -0
  19. {langgraph_runtime_inmem-0.38.0.dev13 → langgraph_runtime_inmem-0.38.0.dev14}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-runtime-inmem
3
- Version: 0.38.0.dev13
3
+ Version: 0.38.0.dev14
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
@@ -11,7 +11,7 @@ from langgraph_runtime_inmem import (
11
11
  store,
12
12
  )
13
13
 
14
- __version__ = "0.38.0.dev13"
14
+ __version__ = "0.38.0.dev14"
15
15
  __all__ = [
16
16
  "ops",
17
17
  "database",
@@ -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["status"] = "pending"
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
- if orphaned:
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=[str(r["run_id"]) for r in orphaned],
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 _generate_ms_seq_id() -> str:
17
- """Generate a Redis-like millisecond-sequence ID (e.g., '1234567890123-0')"""
18
- # Get current time in milliseconds
19
- ms = int(time.time() * 1000)
20
- # For simplicity, always use sequence 0 since we're not handling high throughput
21
- return f"{ms}-0"
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 = _generate_ms_seq_id().encode()
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 = _generate_ms_seq_id().encode()
135
- queues = self.thread_streams[thread_id]
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
- # Handle ms-seq format (e.g., "1234567890123-0")
214
- if thread_id in self.message_stores:
215
- for message in self.message_stores[thread_id][run_id]:
216
- if message.id.decode() > message_id:
217
- yield message
218
- except TypeError:
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
- # Sort by message ID (which is ms-seq format)
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
- await stream_manager.remove_queue(run_id, thread_id, queue)
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
- await logger.ainfo("Creating thread", thread_id=thread_id)
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 runs are marked interrupted when they're signalled, so
3345
- `finishing` only counts runs whose worker is still applying a rollback.
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 action == "rollback" and run["status"] == "running":
3407
- # Its worker finalizes the rollback; staying "running" holds the thread's next run until then.
3408
- run["cancel_action"] = "rollback"
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
- try:
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
 
@@ -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."