agent-runtime-core 0.1.0__py3-none-any.whl
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.
- agent_runtime/__init__.py +110 -0
- agent_runtime/config.py +172 -0
- agent_runtime/events/__init__.py +55 -0
- agent_runtime/events/base.py +86 -0
- agent_runtime/events/memory.py +89 -0
- agent_runtime/events/redis.py +185 -0
- agent_runtime/events/sqlite.py +168 -0
- agent_runtime/interfaces.py +390 -0
- agent_runtime/llm/__init__.py +83 -0
- agent_runtime/llm/anthropic.py +237 -0
- agent_runtime/llm/litellm_client.py +175 -0
- agent_runtime/llm/openai.py +220 -0
- agent_runtime/queue/__init__.py +55 -0
- agent_runtime/queue/base.py +167 -0
- agent_runtime/queue/memory.py +184 -0
- agent_runtime/queue/redis.py +453 -0
- agent_runtime/queue/sqlite.py +420 -0
- agent_runtime/registry.py +74 -0
- agent_runtime/runner.py +403 -0
- agent_runtime/state/__init__.py +53 -0
- agent_runtime/state/base.py +69 -0
- agent_runtime/state/memory.py +51 -0
- agent_runtime/state/redis.py +109 -0
- agent_runtime/state/sqlite.py +158 -0
- agent_runtime/tracing/__init__.py +47 -0
- agent_runtime/tracing/langfuse.py +119 -0
- agent_runtime/tracing/noop.py +34 -0
- agent_runtime_core-0.1.0.dist-info/METADATA +75 -0
- agent_runtime_core-0.1.0.dist-info/RECORD +31 -0
- agent_runtime_core-0.1.0.dist-info/WHEEL +4 -0
- agent_runtime_core-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLite queue implementation.
|
|
3
|
+
|
|
4
|
+
Good for:
|
|
5
|
+
- Local development with persistence
|
|
6
|
+
- Single-process deployments
|
|
7
|
+
- Testing with real database
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sqlite3
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from datetime import datetime, timedelta, timezone
|
|
14
|
+
from typing import Optional
|
|
15
|
+
from uuid import UUID
|
|
16
|
+
|
|
17
|
+
from agent_runtime.queue.base import RunQueue, QueuedRun, RunStatus
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SQLiteQueue(RunQueue):
|
|
21
|
+
"""
|
|
22
|
+
SQLite-backed queue implementation.
|
|
23
|
+
|
|
24
|
+
Stores runs in a local SQLite database.
|
|
25
|
+
Uses row-level locking for claim operations.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, path: str = "agent_runtime.db", lease_ttl_seconds: int = 30):
|
|
29
|
+
self.path = path
|
|
30
|
+
self.lease_ttl_seconds = lease_ttl_seconds
|
|
31
|
+
self._initialized = False
|
|
32
|
+
|
|
33
|
+
@contextmanager
|
|
34
|
+
def _get_connection(self):
|
|
35
|
+
"""Get a database connection."""
|
|
36
|
+
conn = sqlite3.connect(self.path, timeout=30.0)
|
|
37
|
+
conn.row_factory = sqlite3.Row
|
|
38
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
39
|
+
try:
|
|
40
|
+
yield conn
|
|
41
|
+
finally:
|
|
42
|
+
conn.close()
|
|
43
|
+
|
|
44
|
+
def _ensure_initialized(self):
|
|
45
|
+
"""Ensure the database schema exists."""
|
|
46
|
+
if self._initialized:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
with self._get_connection() as conn:
|
|
50
|
+
conn.execute("""
|
|
51
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
52
|
+
run_id TEXT PRIMARY KEY,
|
|
53
|
+
agent_key TEXT NOT NULL,
|
|
54
|
+
input TEXT NOT NULL,
|
|
55
|
+
metadata TEXT NOT NULL,
|
|
56
|
+
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
57
|
+
attempt INTEGER NOT NULL DEFAULT 1,
|
|
58
|
+
status TEXT NOT NULL DEFAULT 'queued',
|
|
59
|
+
lease_owner TEXT DEFAULT '',
|
|
60
|
+
lease_expires_at TEXT,
|
|
61
|
+
cancel_requested_at TEXT,
|
|
62
|
+
output TEXT,
|
|
63
|
+
error TEXT,
|
|
64
|
+
created_at TEXT NOT NULL,
|
|
65
|
+
started_at TEXT,
|
|
66
|
+
finished_at TEXT,
|
|
67
|
+
available_at TEXT NOT NULL
|
|
68
|
+
)
|
|
69
|
+
""")
|
|
70
|
+
conn.execute("""
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status
|
|
72
|
+
ON runs(status)
|
|
73
|
+
""")
|
|
74
|
+
conn.execute("""
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_runs_available
|
|
76
|
+
ON runs(status, available_at)
|
|
77
|
+
""")
|
|
78
|
+
conn.commit()
|
|
79
|
+
|
|
80
|
+
self._initialized = True
|
|
81
|
+
|
|
82
|
+
async def enqueue(
|
|
83
|
+
self,
|
|
84
|
+
run_id: UUID,
|
|
85
|
+
agent_key: str,
|
|
86
|
+
input: dict,
|
|
87
|
+
metadata: Optional[dict] = None,
|
|
88
|
+
max_attempts: int = 3,
|
|
89
|
+
) -> QueuedRun:
|
|
90
|
+
"""Add a new run to the queue."""
|
|
91
|
+
self._ensure_initialized()
|
|
92
|
+
now = datetime.now(timezone.utc)
|
|
93
|
+
|
|
94
|
+
with self._get_connection() as conn:
|
|
95
|
+
conn.execute(
|
|
96
|
+
"""
|
|
97
|
+
INSERT INTO runs (
|
|
98
|
+
run_id, agent_key, input, metadata, max_attempts,
|
|
99
|
+
status, created_at, available_at
|
|
100
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
101
|
+
""",
|
|
102
|
+
(
|
|
103
|
+
str(run_id),
|
|
104
|
+
agent_key,
|
|
105
|
+
json.dumps(input),
|
|
106
|
+
json.dumps(metadata or {}),
|
|
107
|
+
max_attempts,
|
|
108
|
+
RunStatus.QUEUED.value,
|
|
109
|
+
now.isoformat(),
|
|
110
|
+
now.isoformat(),
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
conn.commit()
|
|
114
|
+
|
|
115
|
+
return QueuedRun(
|
|
116
|
+
run_id=run_id,
|
|
117
|
+
agent_key=agent_key,
|
|
118
|
+
attempt=1,
|
|
119
|
+
lease_expires_at=now,
|
|
120
|
+
input=input,
|
|
121
|
+
metadata=metadata or {},
|
|
122
|
+
max_attempts=max_attempts,
|
|
123
|
+
status=RunStatus.QUEUED,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
async def claim(
|
|
127
|
+
self,
|
|
128
|
+
worker_id: str,
|
|
129
|
+
agent_keys: Optional[list[str]] = None,
|
|
130
|
+
batch_size: int = 1,
|
|
131
|
+
) -> list[QueuedRun]:
|
|
132
|
+
"""Claim runs from the queue."""
|
|
133
|
+
self._ensure_initialized()
|
|
134
|
+
now = datetime.now(timezone.utc)
|
|
135
|
+
lease_expires = now + timedelta(seconds=self.lease_ttl_seconds)
|
|
136
|
+
|
|
137
|
+
with self._get_connection() as conn:
|
|
138
|
+
# Build query
|
|
139
|
+
query = """
|
|
140
|
+
SELECT run_id, agent_key, input, metadata, max_attempts, attempt
|
|
141
|
+
FROM runs
|
|
142
|
+
WHERE status = ?
|
|
143
|
+
AND available_at <= ?
|
|
144
|
+
"""
|
|
145
|
+
params = [RunStatus.QUEUED.value, now.isoformat()]
|
|
146
|
+
|
|
147
|
+
if agent_keys:
|
|
148
|
+
placeholders = ",".join("?" * len(agent_keys))
|
|
149
|
+
query += f" AND agent_key IN ({placeholders})"
|
|
150
|
+
params.extend(agent_keys)
|
|
151
|
+
|
|
152
|
+
query += f" ORDER BY created_at ASC LIMIT {batch_size}"
|
|
153
|
+
|
|
154
|
+
rows = conn.execute(query, params).fetchall()
|
|
155
|
+
|
|
156
|
+
claimed = []
|
|
157
|
+
for row in rows:
|
|
158
|
+
run_id = row["run_id"]
|
|
159
|
+
|
|
160
|
+
# Try to claim with optimistic locking
|
|
161
|
+
result = conn.execute(
|
|
162
|
+
"""
|
|
163
|
+
UPDATE runs
|
|
164
|
+
SET status = ?, lease_owner = ?, lease_expires_at = ?,
|
|
165
|
+
started_at = COALESCE(started_at, ?)
|
|
166
|
+
WHERE run_id = ? AND status = ?
|
|
167
|
+
""",
|
|
168
|
+
(
|
|
169
|
+
RunStatus.RUNNING.value,
|
|
170
|
+
worker_id,
|
|
171
|
+
lease_expires.isoformat(),
|
|
172
|
+
now.isoformat(),
|
|
173
|
+
run_id,
|
|
174
|
+
RunStatus.QUEUED.value,
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if result.rowcount > 0:
|
|
179
|
+
claimed.append(QueuedRun(
|
|
180
|
+
run_id=UUID(row["run_id"]),
|
|
181
|
+
agent_key=row["agent_key"],
|
|
182
|
+
attempt=row["attempt"],
|
|
183
|
+
lease_expires_at=lease_expires,
|
|
184
|
+
input=json.loads(row["input"]),
|
|
185
|
+
metadata=json.loads(row["metadata"]),
|
|
186
|
+
max_attempts=row["max_attempts"],
|
|
187
|
+
status=RunStatus.RUNNING,
|
|
188
|
+
))
|
|
189
|
+
|
|
190
|
+
conn.commit()
|
|
191
|
+
return claimed
|
|
192
|
+
|
|
193
|
+
async def extend_lease(self, run_id: UUID, worker_id: str, seconds: int) -> bool:
|
|
194
|
+
"""Extend the lease on a run."""
|
|
195
|
+
self._ensure_initialized()
|
|
196
|
+
new_expires = datetime.now(timezone.utc) + timedelta(seconds=seconds)
|
|
197
|
+
|
|
198
|
+
with self._get_connection() as conn:
|
|
199
|
+
result = conn.execute(
|
|
200
|
+
"""
|
|
201
|
+
UPDATE runs
|
|
202
|
+
SET lease_expires_at = ?
|
|
203
|
+
WHERE run_id = ? AND lease_owner = ? AND status = ?
|
|
204
|
+
""",
|
|
205
|
+
(
|
|
206
|
+
new_expires.isoformat(),
|
|
207
|
+
str(run_id),
|
|
208
|
+
worker_id,
|
|
209
|
+
RunStatus.RUNNING.value,
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
conn.commit()
|
|
213
|
+
return result.rowcount > 0
|
|
214
|
+
|
|
215
|
+
async def release(
|
|
216
|
+
self,
|
|
217
|
+
run_id: UUID,
|
|
218
|
+
worker_id: str,
|
|
219
|
+
success: bool,
|
|
220
|
+
output: Optional[dict] = None,
|
|
221
|
+
error: Optional[dict] = None,
|
|
222
|
+
) -> None:
|
|
223
|
+
"""Release a run after completion."""
|
|
224
|
+
self._ensure_initialized()
|
|
225
|
+
now = datetime.now(timezone.utc)
|
|
226
|
+
|
|
227
|
+
with self._get_connection() as conn:
|
|
228
|
+
conn.execute(
|
|
229
|
+
"""
|
|
230
|
+
UPDATE runs
|
|
231
|
+
SET status = ?, finished_at = ?, lease_owner = '',
|
|
232
|
+
lease_expires_at = NULL, output = ?, error = ?
|
|
233
|
+
WHERE run_id = ? AND lease_owner = ?
|
|
234
|
+
""",
|
|
235
|
+
(
|
|
236
|
+
RunStatus.SUCCEEDED.value if success else RunStatus.FAILED.value,
|
|
237
|
+
now.isoformat(),
|
|
238
|
+
json.dumps(output) if output else None,
|
|
239
|
+
json.dumps(error) if error else None,
|
|
240
|
+
str(run_id),
|
|
241
|
+
worker_id,
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
conn.commit()
|
|
245
|
+
|
|
246
|
+
async def requeue_for_retry(
|
|
247
|
+
self,
|
|
248
|
+
run_id: UUID,
|
|
249
|
+
worker_id: str,
|
|
250
|
+
error: dict,
|
|
251
|
+
delay_seconds: int = 0,
|
|
252
|
+
) -> bool:
|
|
253
|
+
"""Requeue a run for retry."""
|
|
254
|
+
self._ensure_initialized()
|
|
255
|
+
now = datetime.now(timezone.utc)
|
|
256
|
+
available_at = now + timedelta(seconds=delay_seconds)
|
|
257
|
+
|
|
258
|
+
with self._get_connection() as conn:
|
|
259
|
+
# Get current attempt and max
|
|
260
|
+
row = conn.execute(
|
|
261
|
+
"SELECT attempt, max_attempts FROM runs WHERE run_id = ? AND lease_owner = ?",
|
|
262
|
+
(str(run_id), worker_id),
|
|
263
|
+
).fetchone()
|
|
264
|
+
|
|
265
|
+
if not row:
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
if row["attempt"] >= row["max_attempts"]:
|
|
269
|
+
conn.execute(
|
|
270
|
+
"""
|
|
271
|
+
UPDATE runs
|
|
272
|
+
SET status = ?, error = ?, finished_at = ?,
|
|
273
|
+
lease_owner = '', lease_expires_at = NULL
|
|
274
|
+
WHERE run_id = ?
|
|
275
|
+
""",
|
|
276
|
+
(
|
|
277
|
+
RunStatus.FAILED.value,
|
|
278
|
+
json.dumps(error),
|
|
279
|
+
now.isoformat(),
|
|
280
|
+
str(run_id),
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
conn.commit()
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
conn.execute(
|
|
287
|
+
"""
|
|
288
|
+
UPDATE runs
|
|
289
|
+
SET status = ?, attempt = attempt + 1, error = ?,
|
|
290
|
+
lease_owner = '', lease_expires_at = NULL, available_at = ?
|
|
291
|
+
WHERE run_id = ?
|
|
292
|
+
""",
|
|
293
|
+
(
|
|
294
|
+
RunStatus.QUEUED.value,
|
|
295
|
+
json.dumps(error),
|
|
296
|
+
available_at.isoformat(),
|
|
297
|
+
str(run_id),
|
|
298
|
+
),
|
|
299
|
+
)
|
|
300
|
+
conn.commit()
|
|
301
|
+
return True
|
|
302
|
+
|
|
303
|
+
async def cancel(self, run_id: UUID) -> bool:
|
|
304
|
+
"""Mark a run for cancellation."""
|
|
305
|
+
self._ensure_initialized()
|
|
306
|
+
now = datetime.now(timezone.utc)
|
|
307
|
+
|
|
308
|
+
with self._get_connection() as conn:
|
|
309
|
+
result = conn.execute(
|
|
310
|
+
"""
|
|
311
|
+
UPDATE runs
|
|
312
|
+
SET cancel_requested_at = ?
|
|
313
|
+
WHERE run_id = ? AND status IN (?, ?)
|
|
314
|
+
""",
|
|
315
|
+
(
|
|
316
|
+
now.isoformat(),
|
|
317
|
+
str(run_id),
|
|
318
|
+
RunStatus.QUEUED.value,
|
|
319
|
+
RunStatus.RUNNING.value,
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
conn.commit()
|
|
323
|
+
return result.rowcount > 0
|
|
324
|
+
|
|
325
|
+
async def is_cancelled(self, run_id: UUID) -> bool:
|
|
326
|
+
"""Check if a run has been cancelled."""
|
|
327
|
+
self._ensure_initialized()
|
|
328
|
+
|
|
329
|
+
with self._get_connection() as conn:
|
|
330
|
+
row = conn.execute(
|
|
331
|
+
"SELECT cancel_requested_at FROM runs WHERE run_id = ?",
|
|
332
|
+
(str(run_id),),
|
|
333
|
+
).fetchone()
|
|
334
|
+
|
|
335
|
+
return bool(row and row["cancel_requested_at"])
|
|
336
|
+
|
|
337
|
+
async def recover_expired_leases(self) -> int:
|
|
338
|
+
"""Recover runs with expired leases."""
|
|
339
|
+
self._ensure_initialized()
|
|
340
|
+
now = datetime.now(timezone.utc)
|
|
341
|
+
|
|
342
|
+
with self._get_connection() as conn:
|
|
343
|
+
# Get expired runs
|
|
344
|
+
rows = conn.execute(
|
|
345
|
+
"""
|
|
346
|
+
SELECT run_id, attempt, max_attempts, agent_key
|
|
347
|
+
FROM runs
|
|
348
|
+
WHERE status = ? AND lease_expires_at < ?
|
|
349
|
+
""",
|
|
350
|
+
(RunStatus.RUNNING.value, now.isoformat()),
|
|
351
|
+
).fetchall()
|
|
352
|
+
|
|
353
|
+
count = 0
|
|
354
|
+
for row in rows:
|
|
355
|
+
if row["attempt"] >= row["max_attempts"]:
|
|
356
|
+
conn.execute(
|
|
357
|
+
"""
|
|
358
|
+
UPDATE runs
|
|
359
|
+
SET status = ?, finished_at = ?, error = ?,
|
|
360
|
+
lease_owner = '', lease_expires_at = NULL
|
|
361
|
+
WHERE run_id = ?
|
|
362
|
+
""",
|
|
363
|
+
(
|
|
364
|
+
RunStatus.TIMED_OUT.value,
|
|
365
|
+
now.isoformat(),
|
|
366
|
+
json.dumps({
|
|
367
|
+
"type": "LeaseExpired",
|
|
368
|
+
"message": "Worker lease expired without completion",
|
|
369
|
+
"retriable": False,
|
|
370
|
+
}),
|
|
371
|
+
row["run_id"],
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
else:
|
|
375
|
+
conn.execute(
|
|
376
|
+
"""
|
|
377
|
+
UPDATE runs
|
|
378
|
+
SET status = ?, attempt = attempt + 1,
|
|
379
|
+
lease_owner = '', lease_expires_at = NULL
|
|
380
|
+
WHERE run_id = ?
|
|
381
|
+
""",
|
|
382
|
+
(RunStatus.QUEUED.value, row["run_id"]),
|
|
383
|
+
)
|
|
384
|
+
count += 1
|
|
385
|
+
|
|
386
|
+
conn.commit()
|
|
387
|
+
return count
|
|
388
|
+
|
|
389
|
+
async def get_run(self, run_id: UUID) -> Optional[QueuedRun]:
|
|
390
|
+
"""Get a run by ID."""
|
|
391
|
+
self._ensure_initialized()
|
|
392
|
+
|
|
393
|
+
with self._get_connection() as conn:
|
|
394
|
+
row = conn.execute(
|
|
395
|
+
"""
|
|
396
|
+
SELECT run_id, agent_key, input, metadata, max_attempts,
|
|
397
|
+
attempt, status, lease_expires_at
|
|
398
|
+
FROM runs WHERE run_id = ?
|
|
399
|
+
""",
|
|
400
|
+
(str(run_id),),
|
|
401
|
+
).fetchone()
|
|
402
|
+
|
|
403
|
+
if not row:
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
lease_expires = row["lease_expires_at"]
|
|
407
|
+
|
|
408
|
+
return QueuedRun(
|
|
409
|
+
run_id=UUID(row["run_id"]),
|
|
410
|
+
agent_key=row["agent_key"],
|
|
411
|
+
attempt=row["attempt"],
|
|
412
|
+
lease_expires_at=(
|
|
413
|
+
datetime.fromisoformat(lease_expires) if lease_expires
|
|
414
|
+
else datetime.now(timezone.utc)
|
|
415
|
+
),
|
|
416
|
+
input=json.loads(row["input"]),
|
|
417
|
+
metadata=json.loads(row["metadata"]),
|
|
418
|
+
max_attempts=row["max_attempts"],
|
|
419
|
+
status=RunStatus(row["status"]),
|
|
420
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent runtime registry.
|
|
3
|
+
|
|
4
|
+
Provides a global registry for agent runtimes, allowing them to be
|
|
5
|
+
looked up by key.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from agent_runtime.interfaces import AgentRuntime
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Global registry
|
|
14
|
+
_runtimes: dict[str, AgentRuntime] = {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_runtime(runtime: AgentRuntime) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Register an agent runtime.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
runtime: The runtime to register
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ValueError: If a runtime with the same key is already registered
|
|
26
|
+
"""
|
|
27
|
+
key = runtime.key
|
|
28
|
+
if key in _runtimes:
|
|
29
|
+
raise ValueError(f"Runtime already registered: {key}")
|
|
30
|
+
_runtimes[key] = runtime
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_runtime(key: str) -> Optional[AgentRuntime]:
|
|
34
|
+
"""
|
|
35
|
+
Get a registered runtime by key.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
key: The runtime key
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The runtime, or None if not found
|
|
42
|
+
"""
|
|
43
|
+
return _runtimes.get(key)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def list_runtimes() -> list[str]:
|
|
47
|
+
"""
|
|
48
|
+
List all registered runtime keys.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of runtime keys
|
|
52
|
+
"""
|
|
53
|
+
return list(_runtimes.keys())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def unregister_runtime(key: str) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Unregister a runtime.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
key: The runtime key
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True if unregistered, False if not found
|
|
65
|
+
"""
|
|
66
|
+
if key in _runtimes:
|
|
67
|
+
del _runtimes[key]
|
|
68
|
+
return True
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def clear_registry() -> None:
|
|
73
|
+
"""Clear all registered runtimes. Useful for testing."""
|
|
74
|
+
_runtimes.clear()
|