zero-agent-core 0.2.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_core/__init__.py +525 -0
- agent_core/access/__init__.py +6 -0
- agent_core/access/context.py +45 -0
- agent_core/access/guard.py +40 -0
- agent_core/acp/README.md +236 -0
- agent_core/acp/__init__.py +20 -0
- agent_core/acp/server.py +351 -0
- agent_core/acp/types.py +86 -0
- agent_core/callbacks/__init__.py +17 -0
- agent_core/callbacks/manager.py +105 -0
- agent_core/checkpoint/__init__.py +19 -0
- agent_core/checkpoint/naming.py +93 -0
- agent_core/checkpoint/store.py +186 -0
- agent_core/checkpoint/travel.py +448 -0
- agent_core/compaction/__init__.py +57 -0
- agent_core/compaction/middleware.py +428 -0
- agent_core/compaction/prompts.py +32 -0
- agent_core/compaction/pruner.py +121 -0
- agent_core/compaction/region.py +145 -0
- agent_core/compaction/spill.py +81 -0
- agent_core/compaction/spill_tool.py +42 -0
- agent_core/compaction/summarizer.py +95 -0
- agent_core/compaction/token_limit.py +74 -0
- agent_core/compaction/types.py +93 -0
- agent_core/docs.py +15 -0
- agent_core/documents/__init__.py +23 -0
- agent_core/documents/loaders.py +79 -0
- agent_core/documents/splitters.py +94 -0
- agent_core/documents/types.py +197 -0
- agent_core/errors.py +81 -0
- agent_core/guardrails/__init__.py +19 -0
- agent_core/guardrails/core.py +186 -0
- agent_core/hitl/__init__.py +15 -0
- agent_core/hitl/core.py +517 -0
- agent_core/mcp/__init__.py +14 -0
- agent_core/mcp/client.py +212 -0
- agent_core/mcp/errors.py +26 -0
- agent_core/middleware/__init__.py +26 -0
- agent_core/middleware/base.py +283 -0
- agent_core/model/__init__.py +73 -0
- agent_core/model/catalog.py +73 -0
- agent_core/model/core.py +179 -0
- agent_core/model/governance.py +317 -0
- agent_core/model/providers/__init__.py +43 -0
- agent_core/model/providers/anthropic.py +294 -0
- agent_core/model/providers/common.py +28 -0
- agent_core/model/providers/factory.py +107 -0
- agent_core/model/providers/gemini.py +273 -0
- agent_core/model/providers/ollama.py +80 -0
- agent_core/model/providers/openai_compatible.py +526 -0
- agent_core/model/structured.py +200 -0
- agent_core/observability/__init__.py +21 -0
- agent_core/observability/core.py +311 -0
- agent_core/ports/__init__.py +31 -0
- agent_core/ports/memory.py +130 -0
- agent_core/ports/model_selection.py +64 -0
- agent_core/ports/storage.py +114 -0
- agent_core/prompts/__init__.py +19 -0
- agent_core/prompts/registry.py +236 -0
- agent_core/prompts/templates.py +166 -0
- agent_core/protocol/__init__.py +31 -0
- agent_core/protocol/capabilities.py +49 -0
- agent_core/protocol/messages.py +100 -0
- agent_core/protocol/runtime.py +293 -0
- agent_core/py.typed +1 -0
- agent_core/retrieval/__init__.py +40 -0
- agent_core/retrieval/_common.py +121 -0
- agent_core/retrieval/chroma.py +269 -0
- agent_core/retrieval/core.py +63 -0
- agent_core/retrieval/factory.py +72 -0
- agent_core/retrieval/memory.py +232 -0
- agent_core/retrieval/pgvector.py +239 -0
- agent_core/retrieval/types.py +95 -0
- agent_core/runtime/__init__.py +28 -0
- agent_core/runtime/compat.py +98 -0
- agent_core/runtime/react.py +888 -0
- agent_core/runtime/service.py +557 -0
- agent_core/runtime/store.py +55 -0
- agent_core/serialization/__init__.py +21 -0
- agent_core/serialization/registry.py +318 -0
- agent_core/skills/__init__.py +24 -0
- agent_core/skills/errors.py +27 -0
- agent_core/skills/loader.py +258 -0
- agent_core/skills/registry.py +205 -0
- agent_core/skills/types.py +56 -0
- agent_core/storage/__init__.py +107 -0
- agent_core/storage/_sqlite_utils.py +92 -0
- agent_core/storage/context.py +382 -0
- agent_core/storage/lease.py +435 -0
- agent_core/storage/memory.py +604 -0
- agent_core/storage/model_selection.py +360 -0
- agent_core/storage/runtime.py +899 -0
- agent_core/storage/session.py +904 -0
- agent_core/storage/workflow.py +411 -0
- agent_core/tools/__init__.py +5 -0
- agent_core/tools/executor.py +306 -0
- agent_core/workflow/__init__.py +25 -0
- agent_core/workflow/durable.py +175 -0
- agent_core/workflow/state_machine.py +427 -0
- zero_agent_core-0.2.0.dist-info/METADATA +237 -0
- zero_agent_core-0.2.0.dist-info/RECORD +104 -0
- zero_agent_core-0.2.0.dist-info/WHEEL +4 -0
- zero_agent_core-0.2.0.dist-info/entry_points.txt +3 -0
- zero_agent_core-0.2.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""Run 分布式租约存储。
|
|
2
|
+
|
|
3
|
+
租约只负责保证同一个 Run 同一时间由一个 Worker 执行,不替代 RunContext、
|
|
4
|
+
checkpoint 或任务队列。Worker 必须定期续租,租约过期后其他 Worker 才能接管。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import sqlite3
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from collections.abc import Generator
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import UTC, datetime, timedelta
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from agent_core.storage._sqlite_utils import (
|
|
20
|
+
_run_in_thread,
|
|
21
|
+
run_sync_in_thread,
|
|
22
|
+
sqlite_connection,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _now() -> datetime:
|
|
27
|
+
return datetime.now(UTC)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _iso(value: datetime) -> str:
|
|
31
|
+
return value.isoformat()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _parse(value: str | datetime) -> datetime:
|
|
35
|
+
if isinstance(value, datetime):
|
|
36
|
+
return value if value.tzinfo else value.replace(tzinfo=UTC)
|
|
37
|
+
parsed = datetime.fromisoformat(value)
|
|
38
|
+
return parsed if parsed.tzinfo else parsed.replace(tzinfo=UTC)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class RunLease:
|
|
43
|
+
"""一个 Run 当前的 Worker 所有权。"""
|
|
44
|
+
|
|
45
|
+
thread_id: str
|
|
46
|
+
run_id: str
|
|
47
|
+
worker_id: str
|
|
48
|
+
lease_expires_at: str
|
|
49
|
+
heartbeat_at: str
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def expired(self) -> bool:
|
|
53
|
+
return _parse(self.lease_expires_at) <= _now()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RunLeaseStore(ABC):
|
|
57
|
+
"""Run 租约存储端口。"""
|
|
58
|
+
|
|
59
|
+
async def initialize(self) -> None:
|
|
60
|
+
"""初始化后端。内存和 SQLite 实现无需额外操作。"""
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
async def close(self) -> None:
|
|
64
|
+
"""关闭后端。内存和 SQLite 实现无需额外操作。"""
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
async def claim(
|
|
69
|
+
self,
|
|
70
|
+
thread_id: str,
|
|
71
|
+
run_id: str,
|
|
72
|
+
worker_id: str,
|
|
73
|
+
*,
|
|
74
|
+
ttl_seconds: float,
|
|
75
|
+
) -> bool:
|
|
76
|
+
"""认领 Run;已有未过期租约时返回 False。"""
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
async def renew(
|
|
80
|
+
self,
|
|
81
|
+
thread_id: str,
|
|
82
|
+
run_id: str,
|
|
83
|
+
worker_id: str,
|
|
84
|
+
*,
|
|
85
|
+
ttl_seconds: float,
|
|
86
|
+
) -> bool:
|
|
87
|
+
"""只有当前所有者可以续租。"""
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
async def release(self, thread_id: str, run_id: str, worker_id: str) -> bool:
|
|
91
|
+
"""只有当前所有者可以释放租约。"""
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
async def list_expired(self) -> list[RunLease]:
|
|
95
|
+
"""列出已经过期、可以恢复的租约。"""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class MemoryRunLeaseStore(RunLeaseStore):
|
|
99
|
+
"""测试和单进程开发使用的内存租约。"""
|
|
100
|
+
|
|
101
|
+
def __init__(self) -> None:
|
|
102
|
+
self._leases: dict[tuple[str, str], RunLease] = {}
|
|
103
|
+
self._lock = asyncio.Lock()
|
|
104
|
+
|
|
105
|
+
async def claim(
|
|
106
|
+
self,
|
|
107
|
+
thread_id: str,
|
|
108
|
+
run_id: str,
|
|
109
|
+
worker_id: str,
|
|
110
|
+
*,
|
|
111
|
+
ttl_seconds: float,
|
|
112
|
+
) -> bool:
|
|
113
|
+
_validate_ttl(ttl_seconds)
|
|
114
|
+
async with self._lock:
|
|
115
|
+
key = (thread_id, run_id)
|
|
116
|
+
current = self._leases.get(key)
|
|
117
|
+
if current is not None and not current.expired and current.worker_id != worker_id:
|
|
118
|
+
return False
|
|
119
|
+
now = _now()
|
|
120
|
+
self._leases[key] = RunLease(
|
|
121
|
+
thread_id=thread_id,
|
|
122
|
+
run_id=run_id,
|
|
123
|
+
worker_id=worker_id,
|
|
124
|
+
heartbeat_at=_iso(now),
|
|
125
|
+
lease_expires_at=_iso(now + timedelta(seconds=ttl_seconds)),
|
|
126
|
+
)
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
async def renew(
|
|
130
|
+
self,
|
|
131
|
+
thread_id: str,
|
|
132
|
+
run_id: str,
|
|
133
|
+
worker_id: str,
|
|
134
|
+
*,
|
|
135
|
+
ttl_seconds: float,
|
|
136
|
+
) -> bool:
|
|
137
|
+
_validate_ttl(ttl_seconds)
|
|
138
|
+
async with self._lock:
|
|
139
|
+
key = (thread_id, run_id)
|
|
140
|
+
current = self._leases.get(key)
|
|
141
|
+
if current is None or current.worker_id != worker_id or current.expired:
|
|
142
|
+
return False
|
|
143
|
+
now = _now()
|
|
144
|
+
self._leases[key] = RunLease(
|
|
145
|
+
thread_id=thread_id,
|
|
146
|
+
run_id=run_id,
|
|
147
|
+
worker_id=worker_id,
|
|
148
|
+
heartbeat_at=_iso(now),
|
|
149
|
+
lease_expires_at=_iso(now + timedelta(seconds=ttl_seconds)),
|
|
150
|
+
)
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
async def release(self, thread_id: str, run_id: str, worker_id: str) -> bool:
|
|
154
|
+
async with self._lock:
|
|
155
|
+
key = (thread_id, run_id)
|
|
156
|
+
current = self._leases.get(key)
|
|
157
|
+
if current is None or current.worker_id != worker_id:
|
|
158
|
+
return False
|
|
159
|
+
del self._leases[key]
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
async def list_expired(self) -> list[RunLease]:
|
|
163
|
+
async with self._lock:
|
|
164
|
+
return [lease for lease in self._leases.values() if lease.expired]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class SqliteRunLeaseStore(RunLeaseStore):
|
|
168
|
+
"""开发环境 SQLite 租约实现。"""
|
|
169
|
+
|
|
170
|
+
DDL = """
|
|
171
|
+
CREATE TABLE IF NOT EXISTS agent_run_leases (
|
|
172
|
+
thread_id TEXT NOT NULL,
|
|
173
|
+
run_id TEXT NOT NULL,
|
|
174
|
+
worker_id TEXT NOT NULL,
|
|
175
|
+
lease_expires_at TEXT NOT NULL,
|
|
176
|
+
heartbeat_at TEXT NOT NULL,
|
|
177
|
+
PRIMARY KEY (thread_id, run_id)
|
|
178
|
+
);
|
|
179
|
+
CREATE INDEX IF NOT EXISTS idx_agent_run_leases_expiry
|
|
180
|
+
ON agent_run_leases(lease_expires_at);
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def __init__(self, db_path: str | Path = "./sessions.db") -> None:
|
|
184
|
+
self._db_path = str(Path(db_path).resolve())
|
|
185
|
+
# 建表 DDL/PRAGMA 初始化同样放入工作线程执行,与 async 方法共用同一连接参数规范。
|
|
186
|
+
run_sync_in_thread(self._init_db)
|
|
187
|
+
|
|
188
|
+
def _init_db(self) -> None:
|
|
189
|
+
with self._connect() as connection:
|
|
190
|
+
connection.executescript(self.DDL)
|
|
191
|
+
|
|
192
|
+
@contextmanager
|
|
193
|
+
def _connect(self) -> Generator[sqlite3.Connection]:
|
|
194
|
+
"""获取 SQLite 连接(同步,自动提交/回滚;统一 timeout 与 WAL 规范)。"""
|
|
195
|
+
with sqlite_connection(self._db_path) as connection:
|
|
196
|
+
yield connection
|
|
197
|
+
|
|
198
|
+
async def claim(
|
|
199
|
+
self,
|
|
200
|
+
thread_id: str,
|
|
201
|
+
run_id: str,
|
|
202
|
+
worker_id: str,
|
|
203
|
+
*,
|
|
204
|
+
ttl_seconds: float,
|
|
205
|
+
) -> bool:
|
|
206
|
+
_validate_ttl(ttl_seconds)
|
|
207
|
+
now = _now()
|
|
208
|
+
expires_at = now + timedelta(seconds=ttl_seconds)
|
|
209
|
+
|
|
210
|
+
def _op() -> bool:
|
|
211
|
+
with self._connect() as connection:
|
|
212
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
213
|
+
row = connection.execute(
|
|
214
|
+
"SELECT worker_id, lease_expires_at FROM agent_run_leases WHERE thread_id=? AND run_id=?",
|
|
215
|
+
(thread_id, run_id),
|
|
216
|
+
).fetchone()
|
|
217
|
+
if row and row[0] != worker_id and _parse(row[1]) > now:
|
|
218
|
+
return False
|
|
219
|
+
connection.execute(
|
|
220
|
+
"""INSERT INTO agent_run_leases
|
|
221
|
+
(thread_id,run_id,worker_id,lease_expires_at,heartbeat_at)
|
|
222
|
+
VALUES (?,?,?,?,?)
|
|
223
|
+
ON CONFLICT(thread_id,run_id) DO UPDATE SET
|
|
224
|
+
worker_id=excluded.worker_id,
|
|
225
|
+
lease_expires_at=excluded.lease_expires_at,
|
|
226
|
+
heartbeat_at=excluded.heartbeat_at""",
|
|
227
|
+
(thread_id, run_id, worker_id, _iso(expires_at), _iso(now)),
|
|
228
|
+
)
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
return await _run_in_thread(_op)
|
|
232
|
+
|
|
233
|
+
async def renew(
|
|
234
|
+
self,
|
|
235
|
+
thread_id: str,
|
|
236
|
+
run_id: str,
|
|
237
|
+
worker_id: str,
|
|
238
|
+
*,
|
|
239
|
+
ttl_seconds: float,
|
|
240
|
+
) -> bool:
|
|
241
|
+
_validate_ttl(ttl_seconds)
|
|
242
|
+
now = _now()
|
|
243
|
+
|
|
244
|
+
def _op() -> int:
|
|
245
|
+
with self._connect() as connection:
|
|
246
|
+
cursor = connection.execute(
|
|
247
|
+
"""UPDATE agent_run_leases
|
|
248
|
+
SET lease_expires_at=?, heartbeat_at=?
|
|
249
|
+
WHERE thread_id=? AND run_id=? AND worker_id=? AND lease_expires_at>?""",
|
|
250
|
+
(
|
|
251
|
+
_iso(now + timedelta(seconds=ttl_seconds)),
|
|
252
|
+
_iso(now),
|
|
253
|
+
thread_id,
|
|
254
|
+
run_id,
|
|
255
|
+
worker_id,
|
|
256
|
+
_iso(now),
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
return cursor.rowcount
|
|
260
|
+
|
|
261
|
+
return await _run_in_thread(_op) == 1
|
|
262
|
+
|
|
263
|
+
async def release(self, thread_id: str, run_id: str, worker_id: str) -> bool:
|
|
264
|
+
def _op() -> int:
|
|
265
|
+
with self._connect() as connection:
|
|
266
|
+
cursor = connection.execute(
|
|
267
|
+
"DELETE FROM agent_run_leases WHERE thread_id=? AND run_id=? AND worker_id=?",
|
|
268
|
+
(thread_id, run_id, worker_id),
|
|
269
|
+
)
|
|
270
|
+
return cursor.rowcount
|
|
271
|
+
|
|
272
|
+
return await _run_in_thread(_op) == 1
|
|
273
|
+
|
|
274
|
+
async def list_expired(self) -> list[RunLease]:
|
|
275
|
+
def _op() -> list[Any]:
|
|
276
|
+
with self._connect() as connection:
|
|
277
|
+
return connection.execute(
|
|
278
|
+
"""SELECT thread_id,run_id,worker_id,lease_expires_at,heartbeat_at
|
|
279
|
+
FROM agent_run_leases WHERE lease_expires_at<=?""",
|
|
280
|
+
(_iso(_now()),),
|
|
281
|
+
).fetchall()
|
|
282
|
+
|
|
283
|
+
rows = await _run_in_thread(_op)
|
|
284
|
+
return [RunLease(*row) for row in rows]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class PostgresRunLeaseStore(RunLeaseStore):
|
|
288
|
+
"""生产环境 PostgreSQL 租约实现。"""
|
|
289
|
+
|
|
290
|
+
def __init__(self, dsn: str) -> None:
|
|
291
|
+
self._dsn = dsn.replace("postgresql+asyncpg://", "postgresql://")
|
|
292
|
+
self._pool: Any = None
|
|
293
|
+
|
|
294
|
+
async def initialize(self) -> None:
|
|
295
|
+
try:
|
|
296
|
+
import asyncpg
|
|
297
|
+
except ImportError as exc:
|
|
298
|
+
raise ImportError("PostgresRunLeaseStore 需要 asyncpg") from exc
|
|
299
|
+
self._pool = await asyncpg.create_pool(self._dsn, min_size=2, max_size=10)
|
|
300
|
+
async with self._pool.acquire() as connection:
|
|
301
|
+
await connection.execute(
|
|
302
|
+
"""CREATE TABLE IF NOT EXISTS agent_run_leases (
|
|
303
|
+
thread_id TEXT NOT NULL,
|
|
304
|
+
run_id TEXT NOT NULL,
|
|
305
|
+
worker_id TEXT NOT NULL,
|
|
306
|
+
lease_expires_at TIMESTAMPTZ NOT NULL,
|
|
307
|
+
heartbeat_at TIMESTAMPTZ NOT NULL,
|
|
308
|
+
PRIMARY KEY (thread_id, run_id)
|
|
309
|
+
);
|
|
310
|
+
CREATE INDEX IF NOT EXISTS idx_agent_run_leases_expiry
|
|
311
|
+
ON agent_run_leases(lease_expires_at);"""
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
async def close(self) -> None:
|
|
315
|
+
if self._pool is not None:
|
|
316
|
+
await self._pool.close()
|
|
317
|
+
self._pool = None
|
|
318
|
+
|
|
319
|
+
def _check(self) -> None:
|
|
320
|
+
if self._pool is None:
|
|
321
|
+
raise RuntimeError("PostgresRunLeaseStore 未初始化")
|
|
322
|
+
|
|
323
|
+
async def claim(
|
|
324
|
+
self,
|
|
325
|
+
thread_id: str,
|
|
326
|
+
run_id: str,
|
|
327
|
+
worker_id: str,
|
|
328
|
+
*,
|
|
329
|
+
ttl_seconds: float,
|
|
330
|
+
) -> bool:
|
|
331
|
+
_validate_ttl(ttl_seconds)
|
|
332
|
+
self._check()
|
|
333
|
+
async with self._pool.acquire() as connection:
|
|
334
|
+
row = await connection.fetchrow(
|
|
335
|
+
"""INSERT INTO agent_run_leases
|
|
336
|
+
(thread_id,run_id,worker_id,lease_expires_at,heartbeat_at)
|
|
337
|
+
VALUES ($1,$2,$3,NOW()+($4::double precision * INTERVAL '1 second'),NOW())
|
|
338
|
+
ON CONFLICT(thread_id,run_id) DO UPDATE SET
|
|
339
|
+
worker_id=EXCLUDED.worker_id,
|
|
340
|
+
lease_expires_at=EXCLUDED.lease_expires_at,
|
|
341
|
+
heartbeat_at=EXCLUDED.heartbeat_at
|
|
342
|
+
WHERE agent_run_leases.lease_expires_at<=NOW()
|
|
343
|
+
OR agent_run_leases.worker_id=EXCLUDED.worker_id
|
|
344
|
+
RETURNING thread_id""",
|
|
345
|
+
thread_id,
|
|
346
|
+
run_id,
|
|
347
|
+
worker_id,
|
|
348
|
+
ttl_seconds,
|
|
349
|
+
)
|
|
350
|
+
return row is not None
|
|
351
|
+
|
|
352
|
+
async def renew(
|
|
353
|
+
self,
|
|
354
|
+
thread_id: str,
|
|
355
|
+
run_id: str,
|
|
356
|
+
worker_id: str,
|
|
357
|
+
*,
|
|
358
|
+
ttl_seconds: float,
|
|
359
|
+
) -> bool:
|
|
360
|
+
_validate_ttl(ttl_seconds)
|
|
361
|
+
self._check()
|
|
362
|
+
async with self._pool.acquire() as connection:
|
|
363
|
+
result = await connection.execute(
|
|
364
|
+
"""UPDATE agent_run_leases
|
|
365
|
+
SET lease_expires_at=NOW()+($4::double precision * INTERVAL '1 second'),
|
|
366
|
+
heartbeat_at=NOW()
|
|
367
|
+
WHERE thread_id=$1 AND run_id=$2 AND worker_id=$3
|
|
368
|
+
AND lease_expires_at>NOW()""",
|
|
369
|
+
thread_id,
|
|
370
|
+
run_id,
|
|
371
|
+
worker_id,
|
|
372
|
+
ttl_seconds,
|
|
373
|
+
)
|
|
374
|
+
return bool(result == "UPDATE 1")
|
|
375
|
+
|
|
376
|
+
async def release(self, thread_id: str, run_id: str, worker_id: str) -> bool:
|
|
377
|
+
self._check()
|
|
378
|
+
async with self._pool.acquire() as connection:
|
|
379
|
+
result = await connection.execute(
|
|
380
|
+
"DELETE FROM agent_run_leases WHERE thread_id=$1 AND run_id=$2 AND worker_id=$3",
|
|
381
|
+
thread_id,
|
|
382
|
+
run_id,
|
|
383
|
+
worker_id,
|
|
384
|
+
)
|
|
385
|
+
return bool(result == "DELETE 1")
|
|
386
|
+
|
|
387
|
+
async def list_expired(self) -> list[RunLease]:
|
|
388
|
+
self._check()
|
|
389
|
+
async with self._pool.acquire() as connection:
|
|
390
|
+
rows = await connection.fetch(
|
|
391
|
+
"""SELECT thread_id,run_id,worker_id,lease_expires_at,heartbeat_at
|
|
392
|
+
FROM agent_run_leases WHERE lease_expires_at<=NOW()"""
|
|
393
|
+
)
|
|
394
|
+
return [
|
|
395
|
+
RunLease(
|
|
396
|
+
thread_id=str(row["thread_id"]),
|
|
397
|
+
run_id=str(row["run_id"]),
|
|
398
|
+
worker_id=str(row["worker_id"]),
|
|
399
|
+
lease_expires_at=_iso(_parse(row["lease_expires_at"])),
|
|
400
|
+
heartbeat_at=_iso(_parse(row["heartbeat_at"])),
|
|
401
|
+
)
|
|
402
|
+
for row in rows
|
|
403
|
+
]
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _validate_ttl(ttl_seconds: float) -> None:
|
|
407
|
+
if ttl_seconds <= 0:
|
|
408
|
+
raise ValueError("ttl_seconds 必须大于 0")
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def create_run_lease_store(
|
|
412
|
+
env: str = "development",
|
|
413
|
+
*,
|
|
414
|
+
sqlite_path: str = "./sessions.db",
|
|
415
|
+
postgres_url: str | None = None,
|
|
416
|
+
) -> RunLeaseStore:
|
|
417
|
+
"""按环境创建 Run 租约存储。"""
|
|
418
|
+
normalized = env.lower()
|
|
419
|
+
if normalized in {"test", "testing"}:
|
|
420
|
+
return MemoryRunLeaseStore()
|
|
421
|
+
if normalized in {"production", "prod"}:
|
|
422
|
+
if not postgres_url:
|
|
423
|
+
raise ValueError("生产环境需要 PostgreSQL 连接地址")
|
|
424
|
+
return PostgresRunLeaseStore(postgres_url)
|
|
425
|
+
return SqliteRunLeaseStore(sqlite_path)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
__all__ = [
|
|
429
|
+
"MemoryRunLeaseStore",
|
|
430
|
+
"PostgresRunLeaseStore",
|
|
431
|
+
"RunLease",
|
|
432
|
+
"RunLeaseStore",
|
|
433
|
+
"SqliteRunLeaseStore",
|
|
434
|
+
"create_run_lease_store",
|
|
435
|
+
]
|