dynworkflow 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- {dynworkflow-0.1.2 → dynworkflow-0.1.4}/PKG-INFO +5 -5
- {dynworkflow-0.1.2 → dynworkflow-0.1.4}/README.md +4 -4
- {dynworkflow-0.1.2 → dynworkflow-0.1.4}/pyproject.toml +1 -1
- {dynworkflow-0.1.2 → dynworkflow-0.1.4}/src/dynworkflow/cache.py +38 -9
- {dynworkflow-0.1.2 → dynworkflow-0.1.4}/src/dynworkflow/flow.py +168 -45
- {dynworkflow-0.1.2 → dynworkflow-0.1.4}/src/dynworkflow/reporter.py +14 -0
- {dynworkflow-0.1.2 → dynworkflow-0.1.4}/src/dynworkflow/__init__.py +0 -0
- {dynworkflow-0.1.2 → dynworkflow-0.1.4}/src/dynworkflow/agent.py +0 -0
- {dynworkflow-0.1.2 → dynworkflow-0.1.4}/src/dynworkflow/py.typed +0 -0
- {dynworkflow-0.1.2 → dynworkflow-0.1.4}/src/dynworkflow/schemas.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: dynworkflow
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Dynamic Workflow Library for Alkaid0
|
|
5
5
|
Author: cxykevin
|
|
6
6
|
Author-email: cxykevin <cxykevin@yeah.net>
|
|
@@ -40,18 +40,18 @@ def summarize() -> d.TaskGenerator:
|
|
|
40
40
|
flow.execute(summarize())
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
使用 `@flow.node(name)` 声明节点。普通节点可以返回 `None`、`Result`
|
|
43
|
+
使用 `@flow.node(name)` 声明节点。普通节点可以返回 `None`、`Result`、一个或多个下游 `Execute` 任务,或它们的组合(例如 `return d.Result(value), next_node(x=value)`)。`Result` 只记录本节点结果,不会阻止下游调度。生成器节点可以 `yield Agent` 或 `MultiAgent`,接收 Agent 结果后返回节点结果。
|
|
44
44
|
|
|
45
45
|
## 公共 API
|
|
46
46
|
|
|
47
47
|
- `Flow`:创建并执行工作流。
|
|
48
48
|
- `Flow.node(name)`:注册节点函数。
|
|
49
49
|
- `Flow.execute(*tasks)`:在当前进程中执行任务,并在所有节点结束后抛出节点异常。
|
|
50
|
-
- `Flow.run(*tasks)
|
|
50
|
+
- `Flow.run(*tasks)`:运行工作流,并监听 stdin 中的 JSONL 控制命令。支持 fork 的平台在 fork 子进程中运行,其他平台(如 Windows)在当前进程中运行。
|
|
51
51
|
- `Agent`:描述一次 Agent 请求。
|
|
52
52
|
- `MultiAgent`:并发运行多个 Agent 请求。
|
|
53
53
|
- `AgentClient`:连接 ACP WebSocket 端点。
|
|
54
|
-
- `Result
|
|
54
|
+
- `Result`:保存节点结果;可与下游任务同时返回。
|
|
55
55
|
|
|
56
56
|
## 执行语义
|
|
57
57
|
|
|
@@ -65,7 +65,7 @@ flow.execute(summarize())
|
|
|
65
65
|
|
|
66
66
|
缓存默认启用。可以使用 `Flow(..., cache=False)` 关闭缓存。缓存文件位于 `.alkaid0/workflow.db`;设置 `ALKAID0_WORKFLOW_SESSION_ID` 可以隔离不同会话。
|
|
67
67
|
|
|
68
|
-
可以通过 `Flow(..., report=True)` 或环境变量 `ALKAID0_WORKFLOW_REPORT=1` 开启 JSONL 状态上报。`Flow.run()` 会自动开启上报。事件包括 `graph`、`node`、`agent`、`agents_start`、`node_code` 和 `node_log`。
|
|
68
|
+
可以通过 `Flow(..., report=True)` 或环境变量 `ALKAID0_WORKFLOW_REPORT=1` 开启 JSONL 状态上报。`Flow.run()` 会自动开启上报。事件包括 `graph`、`node`、`node_result`、`agent`、`agents_start`、`node_code` 和 `node_log`。
|
|
69
69
|
|
|
70
70
|
## 示例
|
|
71
71
|
|
|
@@ -30,18 +30,18 @@ def summarize() -> d.TaskGenerator:
|
|
|
30
30
|
flow.execute(summarize())
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
使用 `@flow.node(name)` 声明节点。普通节点可以返回 `None`、`Result`
|
|
33
|
+
使用 `@flow.node(name)` 声明节点。普通节点可以返回 `None`、`Result`、一个或多个下游 `Execute` 任务,或它们的组合(例如 `return d.Result(value), next_node(x=value)`)。`Result` 只记录本节点结果,不会阻止下游调度。生成器节点可以 `yield Agent` 或 `MultiAgent`,接收 Agent 结果后返回节点结果。
|
|
34
34
|
|
|
35
35
|
## 公共 API
|
|
36
36
|
|
|
37
37
|
- `Flow`:创建并执行工作流。
|
|
38
38
|
- `Flow.node(name)`:注册节点函数。
|
|
39
39
|
- `Flow.execute(*tasks)`:在当前进程中执行任务,并在所有节点结束后抛出节点异常。
|
|
40
|
-
- `Flow.run(*tasks)
|
|
40
|
+
- `Flow.run(*tasks)`:运行工作流,并监听 stdin 中的 JSONL 控制命令。支持 fork 的平台在 fork 子进程中运行,其他平台(如 Windows)在当前进程中运行。
|
|
41
41
|
- `Agent`:描述一次 Agent 请求。
|
|
42
42
|
- `MultiAgent`:并发运行多个 Agent 请求。
|
|
43
43
|
- `AgentClient`:连接 ACP WebSocket 端点。
|
|
44
|
-
- `Result
|
|
44
|
+
- `Result`:保存节点结果;可与下游任务同时返回。
|
|
45
45
|
|
|
46
46
|
## 执行语义
|
|
47
47
|
|
|
@@ -55,7 +55,7 @@ flow.execute(summarize())
|
|
|
55
55
|
|
|
56
56
|
缓存默认启用。可以使用 `Flow(..., cache=False)` 关闭缓存。缓存文件位于 `.alkaid0/workflow.db`;设置 `ALKAID0_WORKFLOW_SESSION_ID` 可以隔离不同会话。
|
|
57
57
|
|
|
58
|
-
可以通过 `Flow(..., report=True)` 或环境变量 `ALKAID0_WORKFLOW_REPORT=1` 开启 JSONL 状态上报。`Flow.run()` 会自动开启上报。事件包括 `graph`、`node`、`agent`、`agents_start`、`node_code` 和 `node_log`。
|
|
58
|
+
可以通过 `Flow(..., report=True)` 或环境变量 `ALKAID0_WORKFLOW_REPORT=1` 开启 JSONL 状态上报。`Flow.run()` 会自动开启上报。事件包括 `graph`、`node`、`node_result`、`agent`、`agents_start`、`node_code` 和 `node_log`。
|
|
59
59
|
|
|
60
60
|
## 示例
|
|
61
61
|
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
缓存内容分两类:
|
|
5
5
|
- ``agent_cache``:Agent 运行结果(MultiAgent 按单个 agent 分别缓存),
|
|
6
6
|
以及未正常结束会话的 ACP sessionId(供下次按需 ACP load 续跑);
|
|
7
|
-
- ``node_cache``:节点执行结果——终值(Result/None
|
|
8
|
-
(Execute 链序列化为 [目标节点 id + 调用时参数]
|
|
7
|
+
- ``node_cache``:节点执行结果——终值(Result/None)、下游调度
|
|
8
|
+
(Execute 链序列化为 [目标节点 id + 调用时参数]),或两者的组合;
|
|
9
|
+
命中时重建重放。
|
|
9
10
|
|
|
10
11
|
联合键统一为 [会话作用域 + 工作流名 + 节点 ID + 载荷 hash];节点载荷
|
|
11
12
|
包含函数源码体 hash(代码变更即失效)与全部有效参数。
|
|
@@ -204,11 +205,24 @@ class WorkflowCache:
|
|
|
204
205
|
return False
|
|
205
206
|
return self._put("agent_cache", info, serialized, extra_kind=kind)
|
|
206
207
|
|
|
208
|
+
@staticmethod
|
|
209
|
+
def _valid_execute_items(items: Any) -> bool:
|
|
210
|
+
"""_valid_execute_items() 校验下游调度条目列表是否合法"""
|
|
211
|
+
if not isinstance(items, list):
|
|
212
|
+
return False
|
|
213
|
+
for item in items:
|
|
214
|
+
if (not isinstance(item, dict)
|
|
215
|
+
or not isinstance(item.get("node_id"), str)
|
|
216
|
+
or not isinstance(item.get("args"), dict)):
|
|
217
|
+
return False
|
|
218
|
+
return True
|
|
219
|
+
|
|
207
220
|
def get_node(self, info: CacheKey) -> dict[str, Any] | None:
|
|
208
221
|
"""get_node() 查询节点条目。
|
|
209
222
|
|
|
210
|
-
返回形如 ``{"kind": "none"}``、``{"kind": "result", "value": ...}
|
|
211
|
-
|
|
223
|
+
返回形如 ``{"kind": "none"}``、``{"kind": "result", "value": ...}``、
|
|
224
|
+
``{"kind": "executes", "items": [{"node_id": ..., "args": ...}]}``
|
|
225
|
+
或 ``{"kind": "items", "items": [...]}``(Result 与下游调度组合);
|
|
212
226
|
格式非法视为未命中。
|
|
213
227
|
"""
|
|
214
228
|
conn = self._conn
|
|
@@ -232,17 +246,32 @@ class WorkflowCache:
|
|
|
232
246
|
if not isinstance(parsed, dict):
|
|
233
247
|
return None
|
|
234
248
|
kind = parsed.get("kind")
|
|
235
|
-
if kind not in {"none", "result", "executes"}:
|
|
249
|
+
if kind not in {"none", "result", "executes", "items"}:
|
|
236
250
|
return None
|
|
237
251
|
if kind == "executes":
|
|
252
|
+
if not self._valid_execute_items(parsed.get("items")):
|
|
253
|
+
return None
|
|
254
|
+
elif kind == "items":
|
|
238
255
|
items = parsed.get("items")
|
|
239
|
-
if not isinstance(items, list):
|
|
256
|
+
if not isinstance(items, list) or not items:
|
|
240
257
|
return None
|
|
258
|
+
result_count = 0
|
|
241
259
|
for item in items:
|
|
242
|
-
if
|
|
243
|
-
|
|
244
|
-
|
|
260
|
+
if not isinstance(item, dict):
|
|
261
|
+
return None
|
|
262
|
+
item_kind = item.get("kind")
|
|
263
|
+
if item_kind == "result":
|
|
264
|
+
if "value" not in item:
|
|
265
|
+
return None
|
|
266
|
+
result_count += 1
|
|
267
|
+
elif item_kind == "execute":
|
|
268
|
+
if (not isinstance(item.get("node_id"), str)
|
|
269
|
+
or not isinstance(item.get("args"), dict)):
|
|
270
|
+
return None
|
|
271
|
+
else:
|
|
245
272
|
return None
|
|
273
|
+
if result_count > 1:
|
|
274
|
+
return None
|
|
246
275
|
return parsed
|
|
247
276
|
|
|
248
277
|
def put_node(self, info: CacheKey, descriptor: dict[str, Any]) -> bool:
|
|
@@ -11,6 +11,7 @@ import threading
|
|
|
11
11
|
import time
|
|
12
12
|
import ast
|
|
13
13
|
import textwrap
|
|
14
|
+
import traceback
|
|
14
15
|
from enum import Enum
|
|
15
16
|
|
|
16
17
|
from .agent import (Agent, AgentClientInterface, MultiAgent, AgentStatus,
|
|
@@ -43,7 +44,7 @@ class Graph(TypedDict):
|
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
class Result:
|
|
46
|
-
"Result
|
|
47
|
+
"Result 保存节点结果;节点可同时继续调度下游任务"
|
|
47
48
|
|
|
48
49
|
def __init__(self, result: Any) -> None:
|
|
49
50
|
self.result_val = result
|
|
@@ -52,7 +53,7 @@ class Result:
|
|
|
52
53
|
return f"Result({self.result_val})"
|
|
53
54
|
|
|
54
55
|
def __str__(self) -> str:
|
|
55
|
-
return str(self.
|
|
56
|
+
return str(self.result_val)
|
|
56
57
|
|
|
57
58
|
def result(self) -> Any:
|
|
58
59
|
"Result.result() 获取结果"
|
|
@@ -76,20 +77,54 @@ class NodeStatus(Enum):
|
|
|
76
77
|
ERROR = "error" # 完成
|
|
77
78
|
|
|
78
79
|
|
|
80
|
+
def _split_node_result(
|
|
81
|
+
ret: 'NodeResult') -> "tuple[list[Any], list[Execute]]":
|
|
82
|
+
'"_split_node_result() 拆分节点返回值为 (Result 值列表, 下游任务列表)"'
|
|
83
|
+
|
|
84
|
+
if ret is None:
|
|
85
|
+
return [], []
|
|
86
|
+
if isinstance(ret, (Result, Execute)):
|
|
87
|
+
items: list[Any] = [ret]
|
|
88
|
+
elif isinstance(ret, (list, tuple)):
|
|
89
|
+
items = list(ret)
|
|
90
|
+
else:
|
|
91
|
+
raise ValueError(f"Invalid node result: {ret!r}")
|
|
92
|
+
results: list[Any] = []
|
|
93
|
+
executes: list[Execute] = []
|
|
94
|
+
for item in items:
|
|
95
|
+
if isinstance(item, Result):
|
|
96
|
+
results.append(item.result())
|
|
97
|
+
elif isinstance(item, Execute):
|
|
98
|
+
executes.append(item)
|
|
99
|
+
else:
|
|
100
|
+
raise ValueError(f"Invalid node result item: {item!r}")
|
|
101
|
+
if len(results) > 1:
|
|
102
|
+
raise ValueError("A node may return at most one Result")
|
|
103
|
+
return results, executes
|
|
104
|
+
|
|
105
|
+
|
|
79
106
|
def _describe_node_result(ret: 'NodeResult') -> "dict[str, Any] | None":
|
|
80
107
|
'"_describe_node_result() 把节点返回值序列化为缓存描述符;不可缓存时返回 None'
|
|
81
108
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
109
|
+
results, executes = _split_node_result(ret)
|
|
110
|
+
exec_items: list[dict[str, Any]] = [
|
|
111
|
+
{"node_id": item._node._node_id, "args": dict(item._args)}
|
|
112
|
+
for item in executes
|
|
113
|
+
]
|
|
114
|
+
if not results:
|
|
115
|
+
if not exec_items:
|
|
116
|
+
return {"kind": "none"}
|
|
117
|
+
return {"kind": "executes", "items": exec_items}
|
|
118
|
+
if not exec_items:
|
|
85
119
|
# value 含不可序列化对象时由 put_node 整体放弃写入
|
|
86
|
-
return {"kind": "result", "value":
|
|
87
|
-
items
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
120
|
+
return {"kind": "result", "value": results[0]}
|
|
121
|
+
items: list[dict[str, Any]] = [
|
|
122
|
+
{"kind": "result", "value": results[0]}]
|
|
123
|
+
items.extend(
|
|
124
|
+
{"kind": "execute", "node_id": item._node._node_id,
|
|
125
|
+
"args": dict(item._args)}
|
|
126
|
+
for item in executes)
|
|
127
|
+
return {"kind": "items", "items": items}
|
|
93
128
|
|
|
94
129
|
|
|
95
130
|
class Node():
|
|
@@ -182,6 +217,13 @@ class Node():
|
|
|
182
217
|
flow._reporter.report_node(flow._workflow_id, self._node_id,
|
|
183
218
|
state.value, args, cached=cached)
|
|
184
219
|
|
|
220
|
+
def _report_result(self, flow: 'Flow', value: Any) -> None:
|
|
221
|
+
"_report_result() 推送一次节点终值事件(未启用上报时为空操作)"
|
|
222
|
+
if flow._reporter is None:
|
|
223
|
+
return
|
|
224
|
+
flow._reporter.report_node_result(flow._workflow_id, self._node_id,
|
|
225
|
+
value)
|
|
226
|
+
|
|
185
227
|
def _run(self, flow: 'Flow') -> None:
|
|
186
228
|
"Node.run() 执行节点,并把异常延迟到工作流结束统一抛出。"
|
|
187
229
|
try:
|
|
@@ -300,6 +342,9 @@ class Node():
|
|
|
300
342
|
self._report_status(flow, NodeStatus.ERROR, dic)
|
|
301
343
|
return
|
|
302
344
|
|
|
345
|
+
# 先校验返回值:多个 Result 或非法元素在此失败并转为节点错误
|
|
346
|
+
results, executes = _split_node_result(ret)
|
|
347
|
+
|
|
303
348
|
self.status = NodeStatus.DONE
|
|
304
349
|
# 推送:节点执行完成
|
|
305
350
|
self._report_status(flow, NodeStatus.DONE, dic)
|
|
@@ -310,49 +355,70 @@ class Node():
|
|
|
310
355
|
if descriptor is not None:
|
|
311
356
|
flow.cache.put_node(node_info, descriptor)
|
|
312
357
|
|
|
313
|
-
#
|
|
314
|
-
if
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return
|
|
358
|
+
# 记录 Result;与下游调度互不排斥,可同时生效
|
|
359
|
+
if results:
|
|
360
|
+
value = results[0]
|
|
361
|
+
flow._result[self._node_id] = value
|
|
362
|
+
# 推送:节点终值(宿主据此在 workflow 视图里展示 Result)
|
|
363
|
+
self._report_result(flow, value)
|
|
320
364
|
|
|
321
365
|
# 执行流程下一步
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
for i in ret:
|
|
325
|
-
i.run(flow)
|
|
366
|
+
for item in executes:
|
|
367
|
+
item.run(flow)
|
|
326
368
|
|
|
327
369
|
def _replay_cached(self, flow: 'Flow', cached: dict[str, Any]) -> None:
|
|
328
|
-
"Node._replay_cached()
|
|
370
|
+
"Node._replay_cached() 重放缓存的节点结果(终值/下游调度/两者组合)"
|
|
329
371
|
kind = cached.get("kind")
|
|
330
372
|
if kind == "none":
|
|
331
373
|
return
|
|
332
374
|
if kind == "result":
|
|
333
|
-
|
|
375
|
+
self._replay_result(flow, cached.get("value"))
|
|
334
376
|
return
|
|
377
|
+
known = {node._node_id: node for node in flow._nodes}
|
|
335
378
|
if kind == "executes":
|
|
336
|
-
known = {node._node_id: node for node in flow._nodes}
|
|
337
379
|
items = cached.get("items")
|
|
338
380
|
if not isinstance(items, list):
|
|
339
381
|
items = []
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
382
|
+
self._replay_executes(flow, known,
|
|
383
|
+
cast(list[dict[str, Any]], items))
|
|
384
|
+
return
|
|
385
|
+
if kind == "items":
|
|
386
|
+
raw_items = cached.get("items")
|
|
387
|
+
if not isinstance(raw_items, list):
|
|
388
|
+
raw_items = []
|
|
389
|
+
for item in cast(list[dict[str, Any]], raw_items):
|
|
390
|
+
item_kind = item.get("kind")
|
|
391
|
+
if item_kind == "result":
|
|
392
|
+
self._replay_result(flow, item.get("value"))
|
|
393
|
+
elif item_kind == "execute":
|
|
394
|
+
self._replay_executes(flow, known, [item])
|
|
395
|
+
else:
|
|
345
396
|
raise ValueError(
|
|
346
|
-
f"
|
|
347
|
-
# 重建调用点参数并走正常填充/调度路径,
|
|
348
|
-
# 完整保留跨节点部分参数累积语义。
|
|
349
|
-
args: Any = item.get("args")
|
|
350
|
-
args_value: dict[str, Any] = dict(cast(dict[str, Any], args)) if isinstance(
|
|
351
|
-
args, dict) else {}
|
|
352
|
-
Execute(target, args_value).run(flow)
|
|
397
|
+
f"Invalid cached node item kind: {item_kind!r}")
|
|
353
398
|
return
|
|
354
399
|
raise ValueError(f"Invalid cached node result kind: {kind!r}")
|
|
355
400
|
|
|
401
|
+
def _replay_result(self, flow: 'Flow', value: Any) -> None:
|
|
402
|
+
"_replay_result() 写入并上报缓存中的节点终值"
|
|
403
|
+
flow._result[self._node_id] = value
|
|
404
|
+
# 缓存命中同样上报终值,宿主视图与真实执行保持一致
|
|
405
|
+
self._report_result(flow, value)
|
|
406
|
+
|
|
407
|
+
def _replay_executes(self, flow: 'Flow', known: "dict[str, Node]",
|
|
408
|
+
items: list[dict[str, Any]]) -> None:
|
|
409
|
+
"_replay_executes() 重放缓存中的下游调度"
|
|
410
|
+
for item in items:
|
|
411
|
+
target_id = str(item.get("node_id"))
|
|
412
|
+
target = known.get(target_id)
|
|
413
|
+
if target is None:
|
|
414
|
+
raise ValueError(f"Cached target node not found: {target_id}")
|
|
415
|
+
# 重建调用点参数并走正常填充/调度路径,
|
|
416
|
+
# 完整保留跨节点部分参数累积语义。
|
|
417
|
+
args: Any = item.get("args")
|
|
418
|
+
args_value: dict[str, Any] = dict(
|
|
419
|
+
cast(dict[str, Any], args)) if isinstance(args, dict) else {}
|
|
420
|
+
Execute(target, args_value).run(flow)
|
|
421
|
+
|
|
356
422
|
def _create_thread_task(self, flow: 'Flow') -> threading.Thread:
|
|
357
423
|
"Node.thread_tast() 启动线程"
|
|
358
424
|
if (self._thread is not None and self._thread.is_alive()):
|
|
@@ -427,11 +493,14 @@ type Agents = (
|
|
|
427
493
|
MultiAgent |
|
|
428
494
|
Agent
|
|
429
495
|
) # Agents 子 Agent 对象
|
|
430
|
-
type
|
|
496
|
+
type NodeResultItem = (
|
|
431
497
|
Execute |
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
498
|
+
Result
|
|
499
|
+
) # NodeResultItem 节点返回的单个元素
|
|
500
|
+
type NodeResult = (
|
|
501
|
+
NodeResultItem |
|
|
502
|
+
list[NodeResultItem] |
|
|
503
|
+
tuple[NodeResultItem, ...] |
|
|
435
504
|
None
|
|
436
505
|
) # Nodes 节点返回对象
|
|
437
506
|
type TaskGenerator = (Generator[
|
|
@@ -579,7 +648,7 @@ class Flow:
|
|
|
579
648
|
return {
|
|
580
649
|
"nodes": {
|
|
581
650
|
node._node_id: { # 节点信息
|
|
582
|
-
"name": node.
|
|
651
|
+
"name": node._node_name, # 显示名(@flow.node("...") 的名字)
|
|
583
652
|
} for node in self._nodes
|
|
584
653
|
},
|
|
585
654
|
"edges": edges,
|
|
@@ -614,7 +683,11 @@ class Flow:
|
|
|
614
683
|
self._raise_exceptions()
|
|
615
684
|
|
|
616
685
|
def run(self, *tasks: Execute) -> NoReturn:
|
|
617
|
-
"""
|
|
686
|
+
"""Flow.run() 运行工作流,同时监听 stdin JSONL 控制命令。
|
|
687
|
+
|
|
688
|
+
支持 fork 的平台(Linux/macOS)在 fork 子进程内执行工作流;
|
|
689
|
+
没有 fork 的平台(如 Windows)在当前进程内执行工作流。
|
|
690
|
+
"""
|
|
618
691
|
sys.stdout.write("\x1edynworkflow\x1f\n")
|
|
619
692
|
sys.stdout.flush()
|
|
620
693
|
self._report = True
|
|
@@ -622,6 +695,26 @@ class Flow:
|
|
|
622
695
|
self._reporter = optional_reporter(True)
|
|
623
696
|
if self._agent is None:
|
|
624
697
|
raise ValueError("No availble agent found!")
|
|
698
|
+
if "fork" in multiprocessing.get_all_start_methods():
|
|
699
|
+
self._run_forked(tasks)
|
|
700
|
+
else:
|
|
701
|
+
# Windows 等平台既没有 fork,spawn 又无法序列化 Flow 状态与
|
|
702
|
+
# 节点函数,因此退化为在当前进程内执行工作流。
|
|
703
|
+
self._run_in_current_process(tasks)
|
|
704
|
+
|
|
705
|
+
def _exit(self, code: int) -> NoReturn:
|
|
706
|
+
"_exit() 强制刷新输出并短暂等待后退出,避免宿主漏读最后一批事件"
|
|
707
|
+
try:
|
|
708
|
+
sys.stdout.flush()
|
|
709
|
+
sys.stderr.flush()
|
|
710
|
+
except Exception:
|
|
711
|
+
# 管道已关闭等情况不影响退出流程。
|
|
712
|
+
pass
|
|
713
|
+
time.sleep(0.2)
|
|
714
|
+
sys.exit(code)
|
|
715
|
+
|
|
716
|
+
def _run_forked(self, tasks: tuple[Execute, ...]) -> NoReturn:
|
|
717
|
+
"_run_forked() 在 fork 子进程执行工作流,当前进程监听 stdin JSONL。"
|
|
625
718
|
control: Any = multiprocessing.get_context("fork").Queue()
|
|
626
719
|
process = multiprocessing.get_context("fork").Process(
|
|
627
720
|
target=self._execute_child, args=(tasks, control))
|
|
@@ -657,7 +750,37 @@ class Flow:
|
|
|
657
750
|
process.terminate()
|
|
658
751
|
process.join(2)
|
|
659
752
|
# Preserve a non-zero exit status when the child failed unexpectedly.
|
|
660
|
-
|
|
753
|
+
self._exit(process.exitcode if process.exitcode is not None else 0)
|
|
754
|
+
|
|
755
|
+
def _run_in_current_process(self, tasks: tuple[Execute, ...]) -> NoReturn:
|
|
756
|
+
"_run_in_current_process() 在没有 fork 的平台于当前进程内执行工作流,"
|
|
757
|
+
"stdin 由 daemon 线程转发到控制队列。"
|
|
758
|
+
control: Any = queue.Queue()
|
|
759
|
+
|
|
760
|
+
def read_stdin() -> None:
|
|
761
|
+
try:
|
|
762
|
+
for line in sys.stdin:
|
|
763
|
+
try:
|
|
764
|
+
command = json.loads(line)
|
|
765
|
+
except json.JSONDecodeError:
|
|
766
|
+
continue
|
|
767
|
+
if isinstance(command, dict):
|
|
768
|
+
command = cast(dict[str, str], command)
|
|
769
|
+
control.put(command)
|
|
770
|
+
if command.get("cmd") == "shutdown":
|
|
771
|
+
return
|
|
772
|
+
finally:
|
|
773
|
+
# stdin 结束视为关闭请求,与 fork 版本父进程的清理行为一致。
|
|
774
|
+
control.put({"cmd": "shutdown"})
|
|
775
|
+
|
|
776
|
+
threading.Thread(target=read_stdin, daemon=True).start()
|
|
777
|
+
try:
|
|
778
|
+
self._execute_child(tasks, control)
|
|
779
|
+
except BaseException:
|
|
780
|
+
# 保留非零退出码,行为与 fork 子进程异常退出保持一致。
|
|
781
|
+
traceback.print_exc()
|
|
782
|
+
self._exit(1)
|
|
783
|
+
self._exit(0)
|
|
661
784
|
|
|
662
785
|
def _execute_child(self, tasks: tuple[Execute, ...], control: Any) -> None:
|
|
663
786
|
def listen() -> None:
|
|
@@ -156,6 +156,20 @@ class StatusReporter:
|
|
|
156
156
|
"code": code,
|
|
157
157
|
}])
|
|
158
158
|
|
|
159
|
+
def report_node_result(self, workflow_id: str, node_id: str,
|
|
160
|
+
result: Any) -> None:
|
|
161
|
+
"""report_node_result() 上报节点终值(`Result` 携带的 value)。
|
|
162
|
+
|
|
163
|
+
值不可 JSON 序列化时由 `emit` 的 `default=str` 兜底;上报失败不影响执行。
|
|
164
|
+
"""
|
|
165
|
+
self.emit([{
|
|
166
|
+
"type": "node_result",
|
|
167
|
+
"time": _utc_now(),
|
|
168
|
+
"workflow": workflow_id,
|
|
169
|
+
"nodeId": node_id,
|
|
170
|
+
"result": result,
|
|
171
|
+
}])
|
|
172
|
+
|
|
159
173
|
def report_graph(self, workflow_id: str, graph: Graph) -> None:
|
|
160
174
|
"""report_graph() 上报 Flow 生成的流程图(set 序列化为有序列表)。"""
|
|
161
175
|
self.emit([{
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|