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,448 @@
|
|
|
1
|
+
"""时间旅行 checkpoint:版本管理与回滚。
|
|
2
|
+
|
|
3
|
+
职责:
|
|
4
|
+
- `CheckpointVersion`:单个 checkpoint 版本(state + metadata)的值对象;
|
|
5
|
+
- `TimeTravelCheckpointer`:多版本存储,支持保存/加载/回滚/对比/删除版本,
|
|
6
|
+
每次保存创建新版本,可选 JSON 文件持久化,版本数量有上限。
|
|
7
|
+
|
|
8
|
+
依赖方向:本模块仅依赖 `naming`(文件名清洗与原子写入),不反向依赖 `store`。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import copy
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import uuid
|
|
17
|
+
from datetime import UTC, datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from agent_core.checkpoint.naming import _atomic_write_json, _sanitize_thread_id
|
|
22
|
+
from agent_core.errors import CheckpointError
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CheckpointVersion:
|
|
28
|
+
"""Checkpoint 版本信息。"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
version_id: str,
|
|
33
|
+
timestamp: str,
|
|
34
|
+
state: dict[str, Any],
|
|
35
|
+
metadata: dict[str, Any] | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""初始化。
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
version_id: 版本 ID。
|
|
41
|
+
timestamp: 时间戳(ISO 格式)。
|
|
42
|
+
state: 状态字典。
|
|
43
|
+
metadata: 元数据(可选)。
|
|
44
|
+
"""
|
|
45
|
+
self.version_id = version_id
|
|
46
|
+
self.timestamp = timestamp
|
|
47
|
+
self.state = state
|
|
48
|
+
self.metadata = metadata or {}
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict[str, Any]:
|
|
51
|
+
"""转换为字典。"""
|
|
52
|
+
return {
|
|
53
|
+
"version_id": self.version_id,
|
|
54
|
+
"timestamp": self.timestamp,
|
|
55
|
+
"state": self.state,
|
|
56
|
+
"metadata": self.metadata,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_dict(cls, data: dict[str, Any]) -> CheckpointVersion:
|
|
61
|
+
"""从字典创建。"""
|
|
62
|
+
return cls(
|
|
63
|
+
version_id=data["version_id"],
|
|
64
|
+
timestamp=data["timestamp"],
|
|
65
|
+
state=data["state"],
|
|
66
|
+
metadata=data.get("metadata"),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TimeTravelCheckpointer:
|
|
71
|
+
"""支持时间旅行的 Checkpointer。
|
|
72
|
+
|
|
73
|
+
功能:
|
|
74
|
+
1. 保存多个版本(每次保存创建新版本)
|
|
75
|
+
2. 查看历史版本列表
|
|
76
|
+
3. 回滚到任意版本
|
|
77
|
+
4. 对比两个版本
|
|
78
|
+
5. 每个 thread 的版本数量有上限,超过后淘汰最旧版本,避免无限膨胀
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
storage_dir: str | Path | None = None,
|
|
84
|
+
*,
|
|
85
|
+
max_versions_per_thread: int = 100,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""初始化。
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
storage_dir: 存储目录(可选,None 表示仅内存存储)。
|
|
91
|
+
max_versions_per_thread: 每个 thread 保留的最大版本数,
|
|
92
|
+
追加超过上限时淘汰最旧版本(默认 100)。
|
|
93
|
+
"""
|
|
94
|
+
if max_versions_per_thread < 1:
|
|
95
|
+
raise ValueError("max_versions_per_thread 必须大于 0")
|
|
96
|
+
self._storage_dir = Path(storage_dir) if storage_dir else None
|
|
97
|
+
if self._storage_dir:
|
|
98
|
+
self._storage_dir.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
# thread_id -> list of CheckpointVersion
|
|
101
|
+
self._versions: dict[str, list[CheckpointVersion]] = {}
|
|
102
|
+
|
|
103
|
+
# 当前版本索引
|
|
104
|
+
self._current_version: dict[str, int] = {}
|
|
105
|
+
|
|
106
|
+
# 每个 thread 的版本数量上限
|
|
107
|
+
self._max_versions_per_thread = max_versions_per_thread
|
|
108
|
+
|
|
109
|
+
def _get_version_file(self, thread_id: str) -> Path | None:
|
|
110
|
+
"""获取版本文件路径(文件名经 `_sanitize_thread_id` 严格清洗)。"""
|
|
111
|
+
if not self._storage_dir:
|
|
112
|
+
return None
|
|
113
|
+
safe_id = _sanitize_thread_id(thread_id)
|
|
114
|
+
return self._storage_dir / f"{safe_id}_versions.json"
|
|
115
|
+
|
|
116
|
+
async def save(
|
|
117
|
+
self,
|
|
118
|
+
thread_id: str,
|
|
119
|
+
state: dict[str, Any],
|
|
120
|
+
metadata: dict[str, Any] | None = None,
|
|
121
|
+
) -> str:
|
|
122
|
+
"""保存新版本。
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
thread_id: 会话 ID。
|
|
126
|
+
state: 状态字典。
|
|
127
|
+
metadata: 元数据(可选)。
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
版本 ID。
|
|
131
|
+
"""
|
|
132
|
+
# 创建版本
|
|
133
|
+
version_id = str(uuid.uuid4())
|
|
134
|
+
timestamp = datetime.now(UTC).isoformat()
|
|
135
|
+
|
|
136
|
+
version = CheckpointVersion(
|
|
137
|
+
version_id=version_id,
|
|
138
|
+
timestamp=timestamp,
|
|
139
|
+
state=copy.deepcopy(state),
|
|
140
|
+
metadata=metadata or {},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# 添加到版本列表(超过容量上限时淘汰最旧版本,指针随淘汰前移)
|
|
144
|
+
if thread_id not in self._versions:
|
|
145
|
+
self._versions[thread_id] = []
|
|
146
|
+
versions = self._versions[thread_id]
|
|
147
|
+
|
|
148
|
+
# 记录保存前的当前指针:持久化失败时按版本身份恢复(淘汰会改变索引)。
|
|
149
|
+
previous_index = self._current_version.get(thread_id)
|
|
150
|
+
previous_current: CheckpointVersion | None = None
|
|
151
|
+
if previous_index is not None and 0 <= previous_index < len(versions):
|
|
152
|
+
previous_current = versions[previous_index]
|
|
153
|
+
|
|
154
|
+
versions.append(version)
|
|
155
|
+
self._trim_versions(thread_id)
|
|
156
|
+
self._current_version[thread_id] = len(versions) - 1
|
|
157
|
+
|
|
158
|
+
# 持久化到文件
|
|
159
|
+
try:
|
|
160
|
+
await self._persist_versions(thread_id)
|
|
161
|
+
except Exception:
|
|
162
|
+
# 按身份移除本次追加的版本;并发 save_version 时列表末尾可能是其他调用者的版本。
|
|
163
|
+
for position in range(len(versions) - 1, -1, -1):
|
|
164
|
+
if versions[position] is version:
|
|
165
|
+
del versions[position]
|
|
166
|
+
break
|
|
167
|
+
self._restore_current_pointer(thread_id, previous_current)
|
|
168
|
+
raise
|
|
169
|
+
|
|
170
|
+
logger.info(
|
|
171
|
+
"Saved checkpoint version %s for thread %r (total versions: %d)",
|
|
172
|
+
version_id,
|
|
173
|
+
thread_id,
|
|
174
|
+
len(self._versions[thread_id]),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return version_id
|
|
178
|
+
|
|
179
|
+
async def load(self, thread_id: str, version_id: str | None = None) -> dict[str, Any] | None:
|
|
180
|
+
"""加载指定版本的 state。
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
thread_id: 会话 ID。
|
|
184
|
+
version_id: 版本 ID(None 表示最新版本)。
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
状态字典,如果不存在则返回 None。
|
|
188
|
+
"""
|
|
189
|
+
# 从文件加载(如果需要)
|
|
190
|
+
await self._load_versions_from_file(thread_id)
|
|
191
|
+
|
|
192
|
+
versions = self._versions.get(thread_id, [])
|
|
193
|
+
if not versions:
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
# 查找版本
|
|
197
|
+
if version_id:
|
|
198
|
+
for v in versions:
|
|
199
|
+
if v.version_id == version_id:
|
|
200
|
+
logger.info(
|
|
201
|
+
"Loaded checkpoint version %s for thread %r",
|
|
202
|
+
version_id,
|
|
203
|
+
thread_id,
|
|
204
|
+
)
|
|
205
|
+
return copy.deepcopy(v.state)
|
|
206
|
+
logger.warning(
|
|
207
|
+
"Checkpoint version %s not found for thread %r",
|
|
208
|
+
version_id,
|
|
209
|
+
thread_id,
|
|
210
|
+
)
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
# 没有显式版本时读取当前指针。回滚后不能意外跳回最新版本。
|
|
214
|
+
current_index = self._current_version.get(thread_id, len(versions) - 1)
|
|
215
|
+
current_index = max(0, min(current_index, len(versions) - 1))
|
|
216
|
+
latest = versions[current_index]
|
|
217
|
+
logger.info(
|
|
218
|
+
"Loaded current checkpoint version %s for thread %r",
|
|
219
|
+
latest.version_id,
|
|
220
|
+
thread_id,
|
|
221
|
+
)
|
|
222
|
+
return copy.deepcopy(latest.state)
|
|
223
|
+
|
|
224
|
+
async def list_versions(self, thread_id: str) -> list[dict[str, Any]]:
|
|
225
|
+
"""列出所有版本。
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
thread_id: 会话 ID。
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
版本信息列表(每个元素包含 version_id, timestamp, metadata)。
|
|
232
|
+
"""
|
|
233
|
+
await self._load_versions_from_file(thread_id)
|
|
234
|
+
|
|
235
|
+
versions = self._versions.get(thread_id, [])
|
|
236
|
+
return [
|
|
237
|
+
{
|
|
238
|
+
"version_id": v.version_id,
|
|
239
|
+
"timestamp": v.timestamp,
|
|
240
|
+
"metadata": v.metadata,
|
|
241
|
+
}
|
|
242
|
+
for v in versions
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
async def rollback(self, thread_id: str, version_id: str) -> bool:
|
|
246
|
+
"""回滚到指定版本。
|
|
247
|
+
|
|
248
|
+
注意:这不会删除后续版本,只是将当前指针移动到指定版本。
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
thread_id: 会话 ID。
|
|
252
|
+
version_id: 目标版本 ID。
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
是否成功回滚。
|
|
256
|
+
"""
|
|
257
|
+
await self._load_versions_from_file(thread_id)
|
|
258
|
+
|
|
259
|
+
versions = self._versions.get(thread_id, [])
|
|
260
|
+
if not versions:
|
|
261
|
+
logger.warning("No versions found for thread %r", thread_id)
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
# 查找版本索引
|
|
265
|
+
for i, v in enumerate(versions):
|
|
266
|
+
if v.version_id == version_id:
|
|
267
|
+
self._current_version[thread_id] = i
|
|
268
|
+
# 当前版本指针也属于时间旅行状态;必须和版本列表一起持久化,
|
|
269
|
+
# 否则进程重启后会再次指向最新版本。
|
|
270
|
+
await self._persist_versions(thread_id)
|
|
271
|
+
logger.info(
|
|
272
|
+
"Rolled back to version %s for thread %r (index: %d)",
|
|
273
|
+
version_id,
|
|
274
|
+
thread_id,
|
|
275
|
+
i,
|
|
276
|
+
)
|
|
277
|
+
return True
|
|
278
|
+
|
|
279
|
+
logger.warning(
|
|
280
|
+
"Version %s not found for thread %r",
|
|
281
|
+
version_id,
|
|
282
|
+
thread_id,
|
|
283
|
+
)
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
async def delete_version(self, thread_id: str, version_id: str) -> bool:
|
|
287
|
+
"""删除指定版本。
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
thread_id: 会话 ID。
|
|
291
|
+
version_id: 版本 ID。
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
是否成功删除。
|
|
295
|
+
"""
|
|
296
|
+
await self._load_versions_from_file(thread_id)
|
|
297
|
+
|
|
298
|
+
versions = self._versions.get(thread_id, [])
|
|
299
|
+
if not versions:
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
# 查找并删除
|
|
303
|
+
for i, v in enumerate(versions):
|
|
304
|
+
if v.version_id == version_id:
|
|
305
|
+
versions.pop(i)
|
|
306
|
+
if versions:
|
|
307
|
+
current = self._current_version.get(thread_id, len(versions) - 1)
|
|
308
|
+
if i < current:
|
|
309
|
+
current -= 1
|
|
310
|
+
self._current_version[thread_id] = max(0, min(current, len(versions) - 1))
|
|
311
|
+
else:
|
|
312
|
+
self._current_version.pop(thread_id, None)
|
|
313
|
+
await self._persist_versions(thread_id)
|
|
314
|
+
logger.info(
|
|
315
|
+
"Deleted version %s for thread %r",
|
|
316
|
+
version_id,
|
|
317
|
+
thread_id,
|
|
318
|
+
)
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
return False
|
|
322
|
+
|
|
323
|
+
async def delete(self, thread_id: str) -> None:
|
|
324
|
+
"""删除一个会话的全部版本,并满足 Agent Loop 的 Checkpointer 协议。"""
|
|
325
|
+
self._versions.pop(thread_id, None)
|
|
326
|
+
self._current_version.pop(thread_id, None)
|
|
327
|
+
file_path = self._get_version_file(thread_id)
|
|
328
|
+
if file_path is not None:
|
|
329
|
+
try:
|
|
330
|
+
file_path.unlink(missing_ok=True)
|
|
331
|
+
except OSError as exc:
|
|
332
|
+
raise CheckpointError(f"删除会话 {thread_id!r} 的 checkpoint 失败:{exc}") from exc
|
|
333
|
+
|
|
334
|
+
async def compare_versions(
|
|
335
|
+
self,
|
|
336
|
+
thread_id: str,
|
|
337
|
+
version_id_1: str,
|
|
338
|
+
version_id_2: str,
|
|
339
|
+
) -> dict[str, Any]:
|
|
340
|
+
"""对比两个版本。
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
thread_id: 会话 ID。
|
|
344
|
+
version_id_1: 版本 1 ID。
|
|
345
|
+
version_id_2: 版本 2 ID。
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
对比结果(包含差异信息)。
|
|
349
|
+
"""
|
|
350
|
+
state1 = await self.load(thread_id, version_id_1)
|
|
351
|
+
state2 = await self.load(thread_id, version_id_2)
|
|
352
|
+
|
|
353
|
+
if state1 is None or state2 is None:
|
|
354
|
+
return {"error": "One or both versions not found"}
|
|
355
|
+
|
|
356
|
+
# 简单对比(实际项目可以使用更复杂的 diff 算法)
|
|
357
|
+
diff = {
|
|
358
|
+
"version_1": version_id_1,
|
|
359
|
+
"version_2": version_id_2,
|
|
360
|
+
"keys_only_in_v1": list(set(state1.keys()) - set(state2.keys())),
|
|
361
|
+
"keys_only_in_v2": list(set(state2.keys()) - set(state1.keys())),
|
|
362
|
+
"common_keys": list(set(state1.keys()) & set(state2.keys())),
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return diff
|
|
366
|
+
|
|
367
|
+
def _trim_versions(self, thread_id: str) -> int:
|
|
368
|
+
"""把版本数量收敛到 `_max_versions_per_thread` 内,返回淘汰的最旧版本数。
|
|
369
|
+
|
|
370
|
+
淘汰只从列表头部(最旧)进行;当前指针所指版本被淘汰时,指针前移到
|
|
371
|
+
它的继任版本(最旧存活版本),未被淘汰时指针仍指向同一版本。
|
|
372
|
+
"""
|
|
373
|
+
versions = self._versions.get(thread_id)
|
|
374
|
+
if not versions:
|
|
375
|
+
return 0
|
|
376
|
+
excess = len(versions) - self._max_versions_per_thread
|
|
377
|
+
if excess <= 0:
|
|
378
|
+
return 0
|
|
379
|
+
del versions[:excess]
|
|
380
|
+
current = self._current_version.get(thread_id)
|
|
381
|
+
if current is not None:
|
|
382
|
+
self._current_version[thread_id] = max(current - excess, 0)
|
|
383
|
+
logger.info(
|
|
384
|
+
"Evicted %d oldest checkpoint versions for thread %r (limit: %d)",
|
|
385
|
+
excess,
|
|
386
|
+
thread_id,
|
|
387
|
+
self._max_versions_per_thread,
|
|
388
|
+
)
|
|
389
|
+
return excess
|
|
390
|
+
|
|
391
|
+
def _restore_current_pointer(self, thread_id: str, previous: CheckpointVersion | None) -> None:
|
|
392
|
+
"""按版本身份恢复保存前的当前指针(容量淘汰会改变索引)。"""
|
|
393
|
+
versions = self._versions.get(thread_id, [])
|
|
394
|
+
if previous is None:
|
|
395
|
+
self._current_version.pop(thread_id, None)
|
|
396
|
+
return
|
|
397
|
+
position = next((i for i, item in enumerate(versions) if item is previous), None)
|
|
398
|
+
if position is not None:
|
|
399
|
+
self._current_version[thread_id] = position
|
|
400
|
+
elif versions:
|
|
401
|
+
# 保存前的指针版本已被容量淘汰:指针前移到最旧存活版本。
|
|
402
|
+
self._current_version[thread_id] = 0
|
|
403
|
+
else:
|
|
404
|
+
self._current_version.pop(thread_id, None)
|
|
405
|
+
|
|
406
|
+
async def _persist_versions(self, thread_id: str) -> None:
|
|
407
|
+
"""持久化版本列表到文件。"""
|
|
408
|
+
if not self._storage_dir:
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
file_path = self._get_version_file(thread_id)
|
|
412
|
+
if not file_path:
|
|
413
|
+
return
|
|
414
|
+
|
|
415
|
+
versions = self._versions.get(thread_id, [])
|
|
416
|
+
data = {
|
|
417
|
+
"thread_id": thread_id,
|
|
418
|
+
"versions": [v.to_dict() for v in versions],
|
|
419
|
+
"current_version": self._current_version.get(thread_id, 0),
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
_atomic_write_json(file_path, data)
|
|
424
|
+
except Exception as exc:
|
|
425
|
+
raise CheckpointError(f"持久化会话 {thread_id!r} 的 checkpoint 版本失败:{exc}") from exc
|
|
426
|
+
|
|
427
|
+
async def _load_versions_from_file(self, thread_id: str) -> None:
|
|
428
|
+
"""从文件加载版本列表。"""
|
|
429
|
+
if thread_id in self._versions:
|
|
430
|
+
return # 已加载
|
|
431
|
+
|
|
432
|
+
if not self._storage_dir:
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
file_path = self._get_version_file(thread_id)
|
|
436
|
+
if not file_path or not file_path.exists():
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
with file_path.open("r", encoding="utf-8") as f:
|
|
441
|
+
data = json.load(f)
|
|
442
|
+
|
|
443
|
+
self._versions[thread_id] = [CheckpointVersion.from_dict(v) for v in data.get("versions", [])]
|
|
444
|
+
self._current_version[thread_id] = data.get("current_version", 0)
|
|
445
|
+
# 历史文件中的版本数可能超过当前上限:加载后同样收敛,指针随淘汰前移。
|
|
446
|
+
self._trim_versions(thread_id)
|
|
447
|
+
except Exception as exc:
|
|
448
|
+
raise CheckpointError(f"加载会话 {thread_id!r} 的 checkpoint 版本失败:{exc}") from exc
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""通用上下文压缩能力。"""
|
|
2
|
+
|
|
3
|
+
from agent_core.compaction.middleware import (
|
|
4
|
+
OVERFLOW_RETRY_KEY,
|
|
5
|
+
CompactionMiddleware,
|
|
6
|
+
is_context_overflow_error,
|
|
7
|
+
)
|
|
8
|
+
from agent_core.compaction.prompts import DEFAULT_COMPACTION_INSTRUCTION, DEFAULT_SUMMARY_SECTIONS
|
|
9
|
+
from agent_core.compaction.pruner import PRUNE_MARKER, PruneConfig, prune_text, prune_tool_results
|
|
10
|
+
from agent_core.compaction.region import (
|
|
11
|
+
balanced_cut,
|
|
12
|
+
build_checkpoint,
|
|
13
|
+
estimate_message_tokens,
|
|
14
|
+
estimate_tokens,
|
|
15
|
+
extract_previous_summary,
|
|
16
|
+
rebuild,
|
|
17
|
+
select_cut,
|
|
18
|
+
)
|
|
19
|
+
from agent_core.compaction.spill import SpillStore
|
|
20
|
+
from agent_core.compaction.spill_tool import build_spill_tool
|
|
21
|
+
from agent_core.compaction.token_limit import TokenLimitMiddleware
|
|
22
|
+
from agent_core.compaction.types import (
|
|
23
|
+
SUMMARY_CLOSE,
|
|
24
|
+
SUMMARY_OPEN,
|
|
25
|
+
SUMMARY_SOURCE,
|
|
26
|
+
CompactionConfig,
|
|
27
|
+
CompactionError,
|
|
28
|
+
CompactionResult,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"DEFAULT_COMPACTION_INSTRUCTION",
|
|
33
|
+
"DEFAULT_SUMMARY_SECTIONS",
|
|
34
|
+
"OVERFLOW_RETRY_KEY",
|
|
35
|
+
"PRUNE_MARKER",
|
|
36
|
+
"SUMMARY_CLOSE",
|
|
37
|
+
"SUMMARY_OPEN",
|
|
38
|
+
"SUMMARY_SOURCE",
|
|
39
|
+
"CompactionConfig",
|
|
40
|
+
"CompactionError",
|
|
41
|
+
"CompactionMiddleware",
|
|
42
|
+
"CompactionResult",
|
|
43
|
+
"PruneConfig",
|
|
44
|
+
"SpillStore",
|
|
45
|
+
"TokenLimitMiddleware",
|
|
46
|
+
"balanced_cut",
|
|
47
|
+
"build_checkpoint",
|
|
48
|
+
"build_spill_tool",
|
|
49
|
+
"estimate_message_tokens",
|
|
50
|
+
"estimate_tokens",
|
|
51
|
+
"extract_previous_summary",
|
|
52
|
+
"is_context_overflow_error",
|
|
53
|
+
"prune_text",
|
|
54
|
+
"prune_tool_results",
|
|
55
|
+
"rebuild",
|
|
56
|
+
"select_cut",
|
|
57
|
+
]
|