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,382 @@
1
+ """轻量级长期上下文存储。
2
+
3
+ 它保存业务引用和用户偏好,而不是把所有历史消息无限塞进 prompt。
4
+ 应用按环境选择内存、SQLite 或 PostgreSQL;JSON 文件实现仅保留兼容。
5
+
6
+ 存跨会话的业务记忆
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import copy
13
+ import json
14
+ import sqlite3
15
+ from collections.abc import Generator
16
+ from contextlib import contextmanager
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from agent_core.access import AccessContext
21
+ from agent_core.storage._sqlite_utils import (
22
+ _run_in_thread,
23
+ run_sync_in_thread,
24
+ sqlite_connection,
25
+ )
26
+
27
+
28
+ # ──────────────────────────────────────────────
29
+ # 上下文存储实现
30
+ # ──────────────────────────────────────────────
31
+ class ContextStore:
32
+ """按 thread 隔离的持久化上下文存储。"""
33
+
34
+ def __init__(
35
+ self,
36
+ path: str | Path | None = "./agent_context.json",
37
+ *,
38
+ require_access: bool = False,
39
+ ) -> None:
40
+ self._path = Path(path) if path else None
41
+ self._strict_access = require_access
42
+ self._data: dict[str, dict[str, Any]] = {}
43
+ self._lock = asyncio.Lock()
44
+ self._loaded = False
45
+
46
+ async def initialize(self) -> None:
47
+ """初始化存储;文件和内存实现不需要额外操作。"""
48
+
49
+ async def close(self) -> None:
50
+ """关闭存储;文件和内存实现不需要额外操作。"""
51
+
52
+ # 确保上下文数据已加载
53
+ async def _ensure_loaded(self) -> None:
54
+ if self._loaded:
55
+ return
56
+ if self._path and self._path.exists():
57
+ loaded = json.loads(self._path.read_text(encoding="utf-8"))
58
+ if not isinstance(loaded, dict):
59
+ raise ValueError(f"上下文文件根节点必须是对象:{self._path}")
60
+ self._data = loaded
61
+ self._loaded = True
62
+
63
+ # 刷新上下文数据到文件
64
+ async def _flush(self) -> None:
65
+ if not self._path:
66
+ return
67
+ self._path.parent.mkdir(parents=True, exist_ok=True)
68
+ tmp = self._path.with_suffix(self._path.suffix + ".tmp")
69
+ tmp.write_text(json.dumps(self._data, ensure_ascii=False, indent=2), encoding="utf-8")
70
+ tmp.replace(self._path)
71
+
72
+ # 检查上下文数据访问权限
73
+ def _check_scope(
74
+ self,
75
+ data: dict[str, Any],
76
+ access: AccessContext | None,
77
+ ) -> None:
78
+ if self._strict_access and access is None:
79
+ raise PermissionError("该 ContextStore 要求提供 AccessContext")
80
+ owner = data.get("_access")
81
+ if self._strict_access and data and not isinstance(owner, dict):
82
+ raise PermissionError("该 thread 的上下文尚未绑定所有者")
83
+ if (
84
+ access
85
+ and isinstance(owner, dict)
86
+ and not access.can_access(owner.get("user_id"), owner.get("tenant_id"))
87
+ ):
88
+ raise PermissionError("无权访问该 thread 的上下文")
89
+
90
+ # 获取上下文数据
91
+ async def get(
92
+ self,
93
+ thread_id: str,
94
+ *,
95
+ access: AccessContext | None = None,
96
+ ) -> dict[str, Any]:
97
+ async with self._lock:
98
+ await self._ensure_loaded()
99
+ value = copy.deepcopy(self._data.get(thread_id, {}))
100
+ self._check_scope(value, access)
101
+ return value
102
+
103
+ # 更新上下文数据
104
+ async def update(
105
+ self,
106
+ thread_id: str,
107
+ values: dict[str, Any],
108
+ *,
109
+ access: AccessContext | None = None,
110
+ ) -> dict[str, Any]:
111
+ self._validate_values(values)
112
+ async with self._lock:
113
+ await self._ensure_loaded()
114
+ current = self._data.setdefault(thread_id, {})
115
+ self._check_scope(current, access)
116
+ if access and "_access" not in current:
117
+ current["_access"] = access.to_dict()
118
+ current.update(copy.deepcopy(values))
119
+ await self._flush()
120
+ return copy.deepcopy(current)
121
+
122
+ @staticmethod
123
+ def _validate_values(values: dict[str, Any]) -> None:
124
+ if "_access" in values:
125
+ raise ValueError("_access 是存储层保留字段,不能通过上下文更新修改")
126
+
127
+ # 清除上下文数据
128
+ async def clear(
129
+ self,
130
+ thread_id: str,
131
+ *,
132
+ access: AccessContext | None = None,
133
+ ) -> None:
134
+ async with self._lock:
135
+ await self._ensure_loaded()
136
+ self._check_scope(self._data.get(thread_id, {}), access)
137
+ self._data.pop(thread_id, None)
138
+ await self._flush()
139
+
140
+ # 统一端口名称,同时保留业务层已有的 get/update/clear 调用。
141
+ async def get_context(self, thread_id: str, *, access: AccessContext | None = None) -> dict[str, Any]:
142
+ return await self.get(thread_id, access=access)
143
+
144
+ async def update_context(
145
+ self,
146
+ thread_id: str,
147
+ values: dict[str, Any],
148
+ *,
149
+ access: AccessContext | None = None,
150
+ ) -> dict[str, Any]:
151
+ return await self.update(thread_id, values, access=access)
152
+
153
+ async def clear_context(self, thread_id: str, *, access: AccessContext | None = None) -> None:
154
+ await self.clear(thread_id, access=access)
155
+
156
+
157
+ # ──────────────────────────────────────────────
158
+ # 内存上下文存储实现
159
+ # ──────────────────────────────────────────────
160
+ class MemoryContextStore(ContextStore):
161
+ """测试专用内存上下文。"""
162
+
163
+ def __init__(self, *, require_access: bool = False) -> None:
164
+ super().__init__(path=None, require_access=require_access)
165
+
166
+
167
+ # ──────────────────────────────────────────────
168
+ # SQLite 上下文存储实现
169
+ # ──────────────────────────────────────────────
170
+ class SqliteContextStore(ContextStore):
171
+ """开发环境 SQLite 上下文存储。"""
172
+
173
+ def __init__(
174
+ self,
175
+ db_path: str | Path = "./sessions.db",
176
+ *,
177
+ require_access: bool = False,
178
+ ) -> None:
179
+ self._db_path = str(Path(db_path).resolve())
180
+ self._strict_access = require_access
181
+ # 建表 DDL/PRAGMA 初始化同样放入工作线程执行,与 async 方法共用同一连接参数规范。
182
+ run_sync_in_thread(self._init_db)
183
+
184
+ def _init_db(self) -> None:
185
+ with self._connect() as conn:
186
+ conn.execute(
187
+ """CREATE TABLE IF NOT EXISTS agent_context (
188
+ thread_id TEXT PRIMARY KEY,
189
+ data TEXT NOT NULL DEFAULT '{}',
190
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
191
+ )"""
192
+ )
193
+
194
+ # 连接 SQLite 数据库
195
+ @contextmanager
196
+ def _connect(self) -> Generator[sqlite3.Connection]:
197
+ """打开一个自动提交或回滚的 SQLite 连接(统一 timeout 与 WAL 规范)。"""
198
+ with sqlite_connection(self._db_path) as conn:
199
+ yield conn
200
+
201
+ # 获取上下文数据
202
+ async def get(
203
+ self,
204
+ thread_id: str,
205
+ *,
206
+ access: AccessContext | None = None,
207
+ ) -> dict[str, Any]:
208
+ def _op() -> Any:
209
+ with self._connect() as conn:
210
+ return conn.execute(
211
+ "SELECT data FROM agent_context WHERE thread_id = ?", (thread_id,)
212
+ ).fetchone()
213
+
214
+ row = await _run_in_thread(_op)
215
+ value = json.loads(row[0]) if row else {}
216
+ self._check_scope(value, access)
217
+ return value
218
+
219
+ # 更新上下文数据
220
+ async def update(
221
+ self,
222
+ thread_id: str,
223
+ values: dict[str, Any],
224
+ *,
225
+ access: AccessContext | None = None,
226
+ ) -> dict[str, Any]:
227
+ self._validate_values(values)
228
+
229
+ def _op() -> dict[str, Any]:
230
+ with self._connect() as conn:
231
+ conn.execute("BEGIN IMMEDIATE")
232
+ row = conn.execute(
233
+ "SELECT data FROM agent_context WHERE thread_id = ?",
234
+ (thread_id,),
235
+ ).fetchone()
236
+ current = json.loads(row[0]) if row else {}
237
+ self._check_scope(current, access)
238
+ if access and "_access" not in current:
239
+ current["_access"] = access.to_dict()
240
+ current.update(copy.deepcopy(values))
241
+ conn.execute(
242
+ """INSERT INTO agent_context(thread_id, data) VALUES (?, ?)
243
+ ON CONFLICT(thread_id) DO UPDATE SET data=excluded.data,
244
+ updated_at=CURRENT_TIMESTAMP""",
245
+ (thread_id, json.dumps(current, ensure_ascii=False)),
246
+ )
247
+ return copy.deepcopy(current)
248
+
249
+ return await _run_in_thread(_op)
250
+
251
+ # 清除上下文数据
252
+ async def clear(
253
+ self,
254
+ thread_id: str,
255
+ *,
256
+ access: AccessContext | None = None,
257
+ ) -> None:
258
+ await self.get(thread_id, access=access)
259
+
260
+ def _op() -> None:
261
+ with self._connect() as conn:
262
+ conn.execute("DELETE FROM agent_context WHERE thread_id = ?", (thread_id,))
263
+
264
+ await _run_in_thread(_op)
265
+
266
+
267
+ # ──────────────────────────────────────────────
268
+ # PostgreSQL 上下文存储实现
269
+ # ──────────────────────────────────────────────
270
+ class PostgresContextStore(ContextStore):
271
+ """生产环境 PostgreSQL 上下文存储。"""
272
+
273
+ def __init__(self, dsn: str, *, require_access: bool = False) -> None:
274
+ self._dsn = dsn.replace("postgresql+asyncpg://", "postgresql://")
275
+ self._pool: Any = None
276
+ self._strict_access = require_access
277
+
278
+ # 初始化 PostgreSQL 数据库连接池
279
+ # 创建上下文表
280
+ async def initialize(self) -> None:
281
+ try:
282
+ import asyncpg
283
+ except ImportError as exc:
284
+ raise ImportError(
285
+ "PostgresContextStore 需要 asyncpg,请执行:uv sync --extra production"
286
+ ) from exc
287
+ self._pool = await asyncpg.create_pool(self._dsn, min_size=2, max_size=10)
288
+ async with self._pool.acquire() as conn:
289
+ # 创建上下文表
290
+ await conn.execute("""
291
+ CREATE TABLE IF NOT EXISTS agent_context (
292
+ thread_id TEXT PRIMARY KEY,
293
+ data JSONB NOT NULL DEFAULT '{}'::jsonb,
294
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
295
+ )
296
+ """)
297
+
298
+ def _check(self) -> None:
299
+ if self._pool is None:
300
+ raise RuntimeError("PostgresContextStore 未初始化")
301
+
302
+ async def close(self) -> None:
303
+ if self._pool:
304
+ await self._pool.close()
305
+
306
+ async def get(
307
+ self,
308
+ thread_id: str,
309
+ *,
310
+ access: AccessContext | None = None,
311
+ ) -> dict[str, Any]:
312
+ self._check()
313
+ async with self._pool.acquire() as conn:
314
+ row = await conn.fetchrow(
315
+ "SELECT data FROM agent_context WHERE thread_id=$1",
316
+ thread_id,
317
+ )
318
+ if row is None:
319
+ self._check_scope({}, access)
320
+ return {}
321
+ value = json.loads(row["data"]) if isinstance(row["data"], str) else dict(row["data"])
322
+ self._check_scope(value, access)
323
+ return value
324
+
325
+ async def update(
326
+ self,
327
+ thread_id: str,
328
+ values: dict[str, Any],
329
+ *,
330
+ access: AccessContext | None = None,
331
+ ) -> dict[str, Any]:
332
+ self._check()
333
+ self._validate_values(values)
334
+ candidate = copy.deepcopy(values)
335
+ if access:
336
+ candidate = {"_access": access.to_dict(), **candidate}
337
+ async with self._pool.acquire() as conn:
338
+ async with conn.transaction():
339
+ # 冲突更新永远保留已有 _access;首次并发认领后再校验返回归属。
340
+ row = await conn.fetchrow(
341
+ """INSERT INTO agent_context(thread_id,data) VALUES ($1,$2::jsonb)
342
+ ON CONFLICT(thread_id) DO UPDATE SET
343
+ data=agent_context.data || (EXCLUDED.data - '_access'), updated_at=NOW()
344
+ RETURNING data""",
345
+ thread_id,
346
+ json.dumps(candidate, ensure_ascii=False),
347
+ )
348
+ result = json.loads(row["data"]) if isinstance(row["data"], str) else dict(row["data"])
349
+ self._check_scope(result, access)
350
+ return result
351
+
352
+ async def clear(
353
+ self,
354
+ thread_id: str,
355
+ *,
356
+ access: AccessContext | None = None,
357
+ ) -> None:
358
+ await self.get(thread_id, access=access)
359
+ self._check()
360
+ async with self._pool.acquire() as conn:
361
+ await conn.execute("DELETE FROM agent_context WHERE thread_id=$1", thread_id)
362
+
363
+
364
+ # ──────────────────────────────────────────────
365
+ # 创建上下文存储
366
+ # ──────────────────────────────────────────────
367
+ def create_context_store(
368
+ env: str = "development",
369
+ *,
370
+ sqlite_path: str = "./sessions.db",
371
+ postgres_url: str | None = None,
372
+ require_access: bool = False,
373
+ ) -> ContextStore:
374
+ """按环境创建上下文存储。"""
375
+ normalized = env.lower()
376
+ if normalized in {"test", "testing"}:
377
+ return MemoryContextStore(require_access=require_access)
378
+ if normalized in {"production", "prod"}:
379
+ if not postgres_url:
380
+ raise ValueError("生产环境需要配置 PostgreSQL 连接地址")
381
+ return PostgresContextStore(postgres_url, require_access=require_access)
382
+ return SqliteContextStore(sqlite_path, require_access=require_access)