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.
Files changed (104) hide show
  1. agent_core/__init__.py +525 -0
  2. agent_core/access/__init__.py +6 -0
  3. agent_core/access/context.py +45 -0
  4. agent_core/access/guard.py +40 -0
  5. agent_core/acp/README.md +236 -0
  6. agent_core/acp/__init__.py +20 -0
  7. agent_core/acp/server.py +351 -0
  8. agent_core/acp/types.py +86 -0
  9. agent_core/callbacks/__init__.py +17 -0
  10. agent_core/callbacks/manager.py +105 -0
  11. agent_core/checkpoint/__init__.py +19 -0
  12. agent_core/checkpoint/naming.py +93 -0
  13. agent_core/checkpoint/store.py +186 -0
  14. agent_core/checkpoint/travel.py +448 -0
  15. agent_core/compaction/__init__.py +57 -0
  16. agent_core/compaction/middleware.py +428 -0
  17. agent_core/compaction/prompts.py +32 -0
  18. agent_core/compaction/pruner.py +121 -0
  19. agent_core/compaction/region.py +145 -0
  20. agent_core/compaction/spill.py +81 -0
  21. agent_core/compaction/spill_tool.py +42 -0
  22. agent_core/compaction/summarizer.py +95 -0
  23. agent_core/compaction/token_limit.py +74 -0
  24. agent_core/compaction/types.py +93 -0
  25. agent_core/docs.py +15 -0
  26. agent_core/documents/__init__.py +23 -0
  27. agent_core/documents/loaders.py +79 -0
  28. agent_core/documents/splitters.py +94 -0
  29. agent_core/documents/types.py +197 -0
  30. agent_core/errors.py +81 -0
  31. agent_core/guardrails/__init__.py +19 -0
  32. agent_core/guardrails/core.py +186 -0
  33. agent_core/hitl/__init__.py +15 -0
  34. agent_core/hitl/core.py +517 -0
  35. agent_core/mcp/__init__.py +14 -0
  36. agent_core/mcp/client.py +212 -0
  37. agent_core/mcp/errors.py +26 -0
  38. agent_core/middleware/__init__.py +26 -0
  39. agent_core/middleware/base.py +283 -0
  40. agent_core/model/__init__.py +73 -0
  41. agent_core/model/catalog.py +73 -0
  42. agent_core/model/core.py +179 -0
  43. agent_core/model/governance.py +317 -0
  44. agent_core/model/providers/__init__.py +43 -0
  45. agent_core/model/providers/anthropic.py +294 -0
  46. agent_core/model/providers/common.py +28 -0
  47. agent_core/model/providers/factory.py +107 -0
  48. agent_core/model/providers/gemini.py +273 -0
  49. agent_core/model/providers/ollama.py +80 -0
  50. agent_core/model/providers/openai_compatible.py +526 -0
  51. agent_core/model/structured.py +200 -0
  52. agent_core/observability/__init__.py +21 -0
  53. agent_core/observability/core.py +311 -0
  54. agent_core/ports/__init__.py +31 -0
  55. agent_core/ports/memory.py +130 -0
  56. agent_core/ports/model_selection.py +64 -0
  57. agent_core/ports/storage.py +114 -0
  58. agent_core/prompts/__init__.py +19 -0
  59. agent_core/prompts/registry.py +236 -0
  60. agent_core/prompts/templates.py +166 -0
  61. agent_core/protocol/__init__.py +31 -0
  62. agent_core/protocol/capabilities.py +49 -0
  63. agent_core/protocol/messages.py +100 -0
  64. agent_core/protocol/runtime.py +293 -0
  65. agent_core/py.typed +1 -0
  66. agent_core/retrieval/__init__.py +40 -0
  67. agent_core/retrieval/_common.py +121 -0
  68. agent_core/retrieval/chroma.py +269 -0
  69. agent_core/retrieval/core.py +63 -0
  70. agent_core/retrieval/factory.py +72 -0
  71. agent_core/retrieval/memory.py +232 -0
  72. agent_core/retrieval/pgvector.py +239 -0
  73. agent_core/retrieval/types.py +95 -0
  74. agent_core/runtime/__init__.py +28 -0
  75. agent_core/runtime/compat.py +98 -0
  76. agent_core/runtime/react.py +888 -0
  77. agent_core/runtime/service.py +557 -0
  78. agent_core/runtime/store.py +55 -0
  79. agent_core/serialization/__init__.py +21 -0
  80. agent_core/serialization/registry.py +318 -0
  81. agent_core/skills/__init__.py +24 -0
  82. agent_core/skills/errors.py +27 -0
  83. agent_core/skills/loader.py +258 -0
  84. agent_core/skills/registry.py +205 -0
  85. agent_core/skills/types.py +56 -0
  86. agent_core/storage/__init__.py +107 -0
  87. agent_core/storage/_sqlite_utils.py +92 -0
  88. agent_core/storage/context.py +382 -0
  89. agent_core/storage/lease.py +435 -0
  90. agent_core/storage/memory.py +604 -0
  91. agent_core/storage/model_selection.py +360 -0
  92. agent_core/storage/runtime.py +899 -0
  93. agent_core/storage/session.py +904 -0
  94. agent_core/storage/workflow.py +411 -0
  95. agent_core/tools/__init__.py +5 -0
  96. agent_core/tools/executor.py +306 -0
  97. agent_core/workflow/__init__.py +25 -0
  98. agent_core/workflow/durable.py +175 -0
  99. agent_core/workflow/state_machine.py +427 -0
  100. zero_agent_core-0.2.0.dist-info/METADATA +237 -0
  101. zero_agent_core-0.2.0.dist-info/RECORD +104 -0
  102. zero_agent_core-0.2.0.dist-info/WHEEL +4 -0
  103. zero_agent_core-0.2.0.dist-info/entry_points.txt +3 -0
  104. 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
+ ]