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,306 @@
|
|
|
1
|
+
"""手写工具执行器。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from agent_core.protocol.runtime import ToolResult
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# 工具函数签名
|
|
16
|
+
ToolFunc = Callable[..., Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ToolSpec:
|
|
21
|
+
"""工具的可执行元数据,和工具函数本身分离。"""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
description: str
|
|
25
|
+
parameters: dict[str, Any] = field(default_factory=dict)
|
|
26
|
+
risk_level: str = "read"
|
|
27
|
+
requires_approval: bool = False
|
|
28
|
+
timeout_seconds: float | None = None
|
|
29
|
+
idempotent: bool = True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ToolRegistry:
|
|
33
|
+
"""工具注册表。
|
|
34
|
+
|
|
35
|
+
管理所有可用的工具函数。
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
self._tools: dict[str, ToolFunc] = {}
|
|
40
|
+
self._specs: dict[str, ToolSpec] = {}
|
|
41
|
+
|
|
42
|
+
def register(
|
|
43
|
+
self,
|
|
44
|
+
name: str,
|
|
45
|
+
func: ToolFunc,
|
|
46
|
+
description: str,
|
|
47
|
+
*,
|
|
48
|
+
parameters: dict[str, Any] | None = None,
|
|
49
|
+
risk_level: str = "read",
|
|
50
|
+
requires_approval: bool = False,
|
|
51
|
+
timeout_seconds: float | None = None,
|
|
52
|
+
idempotent: bool = True,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""注册一个工具。
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
name: 工具名称(全局唯一)。
|
|
58
|
+
func: 工具函数(可以是同步或异步)。
|
|
59
|
+
description: 工具描述(供 LLM 理解用途)。
|
|
60
|
+
"""
|
|
61
|
+
if name in self._tools:
|
|
62
|
+
logger.warning("Tool %r already registered, overwriting", name)
|
|
63
|
+
self._tools[name] = func
|
|
64
|
+
self._specs[name] = ToolSpec(
|
|
65
|
+
name=name,
|
|
66
|
+
description=description,
|
|
67
|
+
parameters=parameters or {"type": "object", "properties": {}, "required": []},
|
|
68
|
+
risk_level=risk_level,
|
|
69
|
+
requires_approval=requires_approval,
|
|
70
|
+
timeout_seconds=timeout_seconds,
|
|
71
|
+
idempotent=idempotent,
|
|
72
|
+
)
|
|
73
|
+
logger.debug("Registered tool: %r", name)
|
|
74
|
+
|
|
75
|
+
def get_tool(self, name: str) -> ToolFunc | None:
|
|
76
|
+
"""获取工具函数。"""
|
|
77
|
+
return self._tools.get(name)
|
|
78
|
+
|
|
79
|
+
def get_all_tools(self) -> dict[str, ToolFunc]:
|
|
80
|
+
"""获取所有工具。"""
|
|
81
|
+
return self._tools.copy()
|
|
82
|
+
|
|
83
|
+
def get_tool_names(self) -> list[str]:
|
|
84
|
+
"""获取所有工具名称。"""
|
|
85
|
+
return list(self._tools.keys())
|
|
86
|
+
|
|
87
|
+
def get_spec(self, name: str) -> ToolSpec | None:
|
|
88
|
+
"""获取工具策略和参数 schema。"""
|
|
89
|
+
return self._specs.get(name)
|
|
90
|
+
|
|
91
|
+
def get_all_specs(self) -> dict[str, ToolSpec]:
|
|
92
|
+
return self._specs.copy()
|
|
93
|
+
|
|
94
|
+
def build_tool_definitions(self) -> list[dict[str, Any]]:
|
|
95
|
+
"""构造 OpenAI function calling 格式的工具定义列表。
|
|
96
|
+
|
|
97
|
+
返回注册时声明的完整参数 schema。
|
|
98
|
+
"""
|
|
99
|
+
definitions = []
|
|
100
|
+
for name, spec in self._specs.items():
|
|
101
|
+
definitions.append(
|
|
102
|
+
{
|
|
103
|
+
"type": "function",
|
|
104
|
+
"function": {
|
|
105
|
+
"name": name,
|
|
106
|
+
"description": spec.description,
|
|
107
|
+
"parameters": spec.parameters,
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
return definitions
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ToolExecutor:
|
|
115
|
+
"""工具执行器。
|
|
116
|
+
|
|
117
|
+
负责根据 tool_call 执行对应的工具函数。
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
registry: ToolRegistry,
|
|
123
|
+
*,
|
|
124
|
+
approval_callback: Callable[[ToolSpec, dict[str, Any]], Awaitable[bool]] | None = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
self._registry = registry
|
|
127
|
+
self._approval_callback = approval_callback
|
|
128
|
+
|
|
129
|
+
async def execute(
|
|
130
|
+
self,
|
|
131
|
+
tool_name: str,
|
|
132
|
+
arguments: dict[str, Any],
|
|
133
|
+
*,
|
|
134
|
+
approval_granted: bool | None = None,
|
|
135
|
+
) -> str:
|
|
136
|
+
"""执行单个工具调用。
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
tool_name: 工具名称。
|
|
140
|
+
arguments: 工具参数。
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
工具执行结果(字符串格式)。
|
|
144
|
+
"""
|
|
145
|
+
result = await self.execute_result(
|
|
146
|
+
tool_name,
|
|
147
|
+
arguments,
|
|
148
|
+
approval_granted=approval_granted,
|
|
149
|
+
)
|
|
150
|
+
return result.to_text()
|
|
151
|
+
|
|
152
|
+
async def execute_result(
|
|
153
|
+
self,
|
|
154
|
+
tool_name: str,
|
|
155
|
+
arguments: dict[str, Any],
|
|
156
|
+
*,
|
|
157
|
+
approval_granted: bool | None = None,
|
|
158
|
+
) -> ToolResult:
|
|
159
|
+
"""执行工具并返回结构化结果。
|
|
160
|
+
|
|
161
|
+
``approval_granted`` 用于已经在 API 或工作流边界完成审批的调用。
|
|
162
|
+
未显式传入时仍使用构造器注入的审批回调,避免普通 Agent Loop 绕过审批。
|
|
163
|
+
"""
|
|
164
|
+
tool_func = self._registry.get_tool(tool_name)
|
|
165
|
+
if tool_func is None:
|
|
166
|
+
error_msg = f"Tool {tool_name!r} not found."
|
|
167
|
+
logger.warning(error_msg)
|
|
168
|
+
return ToolResult(
|
|
169
|
+
tool_name=tool_name,
|
|
170
|
+
success=False,
|
|
171
|
+
error=error_msg,
|
|
172
|
+
error_kind="not_found",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if not isinstance(arguments, dict):
|
|
176
|
+
return ToolResult(
|
|
177
|
+
tool_name=tool_name,
|
|
178
|
+
success=False,
|
|
179
|
+
error="arguments 必须是对象",
|
|
180
|
+
error_kind="invalid_arguments",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
spec = self._registry.get_spec(tool_name)
|
|
184
|
+
if spec is not None:
|
|
185
|
+
schema_error = self._validate_arguments(spec, arguments)
|
|
186
|
+
if schema_error:
|
|
187
|
+
return ToolResult(
|
|
188
|
+
tool_name=tool_name,
|
|
189
|
+
success=False,
|
|
190
|
+
error=schema_error,
|
|
191
|
+
error_kind="invalid_arguments",
|
|
192
|
+
)
|
|
193
|
+
if spec.requires_approval:
|
|
194
|
+
if approval_granted is True:
|
|
195
|
+
pass
|
|
196
|
+
elif approval_granted is False:
|
|
197
|
+
return ToolResult(
|
|
198
|
+
tool_name=tool_name,
|
|
199
|
+
success=False,
|
|
200
|
+
error="审批拒绝",
|
|
201
|
+
error_kind="approval",
|
|
202
|
+
)
|
|
203
|
+
elif self._approval_callback is None:
|
|
204
|
+
return ToolResult(
|
|
205
|
+
tool_name=tool_name,
|
|
206
|
+
success=False,
|
|
207
|
+
error="未配置审批回调",
|
|
208
|
+
error_kind="approval",
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
try:
|
|
212
|
+
approved = await self._approval_callback(spec, arguments)
|
|
213
|
+
except Exception as exc:
|
|
214
|
+
logger.warning("工具 %r 审批回调失败:%s", tool_name, exc, exc_info=True)
|
|
215
|
+
return ToolResult(
|
|
216
|
+
tool_name=tool_name,
|
|
217
|
+
success=False,
|
|
218
|
+
error=f"审批回调失败:{exc}",
|
|
219
|
+
error_kind="approval",
|
|
220
|
+
retryable=True,
|
|
221
|
+
)
|
|
222
|
+
if not approved:
|
|
223
|
+
return ToolResult(
|
|
224
|
+
tool_name=tool_name,
|
|
225
|
+
success=False,
|
|
226
|
+
error="审批拒绝",
|
|
227
|
+
error_kind="approval",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
logger.debug("Executing tool: %r with args: %s", tool_name, arguments)
|
|
232
|
+
|
|
233
|
+
async def invoke() -> Any:
|
|
234
|
+
if asyncio.iscoroutinefunction(tool_func):
|
|
235
|
+
return await tool_func(**arguments)
|
|
236
|
+
# 同步工具放到线程,避免阻塞 Agent Loop。
|
|
237
|
+
return await asyncio.to_thread(tool_func, **arguments)
|
|
238
|
+
|
|
239
|
+
if spec and spec.timeout_seconds:
|
|
240
|
+
result = await asyncio.wait_for(invoke(), spec.timeout_seconds)
|
|
241
|
+
else:
|
|
242
|
+
result = await invoke()
|
|
243
|
+
|
|
244
|
+
return ToolResult(tool_name=tool_name, success=True, value=result)
|
|
245
|
+
|
|
246
|
+
except TimeoutError:
|
|
247
|
+
logger.warning("Tool %r timed out", tool_name)
|
|
248
|
+
return ToolResult(
|
|
249
|
+
tool_name=tool_name,
|
|
250
|
+
success=False,
|
|
251
|
+
error="工具执行超时",
|
|
252
|
+
error_kind="timeout",
|
|
253
|
+
retryable=True,
|
|
254
|
+
)
|
|
255
|
+
except Exception as exc:
|
|
256
|
+
logger.warning("Tool %r raised exception: %s", tool_name, exc, exc_info=True)
|
|
257
|
+
return ToolResult(
|
|
258
|
+
tool_name=tool_name,
|
|
259
|
+
success=False,
|
|
260
|
+
error=str(exc),
|
|
261
|
+
error_kind="execution",
|
|
262
|
+
retryable=False,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def _validate_arguments(spec: ToolSpec, arguments: dict[str, Any]) -> str | None:
|
|
267
|
+
"""执行一层无第三方依赖的基本 schema 校验。"""
|
|
268
|
+
schema = spec.parameters or {}
|
|
269
|
+
required = schema.get("required", [])
|
|
270
|
+
missing = [name for name in required if name not in arguments]
|
|
271
|
+
if missing:
|
|
272
|
+
return f"缺少必填参数:{', '.join(str(item) for item in missing)}"
|
|
273
|
+
properties = schema.get("properties", {})
|
|
274
|
+
for name, value in arguments.items():
|
|
275
|
+
if name not in properties and schema.get("additionalProperties") is False:
|
|
276
|
+
return f"参数 {name} 未在工具 schema 中声明"
|
|
277
|
+
definition = properties.get(name, {})
|
|
278
|
+
expected = definition.get("type")
|
|
279
|
+
if expected == "string" and not isinstance(value, str):
|
|
280
|
+
return f"参数 {name} 必须是字符串"
|
|
281
|
+
if expected == "number" and (not isinstance(value, (int, float)) or isinstance(value, bool)):
|
|
282
|
+
return f"参数 {name} 必须是数字"
|
|
283
|
+
if expected == "integer" and (not isinstance(value, int) or isinstance(value, bool)):
|
|
284
|
+
return f"参数 {name} 必须是整数"
|
|
285
|
+
if expected == "boolean" and not isinstance(value, bool):
|
|
286
|
+
return f"参数 {name} 必须是布尔值"
|
|
287
|
+
if expected == "array" and not isinstance(value, list):
|
|
288
|
+
return f"参数 {name} 必须是数组"
|
|
289
|
+
if expected == "object" and not isinstance(value, dict):
|
|
290
|
+
return f"参数 {name} 必须是对象"
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
async def execute_batch(
|
|
294
|
+
self,
|
|
295
|
+
tool_calls: list[dict[str, Any]],
|
|
296
|
+
) -> list[str]:
|
|
297
|
+
"""并发执行多个工具调用。
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
tool_calls: 工具调用列表,每个包含 name 和 args。
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
工具执行结果列表(与 tool_calls 顺序对应)。
|
|
304
|
+
"""
|
|
305
|
+
tasks = [self.execute(tc["name"], tc.get("args", {})) for tc in tool_calls]
|
|
306
|
+
return await asyncio.gather(*tasks)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""通用状态机和持久化工作流运行器。"""
|
|
2
|
+
|
|
3
|
+
from agent_core.workflow.durable import DurableWorkflowRunner
|
|
4
|
+
from agent_core.workflow.state_machine import (
|
|
5
|
+
END,
|
|
6
|
+
START,
|
|
7
|
+
NodeExecutionContext,
|
|
8
|
+
NodeExecutionPolicy,
|
|
9
|
+
StateMachine,
|
|
10
|
+
StateMachineBuilder,
|
|
11
|
+
WorkflowPause,
|
|
12
|
+
current_node_execution,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"END",
|
|
17
|
+
"START",
|
|
18
|
+
"DurableWorkflowRunner",
|
|
19
|
+
"NodeExecutionContext",
|
|
20
|
+
"NodeExecutionPolicy",
|
|
21
|
+
"StateMachine",
|
|
22
|
+
"StateMachineBuilder",
|
|
23
|
+
"WorkflowPause",
|
|
24
|
+
"current_node_execution",
|
|
25
|
+
]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""状态机与工作流执行存储的持久化运行器。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_core.access import AccessContext, enforce_access
|
|
8
|
+
from agent_core.storage.workflow import WorkflowExecution, WorkflowExecutionStore
|
|
9
|
+
from agent_core.workflow.state_machine import (
|
|
10
|
+
END,
|
|
11
|
+
START,
|
|
12
|
+
NodeExecutionContext,
|
|
13
|
+
StateMachine,
|
|
14
|
+
WorkflowPause,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DurableWorkflowRunner:
|
|
19
|
+
"""在每个节点边界保存状态,并支持进程重启后继续执行。"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
machine: StateMachine,
|
|
24
|
+
store: WorkflowExecutionStore,
|
|
25
|
+
*,
|
|
26
|
+
require_access: bool = False,
|
|
27
|
+
raise_on_failure: bool = True,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._machine = machine
|
|
30
|
+
self._store = store
|
|
31
|
+
self._strict_access = require_access
|
|
32
|
+
self._raise_on_failure = raise_on_failure
|
|
33
|
+
|
|
34
|
+
async def start(
|
|
35
|
+
self,
|
|
36
|
+
definition_id: str,
|
|
37
|
+
initial_state: dict[str, Any],
|
|
38
|
+
*,
|
|
39
|
+
execution_type: str = "workflow",
|
|
40
|
+
access: AccessContext | None = None,
|
|
41
|
+
) -> WorkflowExecution:
|
|
42
|
+
"""创建执行实例并运行到完成、暂停或失败。"""
|
|
43
|
+
if self._strict_access and access is None:
|
|
44
|
+
raise PermissionError("该 DurableWorkflowRunner 要求提供 AccessContext")
|
|
45
|
+
execution = WorkflowExecution(
|
|
46
|
+
definition_id=definition_id,
|
|
47
|
+
execution_type=execution_type,
|
|
48
|
+
user_id=access.user_id if access else None,
|
|
49
|
+
tenant_id=access.tenant_id if access else None,
|
|
50
|
+
status="queued",
|
|
51
|
+
current_step=START,
|
|
52
|
+
total_steps=self._machine.node_count,
|
|
53
|
+
input_data=dict(initial_state),
|
|
54
|
+
result_data=dict(initial_state),
|
|
55
|
+
events=[{"event_type": "workflow_queued", "step": START}],
|
|
56
|
+
)
|
|
57
|
+
await self._store.save(execution)
|
|
58
|
+
return await self._run(execution)
|
|
59
|
+
|
|
60
|
+
async def resume(
|
|
61
|
+
self,
|
|
62
|
+
execution_id: str,
|
|
63
|
+
*,
|
|
64
|
+
access: AccessContext | None = None,
|
|
65
|
+
) -> WorkflowExecution:
|
|
66
|
+
"""从最后保存的下一节点继续执行。"""
|
|
67
|
+
execution = await self._store.load(execution_id)
|
|
68
|
+
if execution is None:
|
|
69
|
+
raise KeyError(f"工作流执行实例不存在:{execution_id}")
|
|
70
|
+
self._check_access(execution, access)
|
|
71
|
+
if execution.status == "completed":
|
|
72
|
+
return execution
|
|
73
|
+
if execution.current_step == END:
|
|
74
|
+
execution.status = "completed"
|
|
75
|
+
await self._store.save(execution)
|
|
76
|
+
return execution
|
|
77
|
+
execution.events.append({"event_type": "workflow_resumed", "step": execution.current_step or START})
|
|
78
|
+
return await self._run(execution)
|
|
79
|
+
|
|
80
|
+
async def _run(self, execution: WorkflowExecution) -> WorkflowExecution:
|
|
81
|
+
execution.status = "running"
|
|
82
|
+
await self._store.save(execution)
|
|
83
|
+
completed_offset = execution.steps_completed
|
|
84
|
+
|
|
85
|
+
def idempotency_key(node_name: str, completed_nodes: int) -> str:
|
|
86
|
+
sequence = completed_offset + completed_nodes + 1
|
|
87
|
+
return f"{execution.execution_id}:{sequence}:{node_name}"
|
|
88
|
+
|
|
89
|
+
async def persist_pending(context: NodeExecutionContext, _state: dict[str, Any]) -> None:
|
|
90
|
+
execution.events.append(
|
|
91
|
+
{
|
|
92
|
+
"event_type": "workflow_step_pending",
|
|
93
|
+
"step": context.node_name,
|
|
94
|
+
"attempt": context.attempt,
|
|
95
|
+
"idempotency_key": context.idempotency_key,
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
await self._store.save(execution)
|
|
99
|
+
|
|
100
|
+
async def persist_step(
|
|
101
|
+
current_node: str,
|
|
102
|
+
next_node: str,
|
|
103
|
+
state: dict[str, Any],
|
|
104
|
+
completed_nodes: int,
|
|
105
|
+
) -> None:
|
|
106
|
+
execution.current_step = next_node
|
|
107
|
+
execution.steps_completed = completed_offset + completed_nodes
|
|
108
|
+
execution.result_data = state
|
|
109
|
+
execution.events.append(
|
|
110
|
+
{
|
|
111
|
+
"event_type": "workflow_step_completed",
|
|
112
|
+
"step": current_node,
|
|
113
|
+
"next_step": next_node,
|
|
114
|
+
"idempotency_key": idempotency_key(current_node, completed_nodes - 1),
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
await self._store.save(execution)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
result = await self._machine.ainvoke_from(
|
|
121
|
+
execution.result_data,
|
|
122
|
+
start_node=execution.current_step or START,
|
|
123
|
+
on_step=persist_step,
|
|
124
|
+
on_node_start=persist_pending,
|
|
125
|
+
idempotency_key_factory=idempotency_key,
|
|
126
|
+
)
|
|
127
|
+
except WorkflowPause as exc:
|
|
128
|
+
execution.status = "interrupted"
|
|
129
|
+
execution.events.append(
|
|
130
|
+
{
|
|
131
|
+
"event_type": "workflow_interrupted",
|
|
132
|
+
"step": execution.current_step,
|
|
133
|
+
"reason": exc.reason,
|
|
134
|
+
"data": exc.data,
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
await self._store.save(execution)
|
|
138
|
+
return execution
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
execution.status = "failed"
|
|
141
|
+
execution.events.append(
|
|
142
|
+
{
|
|
143
|
+
"event_type": "workflow_failed",
|
|
144
|
+
"step": execution.current_step,
|
|
145
|
+
"error": str(exc),
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
await self._store.save(execution)
|
|
149
|
+
if self._raise_on_failure:
|
|
150
|
+
raise
|
|
151
|
+
return execution
|
|
152
|
+
|
|
153
|
+
execution.status = "completed"
|
|
154
|
+
execution.current_step = END
|
|
155
|
+
execution.result_data = result
|
|
156
|
+
execution.events.append({"event_type": "workflow_completed", "step": END})
|
|
157
|
+
await self._store.save(execution)
|
|
158
|
+
return execution
|
|
159
|
+
|
|
160
|
+
def _check_access(
|
|
161
|
+
self,
|
|
162
|
+
execution: WorkflowExecution,
|
|
163
|
+
access: AccessContext | None,
|
|
164
|
+
) -> None:
|
|
165
|
+
enforce_access(
|
|
166
|
+
strict=self._strict_access,
|
|
167
|
+
accessor="DurableWorkflowRunner",
|
|
168
|
+
subject="工作流执行实例",
|
|
169
|
+
owner_user_id=execution.user_id,
|
|
170
|
+
owner_tenant_id=execution.tenant_id,
|
|
171
|
+
access=access,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
__all__ = ["DurableWorkflowRunner"]
|