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,411 @@
|
|
|
1
|
+
"""工作流和宏执行实例存储。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import sqlite3
|
|
8
|
+
import uuid
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from collections.abc import Generator
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from agent_core.storage._sqlite_utils import (
|
|
18
|
+
_run_in_thread,
|
|
19
|
+
run_sync_in_thread,
|
|
20
|
+
sqlite_connection,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _now() -> str:
|
|
25
|
+
return datetime.now(UTC).isoformat()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WorkflowConcurrencyError(RuntimeError):
|
|
29
|
+
"""工作流执行实例版本冲突。"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _json_value(value: Any, default: dict[str, Any] | list[dict[str, Any]]) -> Any:
|
|
33
|
+
"""统一 SQLite 文本和 asyncpg JSONB 的返回形态。"""
|
|
34
|
+
if value is None or value == "":
|
|
35
|
+
return default
|
|
36
|
+
return json.loads(value) if isinstance(value, str) else value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _postgres_datetime(value: str) -> datetime:
|
|
40
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
41
|
+
return parsed if parsed.tzinfo is not None else parsed.replace(tzinfo=UTC)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class WorkflowExecution:
|
|
46
|
+
execution_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
47
|
+
definition_id: str = ""
|
|
48
|
+
execution_type: str = "workflow"
|
|
49
|
+
user_id: str | None = None
|
|
50
|
+
tenant_id: str | None = None
|
|
51
|
+
status: str = "queued"
|
|
52
|
+
current_step: str | None = None
|
|
53
|
+
steps_completed: int = 0
|
|
54
|
+
total_steps: int = 0
|
|
55
|
+
input_data: dict[str, Any] = field(default_factory=dict)
|
|
56
|
+
result_data: dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
events: list[dict[str, Any]] = field(default_factory=list)
|
|
58
|
+
version: int = 0
|
|
59
|
+
created_at: str = field(default_factory=_now)
|
|
60
|
+
updated_at: str = field(default_factory=_now)
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
return {
|
|
64
|
+
"execution_id": self.execution_id,
|
|
65
|
+
"definition_id": self.definition_id,
|
|
66
|
+
"execution_type": self.execution_type,
|
|
67
|
+
"user_id": self.user_id,
|
|
68
|
+
"tenant_id": self.tenant_id,
|
|
69
|
+
"status": self.status,
|
|
70
|
+
"current_step": self.current_step,
|
|
71
|
+
"steps_completed": self.steps_completed,
|
|
72
|
+
"total_steps": self.total_steps,
|
|
73
|
+
"input_data": self.input_data,
|
|
74
|
+
"result_data": self.result_data,
|
|
75
|
+
"events": self.events,
|
|
76
|
+
"version": self.version,
|
|
77
|
+
"created_at": self.created_at,
|
|
78
|
+
"updated_at": self.updated_at,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class WorkflowExecutionStore(ABC):
|
|
83
|
+
@abstractmethod
|
|
84
|
+
async def save(self, execution: WorkflowExecution) -> None: ...
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
async def load(self, execution_id: str) -> WorkflowExecution | None: ...
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
async def list(self, definition_id: str, tenant_id: str | None = None) -> list[WorkflowExecution]: ...
|
|
91
|
+
|
|
92
|
+
# 统一端口名称,同时保留工作流模块已有的 save/load/list 调用。
|
|
93
|
+
async def save_execution(self, execution: WorkflowExecution) -> None:
|
|
94
|
+
await self.save(execution)
|
|
95
|
+
|
|
96
|
+
async def load_execution(self, execution_id: str) -> WorkflowExecution | None:
|
|
97
|
+
return await self.load(execution_id)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _from_dict(value: dict[str, Any]) -> WorkflowExecution:
|
|
101
|
+
input_data = _json_value(value.get("input_data"), {})
|
|
102
|
+
result_data = _json_value(value.get("result_data"), {})
|
|
103
|
+
events = _json_value(value.get("events"), [])
|
|
104
|
+
return WorkflowExecution(
|
|
105
|
+
execution_id=str(value["execution_id"]),
|
|
106
|
+
definition_id=str(value.get("definition_id", "")),
|
|
107
|
+
execution_type=str(value.get("execution_type", "workflow")),
|
|
108
|
+
user_id=value.get("user_id"),
|
|
109
|
+
tenant_id=value.get("tenant_id"),
|
|
110
|
+
status=str(value.get("status", "queued")),
|
|
111
|
+
current_step=value.get("current_step"),
|
|
112
|
+
steps_completed=int(value.get("steps_completed", 0)),
|
|
113
|
+
total_steps=int(value.get("total_steps", 0)),
|
|
114
|
+
input_data=dict(input_data),
|
|
115
|
+
result_data=dict(result_data),
|
|
116
|
+
events=list(events),
|
|
117
|
+
version=int(value.get("version", 0)),
|
|
118
|
+
created_at=str(value.get("created_at", _now())),
|
|
119
|
+
updated_at=str(value.get("updated_at", _now())),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class MemoryWorkflowExecutionStore(WorkflowExecutionStore):
|
|
124
|
+
def __init__(self) -> None:
|
|
125
|
+
self._items: dict[str, dict[str, Any]] = {}
|
|
126
|
+
self._lock = asyncio.Lock()
|
|
127
|
+
|
|
128
|
+
async def save(self, execution: WorkflowExecution) -> None:
|
|
129
|
+
async with self._lock:
|
|
130
|
+
previous = self._items.get(execution.execution_id)
|
|
131
|
+
stored_version = int(previous.get("version", 0)) if previous else 0
|
|
132
|
+
if previous is not None and stored_version != execution.version:
|
|
133
|
+
raise WorkflowConcurrencyError(f"工作流 {execution.execution_id} 版本冲突")
|
|
134
|
+
execution.version = stored_version + 1
|
|
135
|
+
execution.updated_at = _now()
|
|
136
|
+
self._items[execution.execution_id] = execution.to_dict()
|
|
137
|
+
|
|
138
|
+
async def load(self, execution_id: str) -> WorkflowExecution | None:
|
|
139
|
+
value = self._items.get(execution_id)
|
|
140
|
+
return _from_dict(value) if value else None
|
|
141
|
+
|
|
142
|
+
async def list(self, definition_id: str, tenant_id: str | None = None) -> list[WorkflowExecution]:
|
|
143
|
+
return [
|
|
144
|
+
_from_dict(value)
|
|
145
|
+
for value in self._items.values()
|
|
146
|
+
if value.get("definition_id") == definition_id
|
|
147
|
+
and (tenant_id is None or value.get("tenant_id") == tenant_id)
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class SqliteWorkflowExecutionStore(WorkflowExecutionStore):
|
|
152
|
+
DDL = """
|
|
153
|
+
CREATE TABLE IF NOT EXISTS workflow_executions (
|
|
154
|
+
execution_id TEXT PRIMARY KEY,
|
|
155
|
+
definition_id TEXT NOT NULL,
|
|
156
|
+
execution_type TEXT NOT NULL,
|
|
157
|
+
user_id TEXT,
|
|
158
|
+
tenant_id TEXT,
|
|
159
|
+
status TEXT NOT NULL,
|
|
160
|
+
current_step TEXT,
|
|
161
|
+
steps_completed INTEGER NOT NULL DEFAULT 0,
|
|
162
|
+
total_steps INTEGER NOT NULL DEFAULT 0,
|
|
163
|
+
input_data TEXT NOT NULL DEFAULT '{}',
|
|
164
|
+
result_data TEXT NOT NULL DEFAULT '{}',
|
|
165
|
+
events TEXT NOT NULL DEFAULT '[]',
|
|
166
|
+
version INTEGER NOT NULL DEFAULT 0,
|
|
167
|
+
created_at TEXT NOT NULL,
|
|
168
|
+
updated_at TEXT NOT NULL
|
|
169
|
+
);
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_execution_definition
|
|
171
|
+
ON workflow_executions(definition_id, tenant_id, updated_at);
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __init__(self, db_path: str | Path = "./sessions.db") -> None:
|
|
175
|
+
self._db_path = str(Path(db_path).resolve())
|
|
176
|
+
# 建表 DDL/PRAGMA 初始化同样放入工作线程执行,与 async 方法共用同一连接参数规范。
|
|
177
|
+
run_sync_in_thread(self._init_db)
|
|
178
|
+
|
|
179
|
+
def _init_db(self) -> None:
|
|
180
|
+
with self._connect() as conn:
|
|
181
|
+
conn.executescript(self.DDL)
|
|
182
|
+
|
|
183
|
+
@contextmanager
|
|
184
|
+
def _connect(self) -> Generator[sqlite3.Connection]:
|
|
185
|
+
"""获取 SQLite 连接(同步,自动提交/回滚;统一 timeout 与 WAL 规范)。"""
|
|
186
|
+
with sqlite_connection(self._db_path) as conn:
|
|
187
|
+
yield conn
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def _row(row: tuple[Any, ...]) -> WorkflowExecution:
|
|
191
|
+
keys = [
|
|
192
|
+
"execution_id",
|
|
193
|
+
"definition_id",
|
|
194
|
+
"execution_type",
|
|
195
|
+
"user_id",
|
|
196
|
+
"tenant_id",
|
|
197
|
+
"status",
|
|
198
|
+
"current_step",
|
|
199
|
+
"steps_completed",
|
|
200
|
+
"total_steps",
|
|
201
|
+
"input_data",
|
|
202
|
+
"result_data",
|
|
203
|
+
"events",
|
|
204
|
+
"version",
|
|
205
|
+
"created_at",
|
|
206
|
+
"updated_at",
|
|
207
|
+
]
|
|
208
|
+
value = dict(zip(keys, row, strict=True))
|
|
209
|
+
for key in ("input_data", "result_data", "events"):
|
|
210
|
+
value[key] = json.loads(value[key]) if value[key] else ({} if key != "events" else [])
|
|
211
|
+
return _from_dict(value)
|
|
212
|
+
|
|
213
|
+
async def save(self, execution: WorkflowExecution) -> None:
|
|
214
|
+
def _op() -> tuple[int, str]:
|
|
215
|
+
with self._connect() as conn:
|
|
216
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
217
|
+
row = conn.execute(
|
|
218
|
+
"SELECT version FROM workflow_executions WHERE execution_id=?",
|
|
219
|
+
(execution.execution_id,),
|
|
220
|
+
).fetchone()
|
|
221
|
+
stored_version = int(row[0]) if row else 0
|
|
222
|
+
if row is not None and stored_version != execution.version:
|
|
223
|
+
raise WorkflowConcurrencyError(f"工作流 {execution.execution_id} 版本冲突")
|
|
224
|
+
next_version = stored_version + 1
|
|
225
|
+
updated_at = _now()
|
|
226
|
+
value = execution.to_dict()
|
|
227
|
+
cursor = conn.execute(
|
|
228
|
+
"""INSERT INTO workflow_executions
|
|
229
|
+
(execution_id,definition_id,execution_type,user_id,tenant_id,status,current_step,
|
|
230
|
+
steps_completed,total_steps,input_data,result_data,events,version,created_at,updated_at)
|
|
231
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
232
|
+
ON CONFLICT(execution_id) DO UPDATE SET status=excluded.status,
|
|
233
|
+
current_step=excluded.current_step, steps_completed=excluded.steps_completed,
|
|
234
|
+
total_steps=excluded.total_steps, result_data=excluded.result_data,
|
|
235
|
+
events=excluded.events, version=excluded.version, updated_at=excluded.updated_at
|
|
236
|
+
WHERE workflow_executions.version=?""",
|
|
237
|
+
(
|
|
238
|
+
execution.execution_id,
|
|
239
|
+
execution.definition_id,
|
|
240
|
+
execution.execution_type,
|
|
241
|
+
execution.user_id,
|
|
242
|
+
execution.tenant_id,
|
|
243
|
+
execution.status,
|
|
244
|
+
execution.current_step,
|
|
245
|
+
execution.steps_completed,
|
|
246
|
+
execution.total_steps,
|
|
247
|
+
json.dumps(value["input_data"], ensure_ascii=False),
|
|
248
|
+
json.dumps(value["result_data"], ensure_ascii=False),
|
|
249
|
+
json.dumps(value["events"], ensure_ascii=False),
|
|
250
|
+
next_version,
|
|
251
|
+
execution.created_at,
|
|
252
|
+
updated_at,
|
|
253
|
+
stored_version,
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
if cursor.rowcount != 1:
|
|
257
|
+
raise WorkflowConcurrencyError(f"工作流 {execution.execution_id} 保存时发生并发冲突")
|
|
258
|
+
return next_version, updated_at
|
|
259
|
+
|
|
260
|
+
next_version, updated_at = await _run_in_thread(_op)
|
|
261
|
+
execution.version = next_version
|
|
262
|
+
execution.updated_at = updated_at
|
|
263
|
+
|
|
264
|
+
async def load(self, execution_id: str) -> WorkflowExecution | None:
|
|
265
|
+
def _op() -> Any:
|
|
266
|
+
with self._connect() as conn:
|
|
267
|
+
return conn.execute(
|
|
268
|
+
"SELECT execution_id,definition_id,execution_type,user_id,tenant_id,status,current_step,"
|
|
269
|
+
"steps_completed,total_steps,input_data,result_data,events,version,created_at,updated_at "
|
|
270
|
+
"FROM workflow_executions WHERE execution_id=?",
|
|
271
|
+
(execution_id,),
|
|
272
|
+
).fetchone()
|
|
273
|
+
|
|
274
|
+
row = await _run_in_thread(_op)
|
|
275
|
+
return self._row(row) if row else None
|
|
276
|
+
|
|
277
|
+
async def list(self, definition_id: str, tenant_id: str | None = None) -> list[WorkflowExecution]:
|
|
278
|
+
query = (
|
|
279
|
+
"SELECT execution_id,definition_id,execution_type,user_id,tenant_id,"
|
|
280
|
+
"status,current_step,steps_completed,total_steps,input_data,result_data,"
|
|
281
|
+
"events,version,created_at,updated_at FROM workflow_executions "
|
|
282
|
+
"WHERE definition_id=?"
|
|
283
|
+
)
|
|
284
|
+
args: list[Any] = [definition_id]
|
|
285
|
+
if tenant_id is not None:
|
|
286
|
+
query += " AND tenant_id=?"
|
|
287
|
+
args.append(tenant_id)
|
|
288
|
+
query += " ORDER BY updated_at DESC"
|
|
289
|
+
|
|
290
|
+
def _op() -> list[Any]:
|
|
291
|
+
with self._connect() as conn:
|
|
292
|
+
return conn.execute(query, args).fetchall()
|
|
293
|
+
|
|
294
|
+
rows = await _run_in_thread(_op)
|
|
295
|
+
return [self._row(row) for row in rows]
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class PostgresWorkflowExecutionStore(WorkflowExecutionStore):
|
|
299
|
+
def __init__(self, dsn: str) -> None:
|
|
300
|
+
self._dsn = dsn.replace("postgresql+asyncpg://", "postgresql://")
|
|
301
|
+
self._pool: Any = None
|
|
302
|
+
|
|
303
|
+
async def initialize(self) -> None:
|
|
304
|
+
try:
|
|
305
|
+
import asyncpg
|
|
306
|
+
except ImportError as exc:
|
|
307
|
+
raise ImportError("PostgresWorkflowExecutionStore 需要 asyncpg") from exc
|
|
308
|
+
self._pool = await asyncpg.create_pool(self._dsn, min_size=2, max_size=10)
|
|
309
|
+
async with self._pool.acquire() as conn:
|
|
310
|
+
await conn.execute("""
|
|
311
|
+
CREATE TABLE IF NOT EXISTS workflow_executions (
|
|
312
|
+
execution_id TEXT PRIMARY KEY, definition_id TEXT NOT NULL,
|
|
313
|
+
execution_type TEXT NOT NULL, user_id TEXT, tenant_id TEXT,
|
|
314
|
+
status TEXT NOT NULL, current_step TEXT,
|
|
315
|
+
steps_completed INTEGER NOT NULL DEFAULT 0, total_steps INTEGER NOT NULL DEFAULT 0,
|
|
316
|
+
input_data JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
317
|
+
result_data JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
318
|
+
events JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
319
|
+
version INTEGER NOT NULL DEFAULT 0,
|
|
320
|
+
created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL
|
|
321
|
+
)
|
|
322
|
+
""")
|
|
323
|
+
|
|
324
|
+
async def close(self) -> None:
|
|
325
|
+
if self._pool:
|
|
326
|
+
await self._pool.close()
|
|
327
|
+
|
|
328
|
+
def _check(self) -> None:
|
|
329
|
+
if self._pool is None:
|
|
330
|
+
raise RuntimeError("PostgresWorkflowExecutionStore 未初始化")
|
|
331
|
+
|
|
332
|
+
async def save(self, execution: WorkflowExecution) -> None:
|
|
333
|
+
self._check()
|
|
334
|
+
async with self._pool.acquire() as conn:
|
|
335
|
+
async with conn.transaction():
|
|
336
|
+
previous = await conn.fetchval(
|
|
337
|
+
"SELECT version FROM workflow_executions WHERE execution_id=$1 FOR UPDATE",
|
|
338
|
+
execution.execution_id,
|
|
339
|
+
)
|
|
340
|
+
stored_version = int(previous) if previous is not None else 0
|
|
341
|
+
if previous is not None and stored_version != execution.version:
|
|
342
|
+
raise WorkflowConcurrencyError(f"工作流 {execution.execution_id} 版本冲突")
|
|
343
|
+
next_version = stored_version + 1
|
|
344
|
+
updated_at = _now()
|
|
345
|
+
value = execution.to_dict()
|
|
346
|
+
command = await conn.execute(
|
|
347
|
+
"""INSERT INTO workflow_executions
|
|
348
|
+
(execution_id,definition_id,execution_type,user_id,tenant_id,status,
|
|
349
|
+
current_step,steps_completed,total_steps,input_data,result_data,
|
|
350
|
+
events,version,created_at,updated_at)
|
|
351
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10::jsonb,$11::jsonb,$12::jsonb,$13,$14,$15)
|
|
352
|
+
ON CONFLICT(execution_id) DO UPDATE SET
|
|
353
|
+
status=$6,current_step=$7,steps_completed=$8,total_steps=$9,
|
|
354
|
+
result_data=$11::jsonb,events=$12::jsonb,version=$13,updated_at=$15
|
|
355
|
+
WHERE workflow_executions.version=$16""",
|
|
356
|
+
execution.execution_id,
|
|
357
|
+
execution.definition_id,
|
|
358
|
+
execution.execution_type,
|
|
359
|
+
execution.user_id,
|
|
360
|
+
execution.tenant_id,
|
|
361
|
+
execution.status,
|
|
362
|
+
execution.current_step,
|
|
363
|
+
execution.steps_completed,
|
|
364
|
+
execution.total_steps,
|
|
365
|
+
json.dumps(value["input_data"]),
|
|
366
|
+
json.dumps(value["result_data"]),
|
|
367
|
+
json.dumps(value["events"]),
|
|
368
|
+
next_version,
|
|
369
|
+
_postgres_datetime(execution.created_at),
|
|
370
|
+
_postgres_datetime(updated_at),
|
|
371
|
+
stored_version,
|
|
372
|
+
)
|
|
373
|
+
expected = "INSERT 0 1" if previous is None else "UPDATE 1"
|
|
374
|
+
if command != expected:
|
|
375
|
+
raise WorkflowConcurrencyError(f"工作流 {execution.execution_id} 保存时发生并发冲突")
|
|
376
|
+
execution.version = next_version
|
|
377
|
+
execution.updated_at = updated_at
|
|
378
|
+
|
|
379
|
+
async def load(self, execution_id: str) -> WorkflowExecution | None:
|
|
380
|
+
self._check()
|
|
381
|
+
async with self._pool.acquire() as conn:
|
|
382
|
+
row = await conn.fetchrow("SELECT * FROM workflow_executions WHERE execution_id=$1", execution_id)
|
|
383
|
+
return _from_dict(dict(row)) if row else None
|
|
384
|
+
|
|
385
|
+
async def list(self, definition_id: str, tenant_id: str | None = None) -> list[WorkflowExecution]:
|
|
386
|
+
self._check()
|
|
387
|
+
async with self._pool.acquire() as conn:
|
|
388
|
+
rows = await conn.fetch(
|
|
389
|
+
"""SELECT * FROM workflow_executions
|
|
390
|
+
WHERE definition_id=$1 AND ($2::text IS NULL OR tenant_id=$2)
|
|
391
|
+
ORDER BY updated_at DESC""",
|
|
392
|
+
definition_id,
|
|
393
|
+
tenant_id,
|
|
394
|
+
)
|
|
395
|
+
return [_from_dict(dict(row)) for row in rows]
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def create_workflow_execution_store(
|
|
399
|
+
env: str = "development",
|
|
400
|
+
*,
|
|
401
|
+
sqlite_path: str = "./sessions.db",
|
|
402
|
+
postgres_url: str | None = None,
|
|
403
|
+
) -> WorkflowExecutionStore:
|
|
404
|
+
normalized = env.lower()
|
|
405
|
+
if normalized in {"test", "testing"}:
|
|
406
|
+
return MemoryWorkflowExecutionStore()
|
|
407
|
+
if normalized in {"production", "prod"}:
|
|
408
|
+
if not postgres_url:
|
|
409
|
+
raise ValueError("生产环境需要 PostgreSQL 连接地址")
|
|
410
|
+
return PostgresWorkflowExecutionStore(postgres_url)
|
|
411
|
+
return SqliteWorkflowExecutionStore(sqlite_path)
|