dynworkflow 0.1.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.
dynworkflow/flow.py ADDED
@@ -0,0 +1,702 @@
1
+ # coding: utf-8
2
+ from typing import Any, Generator, Callable, NoReturn, TypedDict, cast
3
+ import hashlib
4
+ import json
5
+ import multiprocessing
6
+ import inspect
7
+ import os
8
+ import sys
9
+ import queue
10
+ import threading
11
+ import time
12
+ import ast
13
+ import textwrap
14
+ from enum import Enum
15
+
16
+ from .agent import (Agent, AgentClientInterface, MultiAgent, AgentStatus,
17
+ find_agent_group, gen_conn_info_from_env,
18
+ raise_in_thread)
19
+ from .cache import CacheContext, WorkflowCache, default_db_path
20
+ from .reporter import bind_node_print, optional_reporter, unbind_node_print
21
+
22
+ type StructureOutput = (
23
+ dict[str, StructureOutput] |
24
+ list[StructureOutput] |
25
+ str |
26
+ int |
27
+ float |
28
+ bool |
29
+ None
30
+ ) # StructureOutput 结构化输出类型
31
+
32
+
33
+ class GraphNode(TypedDict):
34
+ "GraphNode 节点图对象"
35
+ name: str # 节点名称
36
+
37
+
38
+ class Graph(TypedDict):
39
+ "Graph 节点图"
40
+ nodes: dict[str, GraphNode] # 节点字典
41
+ edges: dict[str, set[str]] # 边列表
42
+ start: set[str] # 起始节点
43
+
44
+
45
+ class Result:
46
+ "Result 向 Main Agent 返回结果"
47
+
48
+ def __init__(self, result: Any) -> None:
49
+ self.result_val = result
50
+
51
+ def __repr__(self) -> str:
52
+ return f"Result({self.result_val})"
53
+
54
+ def __str__(self) -> str:
55
+ return str(self.result)
56
+
57
+ def result(self) -> Any:
58
+ "Result.result() 获取结果"
59
+ return self.result_val
60
+
61
+
62
+ class _Empty:
63
+ "Empty 空对象"
64
+
65
+
66
+ _empty = _Empty() # _empty 空对象
67
+
68
+
69
+ class NodeStatus(Enum):
70
+ "NodeStatus 节点状态"
71
+
72
+ WAIT = "wait" # 等待
73
+ QUEUE = "queue" # 队列中
74
+ RUNNING = "running" # 运行中
75
+ DONE = "done" # 完成
76
+ ERROR = "error" # 完成
77
+
78
+
79
+ def _describe_node_result(ret: 'NodeResult') -> "dict[str, Any] | None":
80
+ '"_describe_node_result() 把节点返回值序列化为缓存描述符;不可缓存时返回 None'
81
+
82
+ if ret is None:
83
+ return {"kind": "none"}
84
+ if isinstance(ret, Result):
85
+ # value 含不可序列化对象时由 put_node 整体放弃写入
86
+ return {"kind": "result", "value": ret.result()}
87
+ items = list(ret) if isinstance(ret, (list, tuple)) else [ret]
88
+ exec_items: list[dict[str, Any]] = []
89
+ for item in items:
90
+ exec_items.append(
91
+ {"node_id": item._node._node_id, "args": dict(item._args)})
92
+ return {"kind": "executes", "items": exec_items}
93
+
94
+
95
+ class Node():
96
+ "Node 节点对象"
97
+
98
+ def __init__(self, node_name: str, node_id: str, func: 'TaskGen') -> None:
99
+ self._node_name = node_name # 节点名称
100
+ self._node_id = node_id # 节点 ID
101
+ self._func = func # 节点生成器
102
+ # === 代码体哈希(参与缓存联合键,函数体变更即失效) ===
103
+ self._source: str | None = None # 节点源码缓存(启动时广播)
104
+ self._code_hash: str | None = self._compute_code_hash()
105
+ # === 节点状态 ===
106
+ self._thread: threading.Thread | None = None # 节点线程
107
+ self._value_lock: threading.Lock = threading.Lock() # 节点值锁
108
+ # === 节点参数缓存 ===
109
+ self._func_cache: dict[str, Any] = {} # 函数调用参数缓存
110
+ self._func_cache_var_keyword: dict[str,
111
+ Any] | None = None # 函数调用可变参数缓存
112
+ self._func_cache_filled_num: int = 0 # 参数已填充数量
113
+ self._func_default_args: dict[str, Any] = {} # 默认参数
114
+ self._init_func_cache() # 初始化缓存
115
+ # === 节点结果 ===
116
+ self.status: NodeStatus = NodeStatus.WAIT # 节点状态
117
+ self.agents: list[AgentStatus] = [] # 节点代理
118
+
119
+ def _compute_code_hash(self) -> str | None:
120
+ "Node._compute_code_hash() 计算函数源码体的 sha256;源码不可得返回 None"
121
+ try:
122
+ source = inspect.getsource(self._func)
123
+ except (OSError, TypeError):
124
+ self._source = None
125
+ return None
126
+ # dedent + strip 归一化缩进,避免同等代码因粘贴缩进不同而误判变化
127
+ normalized = textwrap.dedent(source).strip()
128
+ self._source = normalized
129
+ return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
130
+
131
+ def _init_func_cache(self) -> None:
132
+ "Node._init_func_cache() 初始化函数调用参数缓存"
133
+ with self._value_lock:
134
+ sig = inspect.signature(self._func)
135
+ for name, param in sig.parameters.items():
136
+ if param.kind == inspect.Parameter.POSITIONAL_ONLY or param.kind == inspect.Parameter.VAR_POSITIONAL:
137
+ # 不支持位置参数
138
+ raise ValueError(
139
+ "Cannot use positional only or var positional arguments in node args")
140
+ if param.kind == inspect.Parameter.VAR_KEYWORD:
141
+ # 特殊处理可变关键字参数
142
+ self._func_cache_var_keyword = {}
143
+ continue
144
+ # 填充缓存
145
+ self._func_cache[name] = _empty
146
+ if param.default is not inspect.Parameter.empty:
147
+ # 填充默认值
148
+ self._func_cache[name] = param.default
149
+ self._func_cache_filled_num += 1
150
+ self._func_default_args[name] = param.default
151
+
152
+ def _fill_args(self, **args: Any) -> None:
153
+ "Node.fill_args() 填充参数"
154
+
155
+ with self._value_lock:
156
+ for name, value in args.items():
157
+ if name not in self._func_cache:
158
+ # 参数未定义
159
+ if self._func_cache_var_keyword is None:
160
+ # 不支持可变关键字参数
161
+ raise ValueError(f"Unknown argument: {name}")
162
+ self._func_cache_var_keyword[name] = value
163
+ continue
164
+ if isinstance(self._func_cache[name], _Empty):
165
+ self._func_cache_filled_num += 1
166
+ self._func_cache[name] = value
167
+
168
+ def _check_all_filled(self) -> bool:
169
+ "Node.check_all_filled() 检查所有参数是否已填充"
170
+ with self._value_lock:
171
+ return self._func_cache_filled_num == len(self._func_cache)
172
+
173
+ def __call__(self, **args: Any) -> 'Execute':
174
+ "Node() 覆盖原本调用,用于参数填充"
175
+ return Execute(self, args)
176
+
177
+ def _report_status(self, flow: 'Flow', state: NodeStatus,
178
+ args: dict[str, Any], *, cached: bool = False) -> None:
179
+ "_report_status() 推送一次节点状态事件(未启用上报时为空操作)"
180
+ if flow._reporter is None:
181
+ return
182
+ flow._reporter.report_node(flow._workflow_id, self._node_id,
183
+ state.value, args, cached=cached)
184
+
185
+ def _run(self, flow: 'Flow') -> None:
186
+ "Node.run() 执行节点,并把异常延迟到工作流结束统一抛出。"
187
+ try:
188
+ self._run_impl(flow)
189
+ except (NodeTerminated, WorkflowAborted):
190
+ # 控制命令导致的终止不是节点执行错误。
191
+ self.status = NodeStatus.ERROR
192
+ except Exception as e:
193
+ with self._value_lock:
194
+ args = self._func_cache | (
195
+ self._func_cache_var_keyword
196
+ if self._func_cache_var_keyword is not None else {}
197
+ )
198
+ e.add_note("In node: " + str(self._node_id))
199
+ setattr(e, "node_id", self._node_id)
200
+ self.status = NodeStatus.ERROR
201
+ flow._result[self._node_id] = ""
202
+ flow._error[self._node_id] = e
203
+ # 节点线程内的异常立即上报,但不打断其它节点。
204
+ self._report_status(flow, NodeStatus.ERROR, args)
205
+
206
+ def _run_impl(self, flow: 'Flow') -> None:
207
+ "Node._run_impl() 执行节点主体。"
208
+ self.status = NodeStatus.RUNNING
209
+ with self._value_lock:
210
+ dic = self._func_cache | (
211
+ # 合并参数,填充可变关键字参数(若有)
212
+ self._func_cache_var_keyword if
213
+ self._func_cache_var_keyword is not None else {}
214
+ )
215
+ # === 节点级缓存上下文(携带代码体哈希,函数体变更即失效) ===
216
+ cache_ctx = CacheContext(
217
+ cache=flow.cache, workflow=flow._workflow_id,
218
+ node_id=self._node_id, session_scope=flow._session_scope,
219
+ code_hash=self._code_hash)
220
+ node_info = cache_ctx.node_key(dic)
221
+ hit: dict[str, Any] | None = None
222
+ if node_info is not None and flow.cache is not None:
223
+ hit = flow.cache.get_node(node_info)
224
+ # 推送:节点开始运行(含全部已填充参数与缓存命中标记)
225
+ self._report_status(flow, NodeStatus.RUNNING, dic,
226
+ cached=hit is not None)
227
+ # 推送:广播本节点的源码
228
+ if flow._reporter is not None:
229
+ flow._reporter.report_node_code(flow._workflow_id, self._node_id,
230
+ self._node_name, self._source)
231
+ if hit is not None:
232
+ cached = hit
233
+ # 缓存命中:跳过节点函数执行,按描述符重放结果/下游调度
234
+ try:
235
+ self._replay_cached(flow, cached)
236
+ except Exception as e:
237
+ e.add_note("In node: " + str(self._node_id))
238
+ setattr(e, "node_id", self._node_id)
239
+ self.status = NodeStatus.ERROR
240
+ flow._result[self._node_id] = ""
241
+ flow._error[self._node_id] = e
242
+ # 推送:缓存命中节点重放失败
243
+ self._report_status(flow, NodeStatus.ERROR, dic,
244
+ cached=True)
245
+ return
246
+ self.status = NodeStatus.DONE
247
+ # 推送:缓存命中的节点同样推送完成状态与参数
248
+ self._report_status(flow, NodeStatus.DONE, dic, cached=True)
249
+ return
250
+
251
+ ret: NodeResult = None # 返回值
252
+ try:
253
+ if inspect.isgeneratorfunction(self._func):
254
+ it: TaskGenerator = self._func(**dic) # 调用生成器
255
+ result: Any | None = None # Agent 返回
256
+ call_number: int = 0 # 本节点内第几次 Agent/MultiAgent 调用
257
+ while True:
258
+ try: # 循环调用生成器
259
+ agents_call = it.send(result)
260
+ except StopIteration as e:
261
+ # 生成器结束
262
+ ret = e.value
263
+ break
264
+ # 执行子 Agent(透传缓存上下文与调用序号)
265
+ call_number += 1
266
+ call_info: AgentStatus = []
267
+ self.agents.append(call_info) # 利用引用
268
+ # 推送:节点即将启动本组 agents
269
+ if flow._reporter is not None:
270
+ flow._reporter.report_agents_start(
271
+ flow._workflow_id, self._node_id, call_number,
272
+ agents_call)
273
+ result = agents_call.run(call_info, flow._agent,
274
+ cache_ctx=cache_ctx,
275
+ report=flow._report,
276
+ node_id=self._node_id,
277
+ call_index=call_number)
278
+ else:
279
+ val = self._func(**dic) # 调用函数
280
+ assert not isinstance(val, Generator)
281
+ ret = val
282
+ except NodeTerminated:
283
+ self.status = NodeStatus.ERROR
284
+ self._report_status(flow, NodeStatus.ERROR, dic)
285
+ if flow._reporter is not None:
286
+ flow._reporter.report_node(flow._workflow_id, self._node_id,
287
+ "terminated", dic)
288
+ return
289
+ except WorkflowAborted:
290
+ self.status = NodeStatus.ERROR
291
+ self._report_status(flow, NodeStatus.ERROR, dic)
292
+ return
293
+ except Exception as e:
294
+ e.add_note("In node: " + str(self._node_id))
295
+ setattr(e, "node_id", self._node_id)
296
+ self.status = NodeStatus.ERROR
297
+ flow._result[self._node_id] = ""
298
+ flow._error[self._node_id] = e
299
+ # 推送:节点执行失败
300
+ self._report_status(flow, NodeStatus.ERROR, dic)
301
+ return
302
+
303
+ self.status = NodeStatus.DONE
304
+ # 推送:节点执行完成
305
+ self._report_status(flow, NodeStatus.DONE, dic)
306
+
307
+ # 写入节点缓存(描述符含不可序列化成分时由 put_node 自行忽略)
308
+ if flow.cache is not None and node_info is not None:
309
+ descriptor = _describe_node_result(ret)
310
+ if descriptor is not None:
311
+ flow.cache.put_node(node_info, descriptor)
312
+
313
+ # 无返回值
314
+ if ret is None:
315
+ return
316
+ # 返回值处理
317
+ if isinstance(ret, Result):
318
+ flow._result[self._node_id] = ret.result()
319
+ return
320
+
321
+ # 执行流程下一步
322
+ if isinstance(ret, Execute):
323
+ ret = [ret]
324
+ for i in ret:
325
+ i.run(flow)
326
+
327
+ def _replay_cached(self, flow: 'Flow', cached: dict[str, Any]) -> None:
328
+ "Node._replay_cached() 重放缓存的节点结果(终值或下游调度)"
329
+ kind = cached.get("kind")
330
+ if kind == "none":
331
+ return
332
+ if kind == "result":
333
+ flow._result[self._node_id] = cached.get("value")
334
+ return
335
+ if kind == "executes":
336
+ known = {node._node_id: node for node in flow._nodes}
337
+ items = cached.get("items")
338
+ if not isinstance(items, list):
339
+ items = []
340
+ items = cast(list[dict[str, Any]], items)
341
+ for item in items:
342
+ target_id = str(item.get("node_id"))
343
+ target = known.get(target_id)
344
+ if target is None:
345
+ raise ValueError(
346
+ f"Cached target node not found: {target_id}")
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)
353
+ return
354
+ raise ValueError(f"Invalid cached node result kind: {kind!r}")
355
+
356
+ def _create_thread_task(self, flow: 'Flow') -> threading.Thread:
357
+ "Node.thread_tast() 启动线程"
358
+ if (self._thread is not None and self._thread.is_alive()):
359
+ # 线程已启动
360
+ raise RuntimeError("Node thread already started")
361
+
362
+ def _target() -> None:
363
+ if flow._reporter is not None:
364
+ # 子线程内替换内置 print:节点日志以 node_log 事件推送
365
+ bind_node_print(flow._reporter, flow._workflow_id,
366
+ self._node_id)
367
+ try:
368
+ self._run(flow)
369
+ finally:
370
+ unbind_node_print()
371
+
372
+ self._thread = threading.Thread(target=_target, daemon=True)
373
+ self._thread.start()
374
+ _NODE_RUNTIMES[self._node_id] = (self, self._thread, flow)
375
+ return self._thread
376
+
377
+ def clean_cache(self, *val: str) -> None:
378
+ "Node.clean_cache() 清理缓存"
379
+
380
+ with self._value_lock:
381
+ if (len(val) == 0): # 清空所有缓存
382
+ for i in self._func_cache.keys():
383
+ self._func_cache[i] = _empty
384
+ self._func_cache_var_keyword = {}
385
+ self._func_cache_filled_num = 0
386
+
387
+ # 填充默认值
388
+ for i in self._func_default_args.keys():
389
+ self._func_cache[i] = self._func_default_args[i]
390
+ self._func_cache_filled_num += 1
391
+ else:
392
+ for i in val:
393
+ if i in self._func_cache:
394
+ # 标准参数
395
+ if isinstance(self._func_cache[i], _Empty):
396
+ continue
397
+ # 参数已填充
398
+ self._func_cache[i] = _empty
399
+ self._func_cache_filled_num -= 1
400
+ if i in self._func_default_args: # 填充默认值
401
+ self._func_cache[i] = self._func_default_args[i]
402
+ self._func_cache_filled_num += 1
403
+ elif self._func_cache_var_keyword is not None and i in self._func_cache_var_keyword:
404
+ # 可变关键字参数
405
+ del self._func_cache_var_keyword[i]
406
+ else: # 参数未定义
407
+ raise ValueError(f"Unknown argument: {i}")
408
+
409
+
410
+ class Execute:
411
+ "Execute 执行对象"
412
+
413
+ def __init__(self, node: Node, args: dict[str, Any]) -> None:
414
+ self._node = node
415
+ self._args = args
416
+
417
+ def run(self, flow: 'Flow') -> None:
418
+ "Execute.run() 执行节点"
419
+
420
+ self._node._fill_args(**self._args)
421
+ if self._node._check_all_filled():
422
+ # 所有参数已填充,执行节点
423
+ flow._taskque.put(self._node)
424
+
425
+
426
+ type Agents = (
427
+ MultiAgent |
428
+ Agent
429
+ ) # Agents 子 Agent 对象
430
+ type NodeResult = (
431
+ Execute |
432
+ list[Execute] |
433
+ tuple[Execute, ...] |
434
+ Result |
435
+ None
436
+ ) # Nodes 节点返回对象
437
+ type TaskGenerator = (Generator[
438
+ Agents,
439
+ StructureOutput | str,
440
+ NodeResult
441
+ ]) # TaskGenerator 任务生成器
442
+ type TaskGen = (Callable[
443
+ ...,
444
+ TaskGenerator
445
+ ] | Callable[
446
+ ...,
447
+ NodeResult
448
+ ]) # TaskGen 任务生成函数
449
+
450
+
451
+ class NodeTerminated(BaseException):
452
+ """stdin 命令终止正在运行的节点。"""
453
+
454
+
455
+ class WorkflowAborted(BaseException):
456
+ """stdin 命令终止整个工作流。"""
457
+
458
+
459
+ _NODE_RUNTIMES: dict[str, tuple[Node, threading.Thread, "Flow"]] = {}
460
+
461
+
462
+ class Flow:
463
+ "Flow 工作流对象"
464
+
465
+ def __init__(self, workflow_id: str, /, *, agent: AgentClientInterface | None = None,
466
+ cache: bool = True, report: bool | None = None) -> None:
467
+ if agent is None:
468
+ agent = gen_conn_info_from_env()
469
+ self._workflow_id = workflow_id
470
+ self._nodes: list[Node] = []
471
+ self._taskque: queue.Queue[Node] = queue.Queue()
472
+ self._result: dict[str, Any] = {}
473
+ self._error: dict[str, Exception | None] = {}
474
+ self._agent = agent
475
+ self._freeze = False
476
+ self._graph: Graph | None = None
477
+ self._entries: list[Node] = []
478
+ # === 工作流缓存(.alkaid0/workflow.db,cache=False 可关闭) ===
479
+ self.cache: WorkflowCache | None = (
480
+ WorkflowCache(default_db_path()) if cache else None)
481
+ self._session_scope: str = "" # ALKAID0_WORKFLOW_SESSION_ID 作用域
482
+ # === stdio JSONL 状态上报(report=True 或 ALKAID0_WORKFLOW_REPORT=1 开启) ===
483
+ if report is None:
484
+ report = os.environ.get("ALKAID0_WORKFLOW_REPORT") == "1"
485
+ self._report: bool = report
486
+ self._reporter = optional_reporter(report)
487
+ self._abort = False
488
+
489
+ def node(self, node_name: str, /) -> Callable[[TaskGen], Node]:
490
+ "@Flow.node() 新增节点"
491
+
492
+ if self._freeze:
493
+ raise RuntimeError("Flow is frozen")
494
+
495
+ def wrapper(func: TaskGen) -> Node:
496
+ "Flow.node().wrapper() 节点包装器"
497
+
498
+ if self._freeze:
499
+ raise RuntimeError("Flow is frozen")
500
+
501
+ # 包装函数
502
+ funcname: str = func.__name__
503
+ node = Node(node_name=node_name, func=func, node_id=funcname)
504
+ self._nodes.append(node)
505
+ return node
506
+ return wrapper
507
+
508
+ def _loop(self) -> None:
509
+ "Flow._loop() 循环执行任务队列"
510
+
511
+ threads: list[threading.Thread] = []
512
+ while True:
513
+ try:
514
+ node = self._taskque.get(False) # 取队列
515
+ except queue.Empty: # 队列为空
516
+ alive = any(t.is_alive() for t in threads)
517
+ if not alive or self._abort:
518
+ break
519
+ time.sleep(1)
520
+ continue
521
+ if not self._abort:
522
+ threads.append(node._create_thread_task(self))
523
+
524
+ def _generate_graph_edges(self) -> dict[str, set[str]]:
525
+ "Node._generate_graph_edges() 生成流程图图边"
526
+
527
+ edges: dict[str, set[str]] = {} # 边集
528
+ known_node_ids = {
529
+ node._node_id for node in self._nodes} # 已知节点 ID,用于查找
530
+ for nodex in self._nodes: # 遍历节点
531
+ func = nodex._func
532
+ try: # 获取函数源码
533
+ src = inspect.getsource(func)
534
+ except OSError:
535
+ raise ValueError("Function source not found")
536
+ try: # 解析源码
537
+ tree = ast.parse(textwrap.dedent(src))
538
+ except SyntaxError:
539
+ raise ValueError("Function source syntax error")
540
+
541
+ # 取函数体
542
+ func_def = None
543
+ for ast_node in ast.walk(tree):
544
+ if isinstance(ast_node, ast.FunctionDef) and ast_node.name == nodex._func.__name__:
545
+ func_def = ast_node
546
+ break
547
+ if func_def is None:
548
+ raise ValueError("Function definition not found")
549
+
550
+ # 查找函数调用
551
+ referenced: set[str] = set()
552
+
553
+ class CallCollector(ast.NodeVisitor):
554
+ "Node._generate_graph_edges.CallCollector 调用收集器"
555
+
556
+ def visit_Call(self, node: ast.Call) -> Any:
557
+ "Node._generate_graph_edges.CallCollector.visit_Call() 遍历调用者"
558
+
559
+ # 查找调用者
560
+ if isinstance(node.func, ast.Name):
561
+ if node.func.id in known_node_ids:
562
+ referenced.add(node.func.id)
563
+
564
+ # 递归遍历参数及内部表达式
565
+ self.generic_visit(node)
566
+
567
+ # 启动收集
568
+ CallCollector().visit(func_def)
569
+
570
+ # 添加边
571
+ edges[nodex._node_id] = referenced
572
+
573
+ return edges
574
+
575
+ def _generate_graph(self, tasks: tuple[Execute, ...]) -> Graph:
576
+ "Node._generate_graph() 生成流程图"
577
+
578
+ edges = self._generate_graph_edges()
579
+ return {
580
+ "nodes": {
581
+ node._node_id: { # 节点信息
582
+ "name": node._node_id,
583
+ } for node in self._nodes
584
+ },
585
+ "edges": edges,
586
+ "start": {
587
+ i._node._node_id
588
+ for i in tasks
589
+ if i._node._check_all_filled() # 检查所有参数是否填充,可以正常执行
590
+ }
591
+ }
592
+
593
+ def _raise_exceptions(self) -> None:
594
+ "Flow._raise_exceptions() 抛出异常"
595
+ exceptions: list[Exception] = [
596
+ i for i in self._error.values() if i is not None]
597
+ if len(exceptions) > 0:
598
+ raise ExceptionGroup(
599
+ f"{len(exceptions)} node(s) run failed", exceptions)
600
+
601
+ def execute(self, *tasks: Execute) -> None:
602
+ "Flow.execute() 执行工作流"
603
+
604
+ # 每次执行时读取会话作用域,作为缓存联合键的组成部分
605
+ self._session_scope = os.environ.get("ALKAID0_WORKFLOW_SESSION_ID", "")
606
+ self._freeze = True
607
+ for i in tasks: # 填充参数
608
+ i.run(self)
609
+ self._graph = self._generate_graph(tasks)
610
+ if self._reporter is not None:
611
+ # 上报本次生成的流程图(JSONL,包裹 bracketed paste 标记)
612
+ self._reporter.report_graph(self._workflow_id, self._graph)
613
+ self._loop()
614
+ self._raise_exceptions()
615
+
616
+ def run(self, *tasks: Execute) -> NoReturn:
617
+ """Node.run() 在 fork 子进程执行工作流,主进程监听 stdin JSONL。"""
618
+ self._report = True
619
+ if self._reporter is None:
620
+ self._reporter = optional_reporter(True)
621
+ if self._agent is None:
622
+ raise ValueError("No availble agent found!")
623
+ control: Any = multiprocessing.get_context("fork").Queue()
624
+ process = multiprocessing.get_context("fork").Process(
625
+ target=self._execute_child, args=(tasks, control))
626
+ process.start()
627
+ stdin_done = threading.Event()
628
+
629
+ def read_stdin() -> None:
630
+ try:
631
+ for line in sys.stdin:
632
+ try:
633
+ command = json.loads(line)
634
+ except json.JSONDecodeError:
635
+ continue
636
+ if isinstance(command, dict):
637
+ command = cast(dict[str, str], command)
638
+ control.put(command)
639
+ if command.get("cmd") == "shutdown":
640
+ return
641
+ finally:
642
+ stdin_done.set()
643
+
644
+ # Reading stdin can block forever, so keep it off the main thread. The
645
+ # main thread can then observe a worker crash or normal exit and stop.
646
+ threading.Thread(target=read_stdin, daemon=True).start()
647
+ try:
648
+ while process.is_alive() and not stdin_done.is_set():
649
+ process.join(0.1)
650
+ finally:
651
+ if process.is_alive():
652
+ control.put({"cmd": "shutdown"})
653
+ process.join(10)
654
+ if process.is_alive():
655
+ process.terminate()
656
+ process.join(2)
657
+ # Preserve a non-zero exit status when the child failed unexpectedly.
658
+ sys.exit(process.exitcode if process.exitcode is not None else 0)
659
+
660
+ def _execute_child(self, tasks: tuple[Execute, ...], control: Any) -> None:
661
+ def listen() -> None:
662
+ while True:
663
+ command = control.get()
664
+ if isinstance(command, dict):
665
+ command = cast(dict[str, str], command)
666
+ self._handle_command(command)
667
+ if command.get("cmd") == "shutdown":
668
+ return
669
+ threading.Thread(target=listen, daemon=True).start()
670
+ self.execute(*tasks)
671
+
672
+ def _handle_command(self, command: dict[str, Any]) -> None:
673
+ cmd = command.get("cmd")
674
+ if cmd == "shutdown":
675
+ self._abort = True
676
+ for node, thread, _ in list(_NODE_RUNTIMES.values()):
677
+ if thread.is_alive() and thread.ident is not None:
678
+ raise_in_thread(thread.ident, WorkflowAborted)
679
+ return
680
+ if cmd == "agent":
681
+ group = find_agent_group(str(command.get("nodeId", "")),
682
+ int(command.get("callIndex", 0)))
683
+ if group is None:
684
+ return
685
+ index = int(command.get("agentIndex", 0)) - 1
686
+ if command.get("action") == "terminate":
687
+ group.terminate(index)
688
+ elif command.get("action") == "retry":
689
+ group.retry(index)
690
+ return
691
+ if cmd == "node":
692
+ runtime = _NODE_RUNTIMES.get(str(command.get("nodeId", "")))
693
+ if runtime is None:
694
+ return
695
+ node, thread, flow = runtime
696
+ if not thread.is_alive() or thread.ident is None:
697
+ return
698
+ raise_in_thread(thread.ident, NodeTerminated)
699
+ if command.get("action") == "restart":
700
+ thread.join(2)
701
+ if not thread.is_alive():
702
+ node._create_thread_task(flow)
dynworkflow/py.typed ADDED
File without changes