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,427 @@
|
|
|
1
|
+
"""手写状态机调度器。
|
|
2
|
+
|
|
3
|
+
核心概念:
|
|
4
|
+
- State: 字典,存储所有运行时状态
|
|
5
|
+
- Node: 可调用函数,接受 state,返回更新字典
|
|
6
|
+
- Edge: 节点间的转移规则(固定边 / 条件边)
|
|
7
|
+
- Execution: 从 START 开始,按边的规则依次执行节点,直到 END
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import logging
|
|
14
|
+
from collections.abc import Awaitable, Callable
|
|
15
|
+
from contextvars import ContextVar
|
|
16
|
+
from copy import deepcopy
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from inspect import isawaitable
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from agent_core.errors import StateMachineError
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# 特殊节点名称
|
|
26
|
+
START = "__start__"
|
|
27
|
+
END = "__end__"
|
|
28
|
+
|
|
29
|
+
# 节点函数签名
|
|
30
|
+
NodeFunc = Callable[[dict[str, Any]], dict[str, Any] | Awaitable[dict[str, Any]]]
|
|
31
|
+
# 条件路由函数签名:返回下一个节点名称
|
|
32
|
+
ConditionalRouteFunc = Callable[[dict[str, Any]], str | Awaitable[str]]
|
|
33
|
+
StepCallback = Callable[
|
|
34
|
+
[str, str, dict[str, Any], int],
|
|
35
|
+
Awaitable[None] | None,
|
|
36
|
+
]
|
|
37
|
+
NodeStartCallback = Callable[["NodeExecutionContext", dict[str, Any]], Awaitable[None] | None]
|
|
38
|
+
IdempotencyKeyFactory = Callable[[str, int], str]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class NodeExecutionPolicy:
|
|
43
|
+
"""单个工作流节点的超时和重试策略。"""
|
|
44
|
+
|
|
45
|
+
timeout_seconds: float | None = None
|
|
46
|
+
max_attempts: int = 1
|
|
47
|
+
backoff_initial_seconds: float = 0.0
|
|
48
|
+
backoff_multiplier: float = 2.0
|
|
49
|
+
backoff_max_seconds: float = 30.0
|
|
50
|
+
retry_exceptions: tuple[type[Exception], ...] = (Exception,)
|
|
51
|
+
idempotent: bool = False
|
|
52
|
+
|
|
53
|
+
def __post_init__(self) -> None:
|
|
54
|
+
if self.timeout_seconds is not None and self.timeout_seconds <= 0:
|
|
55
|
+
raise ValueError("timeout_seconds 必须大于 0")
|
|
56
|
+
if self.max_attempts <= 0:
|
|
57
|
+
raise ValueError("max_attempts 必须大于 0")
|
|
58
|
+
if self.max_attempts > 1 and not self.idempotent:
|
|
59
|
+
raise ValueError("开启节点重试前必须声明 idempotent=True")
|
|
60
|
+
if self.backoff_initial_seconds < 0 or self.backoff_max_seconds < 0:
|
|
61
|
+
raise ValueError("退避时间不能小于 0")
|
|
62
|
+
if self.backoff_multiplier < 1:
|
|
63
|
+
raise ValueError("backoff_multiplier 不能小于 1")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class NodeExecutionContext:
|
|
68
|
+
"""当前节点调用可读取的执行上下文。"""
|
|
69
|
+
|
|
70
|
+
node_name: str
|
|
71
|
+
attempt: int
|
|
72
|
+
idempotency_key: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_CURRENT_NODE_EXECUTION: ContextVar[NodeExecutionContext | None] = ContextVar(
|
|
76
|
+
"agent_core_node_execution",
|
|
77
|
+
default=None,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def current_node_execution() -> NodeExecutionContext | None:
|
|
82
|
+
"""返回当前节点执行上下文,供外部副作用传递幂等键。"""
|
|
83
|
+
|
|
84
|
+
return _CURRENT_NODE_EXECUTION.get()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class WorkflowPause(Exception):
|
|
88
|
+
"""节点主动暂停工作流,等待外部条件满足后恢复。"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, reason: str, data: dict[str, Any] | None = None) -> None:
|
|
91
|
+
super().__init__(reason)
|
|
92
|
+
self.reason = reason
|
|
93
|
+
self.data = dict(data or {})
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class StateMachine:
|
|
97
|
+
"""手写状态机调度器。
|
|
98
|
+
|
|
99
|
+
功能:
|
|
100
|
+
1. 注册节点(node)和边(edge)
|
|
101
|
+
2. 从 START 开始执行,按照边的规则转移
|
|
102
|
+
3. 每执行一个节点,将返回值合并到 state
|
|
103
|
+
4. 条件边根据当前 state 动态决定下一个节点
|
|
104
|
+
5. 到达 END 或超过最大步数时停止
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, *, max_steps: int = 50) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Args:
|
|
110
|
+
max_steps: 最大执行步数,防止死循环。
|
|
111
|
+
"""
|
|
112
|
+
self._nodes: dict[str, NodeFunc] = {}
|
|
113
|
+
self._node_policies: dict[str, NodeExecutionPolicy] = {}
|
|
114
|
+
# 固定边:node_name -> next_node_name
|
|
115
|
+
self._edges: dict[str, str] = {}
|
|
116
|
+
# 条件边:node_name -> route_func
|
|
117
|
+
self._conditional_edges: dict[str, ConditionalRouteFunc] = {}
|
|
118
|
+
self._conditional_targets: dict[str, tuple[str, ...]] = {}
|
|
119
|
+
self._max_steps = max_steps
|
|
120
|
+
|
|
121
|
+
def add_node(
|
|
122
|
+
self,
|
|
123
|
+
name: str,
|
|
124
|
+
func: NodeFunc,
|
|
125
|
+
*,
|
|
126
|
+
policy: NodeExecutionPolicy | None = None,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""注册一个节点。
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
name: 节点名称(全局唯一)。
|
|
132
|
+
func: 节点函数,接受 state,返回更新字典。
|
|
133
|
+
"""
|
|
134
|
+
if name in self._nodes:
|
|
135
|
+
raise StateMachineError(f"Node {name!r} already registered.")
|
|
136
|
+
if name in (START, END):
|
|
137
|
+
raise StateMachineError(f"Cannot register reserved node name {name!r}.")
|
|
138
|
+
self._nodes[name] = func
|
|
139
|
+
self._node_policies[name] = policy or NodeExecutionPolicy()
|
|
140
|
+
|
|
141
|
+
def add_edge(self, from_node: str, to_node: str) -> None:
|
|
142
|
+
"""注册一条固定边:from_node 执行完毕后自动转移到 to_node。
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
from_node: 源节点名称。
|
|
146
|
+
to_node: 目标节点名称。
|
|
147
|
+
"""
|
|
148
|
+
if from_node in self._conditional_edges:
|
|
149
|
+
raise StateMachineError(f"Node {from_node!r} already has a conditional edge.")
|
|
150
|
+
self._edges[from_node] = to_node
|
|
151
|
+
|
|
152
|
+
def add_conditional_edges(
|
|
153
|
+
self,
|
|
154
|
+
from_node: str,
|
|
155
|
+
route_func: ConditionalRouteFunc,
|
|
156
|
+
*,
|
|
157
|
+
possible_targets: list[str] | tuple[str, ...] | None = None,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""注册一条条件边:from_node 执行完毕后,调用 route_func(state) 决定下一个节点。
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
from_node: 源节点名称。
|
|
163
|
+
route_func: 路由函数,接受 state,返回下一个节点名称。
|
|
164
|
+
"""
|
|
165
|
+
if from_node in self._edges:
|
|
166
|
+
raise StateMachineError(f"Node {from_node!r} already has a fixed edge.")
|
|
167
|
+
self._conditional_edges[from_node] = route_func
|
|
168
|
+
self._conditional_targets[from_node] = tuple(possible_targets or ())
|
|
169
|
+
|
|
170
|
+
async def ainvoke(self, initial_state: dict[str, Any]) -> dict[str, Any]:
|
|
171
|
+
"""异步执行状态机。
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
initial_state: 初始状态字典。
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
执行完毕后的最终状态。
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
StateMachineError: 节点不存在、超过最大步数等错误。
|
|
181
|
+
"""
|
|
182
|
+
return await self.ainvoke_from(initial_state, start_node=START)
|
|
183
|
+
|
|
184
|
+
async def ainvoke_from(
|
|
185
|
+
self,
|
|
186
|
+
initial_state: dict[str, Any],
|
|
187
|
+
*,
|
|
188
|
+
start_node: str = START,
|
|
189
|
+
on_step: StepCallback | None = None,
|
|
190
|
+
on_node_start: NodeStartCallback | None = None,
|
|
191
|
+
idempotency_key_factory: IdempotencyKeyFactory | None = None,
|
|
192
|
+
) -> dict[str, Any]:
|
|
193
|
+
"""从指定节点执行,并在每个节点完成后发布可持久化快照。"""
|
|
194
|
+
if start_node != START and start_node != END and start_node not in self._nodes:
|
|
195
|
+
raise StateMachineError(f"Node {start_node!r} not found.")
|
|
196
|
+
state = deepcopy(initial_state)
|
|
197
|
+
current_node = start_node
|
|
198
|
+
steps = 0
|
|
199
|
+
completed_nodes = 0
|
|
200
|
+
|
|
201
|
+
logger.info("StateMachine: starting execution from %s", current_node)
|
|
202
|
+
|
|
203
|
+
while current_node != END:
|
|
204
|
+
steps += 1
|
|
205
|
+
if steps > self._max_steps:
|
|
206
|
+
raise StateMachineError(
|
|
207
|
+
f"StateMachine exceeded max steps ({self._max_steps}). Last node: {current_node!r}."
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# 执行当前节点(START 和 END 是虚拟节点,不执行)
|
|
211
|
+
if current_node != START:
|
|
212
|
+
node_func = self._nodes.get(current_node)
|
|
213
|
+
if node_func is None:
|
|
214
|
+
raise StateMachineError(f"Node {current_node!r} not found.")
|
|
215
|
+
|
|
216
|
+
logger.debug("StateMachine: executing node %r (step %d)", current_node, steps)
|
|
217
|
+
try:
|
|
218
|
+
idempotency_key = (
|
|
219
|
+
idempotency_key_factory(current_node, completed_nodes)
|
|
220
|
+
if idempotency_key_factory is not None
|
|
221
|
+
else f"{current_node}:{completed_nodes + 1}"
|
|
222
|
+
)
|
|
223
|
+
updates = await self._execute_node(
|
|
224
|
+
current_node,
|
|
225
|
+
node_func,
|
|
226
|
+
state,
|
|
227
|
+
idempotency_key=idempotency_key,
|
|
228
|
+
on_node_start=on_node_start,
|
|
229
|
+
)
|
|
230
|
+
if updates:
|
|
231
|
+
state.update(updates)
|
|
232
|
+
completed_nodes += 1
|
|
233
|
+
except WorkflowPause:
|
|
234
|
+
raise
|
|
235
|
+
except Exception as exc:
|
|
236
|
+
raise StateMachineError(f"Node {current_node!r} raised exception: {exc}") from exc
|
|
237
|
+
|
|
238
|
+
# 决定下一个节点
|
|
239
|
+
next_node = await self._get_next_node(current_node, state)
|
|
240
|
+
if on_step is not None:
|
|
241
|
+
checkpoint = on_step(current_node, next_node, deepcopy(state), completed_nodes)
|
|
242
|
+
if isawaitable(checkpoint):
|
|
243
|
+
await checkpoint
|
|
244
|
+
logger.debug("StateMachine: %r -> %r", current_node, next_node)
|
|
245
|
+
current_node = next_node
|
|
246
|
+
|
|
247
|
+
logger.info("StateMachine: execution completed in %d steps", steps)
|
|
248
|
+
return state
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def node_count(self) -> int:
|
|
252
|
+
"""返回已注册的实际节点数量。"""
|
|
253
|
+
return len(self._nodes)
|
|
254
|
+
|
|
255
|
+
async def _execute_node(
|
|
256
|
+
self,
|
|
257
|
+
node_name: str,
|
|
258
|
+
node_func: NodeFunc,
|
|
259
|
+
state: dict[str, Any],
|
|
260
|
+
*,
|
|
261
|
+
idempotency_key: str,
|
|
262
|
+
on_node_start: NodeStartCallback | None,
|
|
263
|
+
) -> dict[str, Any]:
|
|
264
|
+
policy = self._node_policies[node_name]
|
|
265
|
+
delay = policy.backoff_initial_seconds
|
|
266
|
+
for attempt in range(1, policy.max_attempts + 1):
|
|
267
|
+
context = NodeExecutionContext(
|
|
268
|
+
node_name=node_name,
|
|
269
|
+
attempt=attempt,
|
|
270
|
+
idempotency_key=idempotency_key,
|
|
271
|
+
)
|
|
272
|
+
if on_node_start is not None:
|
|
273
|
+
pending = on_node_start(context, deepcopy(state))
|
|
274
|
+
if isawaitable(pending):
|
|
275
|
+
await pending
|
|
276
|
+
token = _CURRENT_NODE_EXECUTION.set(context)
|
|
277
|
+
try:
|
|
278
|
+
result = node_func(state)
|
|
279
|
+
if isawaitable(result):
|
|
280
|
+
if policy.timeout_seconds is None:
|
|
281
|
+
result = await result
|
|
282
|
+
else:
|
|
283
|
+
result = await asyncio.wait_for(result, timeout=policy.timeout_seconds)
|
|
284
|
+
return result
|
|
285
|
+
except WorkflowPause:
|
|
286
|
+
raise
|
|
287
|
+
except policy.retry_exceptions:
|
|
288
|
+
if attempt >= policy.max_attempts:
|
|
289
|
+
raise
|
|
290
|
+
if delay > 0:
|
|
291
|
+
await asyncio.sleep(delay)
|
|
292
|
+
delay = min(
|
|
293
|
+
policy.backoff_max_seconds,
|
|
294
|
+
max(delay, 0.001) * policy.backoff_multiplier,
|
|
295
|
+
)
|
|
296
|
+
finally:
|
|
297
|
+
_CURRENT_NODE_EXECUTION.reset(token)
|
|
298
|
+
raise StateMachineError(f"Node {node_name!r} exhausted retry policy.")
|
|
299
|
+
|
|
300
|
+
def describe(self) -> dict[str, Any]:
|
|
301
|
+
"""返回可序列化的状态机结构,用于调试和可视化。"""
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
"nodes": [
|
|
305
|
+
{
|
|
306
|
+
"name": name,
|
|
307
|
+
"policy": {
|
|
308
|
+
"timeout_seconds": policy.timeout_seconds,
|
|
309
|
+
"max_attempts": policy.max_attempts,
|
|
310
|
+
"idempotent": policy.idempotent,
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
for name, policy in self._node_policies.items()
|
|
314
|
+
],
|
|
315
|
+
"edges": [
|
|
316
|
+
{"from": source, "to": target, "kind": "fixed"} for source, target in self._edges.items()
|
|
317
|
+
],
|
|
318
|
+
"conditional_edges": [
|
|
319
|
+
{
|
|
320
|
+
"from": source,
|
|
321
|
+
"targets": list(self._conditional_targets.get(source, ())),
|
|
322
|
+
"kind": "conditional",
|
|
323
|
+
}
|
|
324
|
+
for source in self._conditional_edges
|
|
325
|
+
],
|
|
326
|
+
"max_steps": self._max_steps,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
def to_mermaid(self, *, direction: str = "TD") -> str:
|
|
330
|
+
"""把状态机导出为 Mermaid flowchart。"""
|
|
331
|
+
|
|
332
|
+
normalized_direction = direction.upper()
|
|
333
|
+
if normalized_direction not in {"TD", "TB", "BT", "LR", "RL"}:
|
|
334
|
+
raise ValueError(f"不支持的 Mermaid 方向:{direction}")
|
|
335
|
+
names = [START, *self._nodes, END]
|
|
336
|
+
node_ids = {name: f"N{index}" for index, name in enumerate(names)}
|
|
337
|
+
|
|
338
|
+
def label(value: str) -> str:
|
|
339
|
+
return value.replace("\\", "\\\\").replace('"', """)
|
|
340
|
+
|
|
341
|
+
lines = [f"flowchart {normalized_direction}"]
|
|
342
|
+
lines.append(f' {node_ids[START]}(["START"])')
|
|
343
|
+
for name in self._nodes:
|
|
344
|
+
lines.append(f' {node_ids[name]}["{label(name)}"]')
|
|
345
|
+
lines.append(f' {node_ids[END]}(["END"])')
|
|
346
|
+
for source, target in self._edges.items():
|
|
347
|
+
if source in node_ids and target in node_ids:
|
|
348
|
+
lines.append(f" {node_ids[source]} --> {node_ids[target]}")
|
|
349
|
+
for index, source in enumerate(self._conditional_edges):
|
|
350
|
+
targets = self._conditional_targets.get(source, ())
|
|
351
|
+
if targets:
|
|
352
|
+
for target in targets:
|
|
353
|
+
if target in node_ids:
|
|
354
|
+
lines.append(f' {node_ids[source]} -. "条件" .-> {node_ids[target]}')
|
|
355
|
+
else:
|
|
356
|
+
dynamic_id = f"D{index}"
|
|
357
|
+
lines.append(f' {dynamic_id}{{"动态路由"}}')
|
|
358
|
+
lines.append(f' {node_ids[source]} -. "条件" .-> {dynamic_id}')
|
|
359
|
+
return "\n".join(lines)
|
|
360
|
+
|
|
361
|
+
async def _get_next_node(self, current_node: str, state: dict[str, Any]) -> str:
|
|
362
|
+
"""根据边的规则决定下一个节点。
|
|
363
|
+
|
|
364
|
+
优先级:
|
|
365
|
+
1. 条件边(如果存在)
|
|
366
|
+
2. 固定边(如果存在)
|
|
367
|
+
3. 默认:END(终止执行)
|
|
368
|
+
"""
|
|
369
|
+
# 条件边
|
|
370
|
+
if current_node in self._conditional_edges:
|
|
371
|
+
route_func = self._conditional_edges[current_node]
|
|
372
|
+
try:
|
|
373
|
+
next_node = route_func(state)
|
|
374
|
+
return await next_node if isawaitable(next_node) else next_node
|
|
375
|
+
except Exception as exc:
|
|
376
|
+
raise StateMachineError(
|
|
377
|
+
f"Conditional route from {current_node!r} raised exception: {exc}"
|
|
378
|
+
) from exc
|
|
379
|
+
|
|
380
|
+
# 固定边
|
|
381
|
+
if current_node in self._edges:
|
|
382
|
+
return self._edges[current_node]
|
|
383
|
+
|
|
384
|
+
# 默认终止
|
|
385
|
+
return END
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class StateMachineBuilder:
|
|
389
|
+
"""状态机构建器(类似 StateGraph 的接口风格)。"""
|
|
390
|
+
|
|
391
|
+
def __init__(self, *, max_steps: int = 50) -> None:
|
|
392
|
+
self._machine = StateMachine(max_steps=max_steps)
|
|
393
|
+
|
|
394
|
+
def add_node(
|
|
395
|
+
self,
|
|
396
|
+
name: str,
|
|
397
|
+
func: NodeFunc,
|
|
398
|
+
*,
|
|
399
|
+
policy: NodeExecutionPolicy | None = None,
|
|
400
|
+
) -> StateMachineBuilder:
|
|
401
|
+
"""添加节点(链式调用)。"""
|
|
402
|
+
self._machine.add_node(name, func, policy=policy)
|
|
403
|
+
return self
|
|
404
|
+
|
|
405
|
+
def add_edge(self, from_node: str, to_node: str) -> StateMachineBuilder:
|
|
406
|
+
"""添加固定边(链式调用)。"""
|
|
407
|
+
self._machine.add_edge(from_node, to_node)
|
|
408
|
+
return self
|
|
409
|
+
|
|
410
|
+
def add_conditional_edges(
|
|
411
|
+
self,
|
|
412
|
+
from_node: str,
|
|
413
|
+
route_func: ConditionalRouteFunc,
|
|
414
|
+
*,
|
|
415
|
+
possible_targets: list[str] | tuple[str, ...] | None = None,
|
|
416
|
+
) -> StateMachineBuilder:
|
|
417
|
+
"""添加条件边(链式调用)。"""
|
|
418
|
+
self._machine.add_conditional_edges(
|
|
419
|
+
from_node,
|
|
420
|
+
route_func,
|
|
421
|
+
possible_targets=possible_targets,
|
|
422
|
+
)
|
|
423
|
+
return self
|
|
424
|
+
|
|
425
|
+
def build(self) -> StateMachine:
|
|
426
|
+
"""构建并返回状态机实例。"""
|
|
427
|
+
return self._machine
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: zero-agent-core
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: 零外部平台绑定的 Python Agent 运行时内核
|
|
5
|
+
Project-URL: Homepage, https://github.com/qiuzixu/agent-core
|
|
6
|
+
Project-URL: Documentation, https://github.com/qiuzixu/agent-core/tree/main/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/qiuzixu/agent-core
|
|
8
|
+
Project-URL: Issues, https://github.com/qiuzixu/agent-core/issues
|
|
9
|
+
Author: qiuzixu
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: acp,agent,function-calling,llm,mcp,react
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: <3.14,>=3.13
|
|
20
|
+
Provides-Extra: anthropic
|
|
21
|
+
Requires-Dist: anthropic<1,>=0.40; extra == 'anthropic'
|
|
22
|
+
Provides-Extra: chroma
|
|
23
|
+
Requires-Dist: chromadb<2,>=1.0; extra == 'chroma'
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mkdocs-material<10,>=9.7; extra == 'dev'
|
|
26
|
+
Requires-Dist: mypy<2,>=1.18; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio<2,>=1.3; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest<10,>=9; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff<1,>=0.14; extra == 'dev'
|
|
30
|
+
Provides-Extra: gemini
|
|
31
|
+
Requires-Dist: google-genai<2,>=1.0; extra == 'gemini'
|
|
32
|
+
Provides-Extra: mcp
|
|
33
|
+
Requires-Dist: mcp<2,>=1.27; extra == 'mcp'
|
|
34
|
+
Provides-Extra: models
|
|
35
|
+
Requires-Dist: anthropic<1,>=0.40; extra == 'models'
|
|
36
|
+
Requires-Dist: dashscope<2,>=1.27; extra == 'models'
|
|
37
|
+
Requires-Dist: google-genai<2,>=1.0; extra == 'models'
|
|
38
|
+
Requires-Dist: httpx<1,>=0.28; extra == 'models'
|
|
39
|
+
Requires-Dist: openai<3,>=1.0; extra == 'models'
|
|
40
|
+
Provides-Extra: openai
|
|
41
|
+
Requires-Dist: httpx<1,>=0.28; extra == 'openai'
|
|
42
|
+
Requires-Dist: openai<3,>=1.0; extra == 'openai'
|
|
43
|
+
Provides-Extra: pgvector
|
|
44
|
+
Requires-Dist: asyncpg<1,>=0.30; extra == 'pgvector'
|
|
45
|
+
Provides-Extra: production
|
|
46
|
+
Requires-Dist: asyncpg<1,>=0.30; extra == 'production'
|
|
47
|
+
Provides-Extra: qwen
|
|
48
|
+
Requires-Dist: dashscope<2,>=1.27; extra == 'qwen'
|
|
49
|
+
Description-Content-Type: text/markdown
|
|
50
|
+
|
|
51
|
+
# Handwritten Agent Core
|
|
52
|
+
|
|
53
|
+
<!-- PyPI 徽章(发布到 PyPI 后取消此行注释即生效):
|
|
54
|
+
[](https://pypi.org/project/zero-agent-core/)
|
|
55
|
+
[](https://pypi.org/project/zero-agent-core/)
|
|
56
|
+
-->
|
|
57
|
+
[](https://github.com/qiuzixu/agent-core/actions/workflows/ci.yml)
|
|
58
|
+
[](LICENSE)
|
|
59
|
+
[](https://www.python.org/)
|
|
60
|
+
|
|
61
|
+
一个零外部平台绑定的 Python Agent 运行时内核。项目从头实现 Agent Loop、
|
|
62
|
+
工具调用、MCP、Skill、上下文压缩、持久化恢复、工作流、HITL、多模型适配和 ACP 协议。
|
|
63
|
+
|
|
64
|
+
> 当前版本为 `0.2.0` Alpha,要求 Python 3.13。公开 API 在 `0.x` 阶段仍可能调整。
|
|
65
|
+
|
|
66
|
+
## 主要能力
|
|
67
|
+
|
|
68
|
+
- 普通和流式 ReAct Agent Loop,本轮多个工具并发执行;
|
|
69
|
+
- Function Calling、MCP stdio 客户端和声明式 Skill;
|
|
70
|
+
- OpenAI、Anthropic、Gemini、Ollama,以及自定义模型 Provider;
|
|
71
|
+
- JSON Schema 结构化输出,以及模型限流、并发、超时、熔断和 fallback;
|
|
72
|
+
- Middleware、Guardrails、HITL、调用统计和可选 OpenTelemetry;
|
|
73
|
+
- 生命周期 Callback、统一 RunEvent 和流式增量事件;
|
|
74
|
+
- Document/Blob、文本加载与切分、Embedding/Retriever/VectorStore 协议;
|
|
75
|
+
- 可选 `zero-agent-core-mineru` 扩展包解析 PDF、扫描件和复杂版面;
|
|
76
|
+
- 可选 `zero-agent-core-embeddings` 扩展包接入 OpenAI、Gemini 和 Ollama;
|
|
77
|
+
- 可选 `zero-agent-core-multi-agent` 扩展包编排多个独立 Agent;
|
|
78
|
+
- 内存、Chroma、pgvector 向量存储,关键词检索以及用户/租户/namespace 隔离;
|
|
79
|
+
- 带类型白名单、Schema 版本和迁移函数的安全 JSON 序列化;
|
|
80
|
+
- 安全文本/聊天 Prompt 模板、消息占位符、partial 和版本管理;
|
|
81
|
+
- 基于模型上下文用量的摘要压缩、工具结果瘦身和 spill;
|
|
82
|
+
- Run、事件、幂等键、Worker 租约、心跳、重启恢复和取消;
|
|
83
|
+
- Checkpoint 历史、回滚、节点重试/幂等/pending write 和 Mermaid 状态机导出;
|
|
84
|
+
- 会话、长期上下文、审批、模型选择的内存、SQLite、PostgreSQL 存储;
|
|
85
|
+
- 跨会话长期记忆、TTL、版本冲突、关键词检索和访问隔离;
|
|
86
|
+
- 用户、租户和管理员访问上下文;
|
|
87
|
+
- ACP JSON-RPC/stdio 服务端适配。
|
|
88
|
+
|
|
89
|
+
完整边界见[已实现能力](docs/CAPABILITIES.md),模块和应用调用关系见
|
|
90
|
+
[系统架构](docs/ARCHITECTURE.md)。
|
|
91
|
+
|
|
92
|
+
## 快速开始
|
|
93
|
+
|
|
94
|
+
从 PyPI 安装(导入名是 `agent_core`,不受发行名更名影响):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install zero-agent-core
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
推荐使用 [uv](https://docs.astral.sh/uv/)(与仓库其它项目一致,自动创建 `.venv`):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
cd agent-core
|
|
104
|
+
uv sync --all-extras
|
|
105
|
+
uv run python examples/starter/starter_agent.py # 五分钟上手:单文件最小完整示例
|
|
106
|
+
uv run python examples/basic_agent.py # 更小的最小示例
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`uv sync` 默认安装项目本体与 `dev` 依赖组;按需追加 extra:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
uv sync --extra openai # 单个 extra
|
|
113
|
+
uv sync --extra models,mcp,production
|
|
114
|
+
uv sync --all-extras # 全部可选依赖
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
不用 uv 时也可以用标准 venv + pip:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
cd agent-core
|
|
121
|
+
python -m venv .venv
|
|
122
|
+
python -m pip install -e ".[dev]"
|
|
123
|
+
python examples/starter/starter_agent.py
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`examples/starter/starter_agent.py` 与 `examples/basic_agent.py` 都使用离线演示模型,
|
|
127
|
+
不需要 API Key 或外部服务。五分钟上手路径(安装 → 运行 → 换真实模型 → 加工具 →
|
|
128
|
+
加工作流)见 [examples/starter/README.md](examples/starter/README.md)。
|
|
129
|
+
|
|
130
|
+
真实模型按需安装(uv 用户把 `pip install` 换成 `uv pip install`,或用上面的
|
|
131
|
+
`uv sync --extra ...`):
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pip install "zero-agent-core[openai]"
|
|
135
|
+
pip install "zero-agent-core[anthropic]"
|
|
136
|
+
pip install "zero-agent-core[gemini]"
|
|
137
|
+
pip install "zero-agent-core[models,mcp,production]"
|
|
138
|
+
pip install "zero-agent-core[chroma]" # 本地持久化向量存储
|
|
139
|
+
pip install "zero-agent-core[pgvector]" # PostgreSQL 向量存储
|
|
140
|
+
pip install zero-agent-core-mineru # 独立 MinerU 文档解析扩展包
|
|
141
|
+
pip install "zero-agent-core-embeddings[openai]"
|
|
142
|
+
pip install "zero-agent-core-embeddings[gemini]"
|
|
143
|
+
pip install "zero-agent-core-embeddings[ollama]"
|
|
144
|
+
pip install zero-agent-core-multi-agent # 独立多 Agent 编排扩展包
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
最小组装方式:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from agent_core import ReActAgent, ToolExecutor, ToolRegistry, create_model_provider
|
|
151
|
+
|
|
152
|
+
model = create_model_provider(settings)
|
|
153
|
+
|
|
154
|
+
tools = ToolRegistry()
|
|
155
|
+
tools.register("lookup", lookup, "查询数据")
|
|
156
|
+
|
|
157
|
+
agent = ReActAgent(
|
|
158
|
+
llm=model,
|
|
159
|
+
tool_executor=ToolExecutor(tools),
|
|
160
|
+
system_prompt="你是一个助手。",
|
|
161
|
+
tool_definitions=tools.build_tool_definitions(),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
answer = await agent.run("查询数据")
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
应用可以通过路径依赖使用尚未发布的 Core。**uv 0.11 起 path 源默认非 editable**,
|
|
168
|
+
不加 `editable = true` 时 site-packages 里是同步时刻的快照,Core 源码更新后应用
|
|
169
|
+
会用到旧代码(症状是新符号 ImportError):
|
|
170
|
+
|
|
171
|
+
```toml
|
|
172
|
+
[project]
|
|
173
|
+
dependencies = ["zero-agent-core"]
|
|
174
|
+
|
|
175
|
+
[tool.uv.sources]
|
|
176
|
+
zero-agent-core = { path = "../agent-core", editable = true }
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
随后在应用目录执行 `uv sync`(多包仓库在 workspace 根执行
|
|
180
|
+
`uv sync --all-packages --all-extras`)完成安装。
|
|
181
|
+
|
|
182
|
+
## 文档站
|
|
183
|
+
|
|
184
|
+
文档源文件位于 `docs/`,使用 Material for MkDocs:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
python -m pip install mkdocs-material
|
|
188
|
+
python -m mkdocs serve # 本地预览 http://127.0.0.1:8000
|
|
189
|
+
python -m mkdocs build # 构建到 site/
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
子路径部署时设置 `DOCS_BASE`,例如 `/zero-agent-core/`。文档入口为
|
|
193
|
+
[docs/index.md](docs/index.md),包含快速开始、核心概念、模型、工具、MCP、Skill、存储、
|
|
194
|
+
工作流、ACP 和公共 API 参考。
|
|
195
|
+
|
|
196
|
+
## 项目边界
|
|
197
|
+
|
|
198
|
+
Core 只提供框架通用能力(Agent Loop、模型适配、工具执行、会话与存储等),
|
|
199
|
+
不包含任何特定业务。应用侧仍负责:
|
|
200
|
+
|
|
201
|
+
- HTTP、WebSocket 或 SSE 等 API 接入;
|
|
202
|
+
- 身份认证、业务授权和密钥读取;
|
|
203
|
+
- 业务提示词、业务工具和业务数据模型;
|
|
204
|
+
- 前端通信与外部系统集成;
|
|
205
|
+
- 部署编排、监控后端和数据库运维。
|
|
206
|
+
|
|
207
|
+
新 Agent 直接依赖 `agent_core`。Core 不感知、也不依赖任何具体业务域。
|
|
208
|
+
|
|
209
|
+
## 开发
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
uv run ruff check src tests examples
|
|
213
|
+
uv run pytest
|
|
214
|
+
uv run mkdocs build
|
|
215
|
+
uv build
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
不使用 uv 时:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
python -m pip install build
|
|
222
|
+
ruff check src tests examples
|
|
223
|
+
pytest
|
|
224
|
+
python -m mkdocs build
|
|
225
|
+
python -m build
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
贡献前请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 和 [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)。
|
|
229
|
+
安全问题按 [SECURITY.md](SECURITY.md) 私密报告。首次发布前的外部配置和决策记录在
|
|
230
|
+
[OPEN_SOURCE_CHECKLIST.md](OPEN_SOURCE_CHECKLIST.md)。
|
|
231
|
+
|
|
232
|
+
## 许可证
|
|
233
|
+
|
|
234
|
+
项目按 [Apache License 2.0](LICENSE) 发布。
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
<!-- 嵌入式 Agent 内核 这个名字备选 -->
|