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/__init__.py +19 -0
- dynworkflow/agent.py +1022 -0
- dynworkflow/cache.py +292 -0
- dynworkflow/flow.py +702 -0
- dynworkflow/py.typed +0 -0
- dynworkflow/reporter.py +250 -0
- dynworkflow/schemas.py +88 -0
- dynworkflow-0.1.0.dist-info/METADATA +93 -0
- dynworkflow-0.1.0.dist-info/RECORD +10 -0
- dynworkflow-0.1.0.dist-info/WHEEL +4 -0
dynworkflow/reporter.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""基于 stdio 输出的 JSONL 状态上报。
|
|
3
|
+
|
|
4
|
+
工作流运行期间把四类事件以 JSONL(每行一个 JSON 对象)写到 stdout:
|
|
5
|
+
|
|
6
|
+
- ``graph``:``Flow.execute`` 生成的流程图(nodes/edges/start);
|
|
7
|
+
- ``node``:节点状态变更(running/done/error,含全部已填充参数,
|
|
8
|
+
缓存命中同样推送并以 ``cached`` 标记);
|
|
9
|
+
- ``agents_start``:节点即将启动 Agent/MultiAgent(含本次调用的
|
|
10
|
+
序号、prompt 列表);
|
|
11
|
+
- ``agent``:子 Agent 的实时状态(waiting/running/success/failure、
|
|
12
|
+
工具调用列表、session id、所属节点与序号等),每次状态变化立即上报;
|
|
13
|
+
- ``node_code``:节点启动时广播其源码;
|
|
14
|
+
- ``node_log``:节点线程内 ``print`` 的输出转发(替换内置 print 实现,按 ``\n`` 成行缓冲,正确处理多参数、sep、end 与 flush)。
|
|
15
|
+
|
|
16
|
+
每次输出 JSONL 时,先写入 ``\\x1b[?2004h``、结尾写入 ``\\x1b[?2004l``,
|
|
17
|
+
把整块上报输出包裹为宿主可识别的区段。上报过程绝不抛出异常干扰工作流执行。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import builtins
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
import threading
|
|
26
|
+
from collections.abc import Iterable
|
|
27
|
+
from datetime import datetime, timezone
|
|
28
|
+
from typing import TYPE_CHECKING, Any, TextIO
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from .agent import AgentStatusSingleAgent
|
|
32
|
+
from .flow import Graph
|
|
33
|
+
|
|
34
|
+
BRACKETED_PASTE_ENABLE = "\x1b[?2004h" # JSONL 输出块起始标记
|
|
35
|
+
BRACKETED_PASTE_DISABLE = "\x1b[?2004l" # JSONL 输出块结束标记
|
|
36
|
+
|
|
37
|
+
type ReportEvent = dict[str, Any] # 单条上报事件
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _utc_now() -> str:
|
|
41
|
+
"_utc_now() 当前 UTC 时间的 ISO8601 字符串"
|
|
42
|
+
return datetime.now(timezone.utc).isoformat()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class StatusReporter:
|
|
46
|
+
"""StatusReporter 把生成的图与 Agent 实时状态以 JSONL 上报到 stdout。"""
|
|
47
|
+
|
|
48
|
+
# 类级写锁:不同实例(例如并发子 Agent 各自创建的实例)写出互不交错
|
|
49
|
+
_write_lock = threading.Lock()
|
|
50
|
+
|
|
51
|
+
def __init__(self, stream: TextIO | None = None, *,
|
|
52
|
+
enabled: bool = True) -> None:
|
|
53
|
+
self.stream: TextIO = sys.stdout if stream is None else stream
|
|
54
|
+
self.enabled = enabled
|
|
55
|
+
|
|
56
|
+
def emit(self, events: Iterable[ReportEvent]) -> None:
|
|
57
|
+
"""emit() 把一批事件序列化为 JSONL,整体包裹起止标记后写出到 stdout。"""
|
|
58
|
+
if not self.enabled:
|
|
59
|
+
return
|
|
60
|
+
payload = "".join(
|
|
61
|
+
json.dumps(event, ensure_ascii=False, default=str) + "\n"
|
|
62
|
+
for event in events)
|
|
63
|
+
if not payload:
|
|
64
|
+
return
|
|
65
|
+
with StatusReporter._write_lock:
|
|
66
|
+
try:
|
|
67
|
+
self.stream.write(BRACKETED_PASTE_ENABLE)
|
|
68
|
+
self.stream.write(payload)
|
|
69
|
+
self.stream.write(BRACKETED_PASTE_DISABLE)
|
|
70
|
+
self.stream.flush()
|
|
71
|
+
except Exception:
|
|
72
|
+
# 状态上报不得干扰工作流执行(如管道关闭时的 BrokenPipe)。
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
def bind(self, status: AgentStatusSingleAgent) -> None:
|
|
76
|
+
"""bind() 订阅单个 Agent 状态对象:之后每次状态变化都会自动上报。"""
|
|
77
|
+
status.on_update = self.report_agent
|
|
78
|
+
self.report_agent(status) # 先上报一次初始 waiting 状态
|
|
79
|
+
|
|
80
|
+
def report_agent(self, status: AgentStatusSingleAgent) -> None:
|
|
81
|
+
"""report_agent() 上报一次子 Agent 的当前完整快照。"""
|
|
82
|
+
self.emit([{
|
|
83
|
+
"type": "agent",
|
|
84
|
+
"time": _utc_now(),
|
|
85
|
+
"prompt": status.prompt,
|
|
86
|
+
"path": status.path,
|
|
87
|
+
"state": status.status.value,
|
|
88
|
+
"sessionId": status.session_id,
|
|
89
|
+
"nodeId": status.node_id,
|
|
90
|
+
"callIndex": status.call_index,
|
|
91
|
+
"agentIndex": status.agent_index,
|
|
92
|
+
"agentCount": status.agent_count,
|
|
93
|
+
"attempt": status.attempt,
|
|
94
|
+
"tools": [dict(tool) for tool in list(status.tool_calls)],
|
|
95
|
+
}])
|
|
96
|
+
|
|
97
|
+
def report_node(self, workflow_id: str, node_id: str, state: str,
|
|
98
|
+
args: dict[str, Any], *, cached: bool = False) -> None:
|
|
99
|
+
"""report_node() 上报节点状态变更(含全部已填充参数)。"""
|
|
100
|
+
self.emit([{
|
|
101
|
+
"type": "node",
|
|
102
|
+
"time": _utc_now(),
|
|
103
|
+
"workflow": workflow_id,
|
|
104
|
+
"nodeId": node_id,
|
|
105
|
+
"state": state,
|
|
106
|
+
"cached": cached,
|
|
107
|
+
"args": dict(args),
|
|
108
|
+
}])
|
|
109
|
+
|
|
110
|
+
def report_agents_start(self, workflow_id: str, node_id: str,
|
|
111
|
+
call_index: int, agents: Any) -> None:
|
|
112
|
+
"""report_agents_start() 在节点即将启动 Agent/MultiAgent 前上报。
|
|
113
|
+
|
|
114
|
+
``agents`` 为 Agent 或 MultiAgent(鸭子类型,取其 prompt(s))。
|
|
115
|
+
"""
|
|
116
|
+
prompts: list[str] = []
|
|
117
|
+
single = getattr(agents, "prompt", None)
|
|
118
|
+
if isinstance(single, str):
|
|
119
|
+
prompts.append(single)
|
|
120
|
+
group = getattr(agents, "agents", None)
|
|
121
|
+
if isinstance(group, (list, tuple)):
|
|
122
|
+
for item in group:
|
|
123
|
+
value = getattr(item, "prompt", None)
|
|
124
|
+
if isinstance(value, str):
|
|
125
|
+
prompts.append(value)
|
|
126
|
+
self.emit([{
|
|
127
|
+
"type": "agents_start",
|
|
128
|
+
"time": _utc_now(),
|
|
129
|
+
"workflow": workflow_id,
|
|
130
|
+
"nodeId": node_id,
|
|
131
|
+
"callIndex": call_index,
|
|
132
|
+
"count": len(prompts),
|
|
133
|
+
"prompts": prompts,
|
|
134
|
+
}])
|
|
135
|
+
|
|
136
|
+
def report_node_log(self, workflow_id: str, node_id: str,
|
|
137
|
+
message: str) -> None:
|
|
138
|
+
"""report_node_log() 把节点线程内 print 的输出转发为 node_log 事件。"""
|
|
139
|
+
self.emit([{
|
|
140
|
+
"type": "node_log",
|
|
141
|
+
"time": _utc_now(),
|
|
142
|
+
"workflow": workflow_id,
|
|
143
|
+
"nodeId": node_id,
|
|
144
|
+
"message": message,
|
|
145
|
+
}])
|
|
146
|
+
|
|
147
|
+
def report_node_code(self, workflow_id: str, node_id: str, name: str,
|
|
148
|
+
code: str | None) -> None:
|
|
149
|
+
"""report_node_code() 在节点启动时广播其源码(不可得时为 null)。"""
|
|
150
|
+
self.emit([{
|
|
151
|
+
"type": "node_code",
|
|
152
|
+
"time": _utc_now(),
|
|
153
|
+
"workflow": workflow_id,
|
|
154
|
+
"nodeId": node_id,
|
|
155
|
+
"name": name,
|
|
156
|
+
"code": code,
|
|
157
|
+
}])
|
|
158
|
+
|
|
159
|
+
def report_graph(self, workflow_id: str, graph: Graph) -> None:
|
|
160
|
+
"""report_graph() 上报 Flow 生成的流程图(set 序列化为有序列表)。"""
|
|
161
|
+
self.emit([{
|
|
162
|
+
"type": "graph",
|
|
163
|
+
"time": _utc_now(),
|
|
164
|
+
"workflow": workflow_id,
|
|
165
|
+
"graph": {
|
|
166
|
+
"nodes": dict(graph["nodes"]),
|
|
167
|
+
"edges": {node_id: sorted(targets)
|
|
168
|
+
for node_id, targets in graph["edges"].items()},
|
|
169
|
+
"start": sorted(graph["start"]),
|
|
170
|
+
},
|
|
171
|
+
}])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def optional_reporter(enabled: bool) -> StatusReporter | None:
|
|
175
|
+
"""optional_reporter() 按布尔开关创建默认 stdout 上报器,关闭时返回 None。"""
|
|
176
|
+
return StatusReporter() if enabled else None
|
|
177
|
+
|
|
178
|
+
# === 节点线程 print 钩子:节点内 print 转为 node_log,其余线程照常 ===
|
|
179
|
+
# 语义与真实 stdout 对齐:sep 连接多参数后追加 end;按 \n 成行推送,
|
|
180
|
+
# 未成行的片段在线程结束或 flush=True 时补发为一条事件。
|
|
181
|
+
|
|
182
|
+
_REAL_PRINT = print # 替换前捕获的原始内置 print
|
|
183
|
+
_NODE_PRINT_CONTEXTS: dict[int, tuple[StatusReporter, str, str]] = {}
|
|
184
|
+
_NODE_PRINT_BUFFERS: dict[int, list[str]] = {} # 每线程待续写的未成行片段
|
|
185
|
+
_NODE_PRINT_LOCK = threading.Lock()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _node_print(*args: Any, sep: str = " ", end: str = "\n",
|
|
189
|
+
file: Any = None, flush: bool = False) -> None:
|
|
190
|
+
"_node_print() 内置 print 的替身:节点线程内转为 node_log 事件"
|
|
191
|
+
tid = threading.get_ident()
|
|
192
|
+
context = _NODE_PRINT_CONTEXTS.get(tid)
|
|
193
|
+
if context is None or (file is not None and file is not sys.stdout):
|
|
194
|
+
# 非节点线程 / 显式重定向到其他流时保持原生行为
|
|
195
|
+
_REAL_PRINT(*args, sep=sep, end=end, file=file, flush=flush)
|
|
196
|
+
return
|
|
197
|
+
reporter, workflow_id, node_id = context
|
|
198
|
+
# 与内置 print 一致:str() 逐个转换参数,用 sep 连接,末尾追加 end
|
|
199
|
+
buffer = _NODE_PRINT_BUFFERS.setdefault(tid, [])
|
|
200
|
+
buffer.append(sep.join(str(arg) for arg in args) + end)
|
|
201
|
+
data = "".join(buffer)
|
|
202
|
+
*lines, pending = data.split("\n")
|
|
203
|
+
for line in lines:
|
|
204
|
+
reporter.report_node_log(workflow_id, node_id, line)
|
|
205
|
+
buffer.clear()
|
|
206
|
+
if pending:
|
|
207
|
+
buffer.append(pending)
|
|
208
|
+
if flush and pending:
|
|
209
|
+
# flush=True:尚未成行的残段立即作为一条事件发出
|
|
210
|
+
reporter.report_node_log(workflow_id, node_id, pending)
|
|
211
|
+
buffer.clear()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _flush_node_print_buffer(reporter: StatusReporter, workflow_id: str,
|
|
215
|
+
node_id: str, fragments: list[str]) -> None:
|
|
216
|
+
"_flush_node_print_buffer() 把未成行的残段补发为一条 node_log 事件"
|
|
217
|
+
text = "".join(fragments)
|
|
218
|
+
if text:
|
|
219
|
+
reporter.report_node_log(workflow_id, node_id, text)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def bind_node_print(reporter: StatusReporter, workflow_id: str,
|
|
223
|
+
node_id: str) -> None:
|
|
224
|
+
"""bind_node_print() 注册当前线程:该线程内的 print 转为 node_log。
|
|
225
|
+
|
|
226
|
+
进程内首个注册会整体替换 ``builtins.print``;未注册线程的 print
|
|
227
|
+
原样透传,行为不变。
|
|
228
|
+
"""
|
|
229
|
+
with _NODE_PRINT_LOCK:
|
|
230
|
+
if not _NODE_PRINT_CONTEXTS:
|
|
231
|
+
setattr(builtins, "print", _node_print)
|
|
232
|
+
_NODE_PRINT_CONTEXTS[threading.get_ident()] = (
|
|
233
|
+
reporter, workflow_id, node_id)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def unbind_node_print() -> None:
|
|
237
|
+
"""unbind_node_print() 解除当前线程注册;全部解除后还原内置 print。
|
|
238
|
+
|
|
239
|
+
解除前把该线程尚未成行的输出残段补发为最后一条 node_log,
|
|
240
|
+
保证 ``end=""`` 之类不完整输出不丢失。
|
|
241
|
+
"""
|
|
242
|
+
tid = threading.get_ident()
|
|
243
|
+
with _NODE_PRINT_LOCK:
|
|
244
|
+
context = _NODE_PRINT_CONTEXTS.pop(tid, None)
|
|
245
|
+
fragments = _NODE_PRINT_BUFFERS.pop(tid, [])
|
|
246
|
+
if not _NODE_PRINT_CONTEXTS:
|
|
247
|
+
setattr(builtins, "print", _REAL_PRINT)
|
|
248
|
+
if context is not None:
|
|
249
|
+
_flush_node_print_buffer(context[0], context[1], context[2],
|
|
250
|
+
fragments)
|
dynworkflow/schemas.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""ACP v2 客户端使用的类型化 JSON schema 定义。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal, NotRequired, TypeAlias, TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
JsonPrimitive: TypeAlias = None | bool | int | float | str
|
|
9
|
+
JsonValue: TypeAlias = JsonPrimitive | list["JsonValue"] | dict[str, "JsonValue"]
|
|
10
|
+
JsonObject: TypeAlias = dict[str, JsonValue]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JsonRpcError(TypedDict):
|
|
14
|
+
code: int
|
|
15
|
+
message: str
|
|
16
|
+
data: NotRequired[JsonValue]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class JsonRpcMessage(TypedDict, total=False):
|
|
20
|
+
jsonrpc: Literal["2.0"]
|
|
21
|
+
id: int | str | None
|
|
22
|
+
method: str
|
|
23
|
+
params: JsonObject
|
|
24
|
+
result: JsonValue
|
|
25
|
+
error: JsonRpcError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ClientInfo(TypedDict):
|
|
29
|
+
name: str
|
|
30
|
+
title: NotRequired[str]
|
|
31
|
+
version: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class InitializeParams(TypedDict):
|
|
35
|
+
protocolVersion: int
|
|
36
|
+
capabilities: JsonObject
|
|
37
|
+
info: ClientInfo
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class InitializeResult(TypedDict):
|
|
41
|
+
protocolVersion: int
|
|
42
|
+
capabilities: JsonObject
|
|
43
|
+
info: JsonObject
|
|
44
|
+
authMethods: NotRequired[list[JsonObject]]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TextContentBlock(TypedDict):
|
|
48
|
+
type: Literal["text"]
|
|
49
|
+
text: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SessionNewParams(TypedDict):
|
|
53
|
+
cwd: str
|
|
54
|
+
mcpServers: list[JsonObject]
|
|
55
|
+
model: NotRequired[str]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SessionResumeParams(SessionNewParams):
|
|
59
|
+
sessionId: str
|
|
60
|
+
replayFrom: NotRequired[JsonObject]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SessionResult(TypedDict):
|
|
64
|
+
sessionId: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PromptParams(TypedDict):
|
|
68
|
+
sessionId: str
|
|
69
|
+
prompt: list[TextContentBlock]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SessionUpdate(TypedDict, total=False):
|
|
73
|
+
sessionUpdate: str
|
|
74
|
+
messageId: str
|
|
75
|
+
content: JsonValue
|
|
76
|
+
state: str
|
|
77
|
+
stopReason: str
|
|
78
|
+
toolCallId: str
|
|
79
|
+
title: str
|
|
80
|
+
kind: str
|
|
81
|
+
status: str
|
|
82
|
+
rawInput: JsonValue
|
|
83
|
+
rawOutput: JsonValue
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class SessionUpdateParams(TypedDict):
|
|
87
|
+
sessionId: str
|
|
88
|
+
update: SessionUpdate
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: dynworkflow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dynamic Workflow Library for Alkaid0
|
|
5
|
+
Author: cxykevin
|
|
6
|
+
Author-email: cxykevin <cxykevin@yeah.net>
|
|
7
|
+
Requires-Dist: websockets>=15.0
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# dynworkflow
|
|
12
|
+
|
|
13
|
+
`dynworkflow` 是一个 Python 3.12 库,用于编排动态工作流,工作流节点可以调用一个或多个 ACP Agent。
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
使用项目包管理器安装:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv sync
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
运行时依赖为 `websockets`,开发依赖包括 `pytest` 和 `mypy`。
|
|
24
|
+
|
|
25
|
+
## 快速开始
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import dynworkflow as d
|
|
29
|
+
|
|
30
|
+
flow = d.Flow(
|
|
31
|
+
"document-flow",
|
|
32
|
+
agent=d.AgentClient("ws://127.0.0.1:7433/acp?k=a"),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@flow.node("summarize")
|
|
36
|
+
def summarize() -> d.TaskGenerator:
|
|
37
|
+
result = yield d.Agent("Summarize the documents in docs/test", path="docs/test")
|
|
38
|
+
return d.Result(result)
|
|
39
|
+
|
|
40
|
+
flow.execute(summarize())
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
使用 `@flow.node(name)` 声明节点。普通节点可以返回 `None`、`Result` 或一个或多个下游 `Execute` 任务。生成器节点可以 `yield Agent` 或 `MultiAgent`,接收 Agent 结果后返回节点结果。
|
|
44
|
+
|
|
45
|
+
## 公共 API
|
|
46
|
+
|
|
47
|
+
- `Flow`:创建并执行工作流。
|
|
48
|
+
- `Flow.node(name)`:注册节点函数。
|
|
49
|
+
- `Flow.execute(*tasks)`:在当前进程中执行任务,并在所有节点结束后抛出节点异常。
|
|
50
|
+
- `Flow.run(*tasks)`:在 fork 子进程中运行工作流,并监听 stdin 中的 JSONL 控制命令。
|
|
51
|
+
- `Agent`:描述一次 Agent 请求。
|
|
52
|
+
- `MultiAgent`:并发运行多个 Agent 请求。
|
|
53
|
+
- `AgentClient`:连接 ACP WebSocket 端点。
|
|
54
|
+
- `Result`:保存节点的最终结果。
|
|
55
|
+
|
|
56
|
+
## 执行语义
|
|
57
|
+
|
|
58
|
+
- 相互独立的节点会并发运行。
|
|
59
|
+
- 节点失败后会立即变为 `error` 状态,并在启用上报时立即发送状态。
|
|
60
|
+
- 失败节点不会停止兄弟节点。
|
|
61
|
+
- 所有节点异常会在最后统一以 `ExceptionGroup` 抛出。
|
|
62
|
+
- 节点和 Agent 的终止命令只影响指定目标;工作流关闭会终止整个工作流。
|
|
63
|
+
|
|
64
|
+
## 缓存与状态上报
|
|
65
|
+
|
|
66
|
+
缓存默认启用。可以使用 `Flow(..., cache=False)` 关闭缓存。缓存文件位于 `.alkaid0/workflow.db`;设置 `ALKAID0_WORKFLOW_SESSION_ID` 可以隔离不同会话。
|
|
67
|
+
|
|
68
|
+
可以通过 `Flow(..., report=True)` 或环境变量 `ALKAID0_WORKFLOW_REPORT=1` 开启 JSONL 状态上报。`Flow.run()` 会自动开启上报。事件包括 `graph`、`node`、`agent`、`agents_start`、`node_code` 和 `node_log`。
|
|
69
|
+
|
|
70
|
+
## 示例
|
|
71
|
+
|
|
72
|
+
参见 `example.py`,其中包含使用 `AgentClient`、`Agent`、`MultiAgent`、分支和回退参数的完整工作流示例。
|
|
73
|
+
|
|
74
|
+
## 测试
|
|
75
|
+
|
|
76
|
+
运行测试套件:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python -m pytest -q
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
运行静态编译检查:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python -m compileall -q src tests
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 文档索引
|
|
89
|
+
|
|
90
|
+
- `docs/SKILL.md`:仅供 AI Agent 读取的使用说明,保持英文。
|
|
91
|
+
- `docs/architecture.md`:面向人的实现与生命周期说明。
|
|
92
|
+
- `docs/protocol.md`:stdin 控制命令和 JSONL 上报事件说明。
|
|
93
|
+
- `docs/test/ai_must_read.md`:示例工作流读取的测试文档。
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
dynworkflow/__init__.py,sha256=s9TjO6XUkcAZDxJEv0s7WVP9MhGBKrHuWRpgxxmtvbY,317
|
|
2
|
+
dynworkflow/agent.py,sha256=7FmxKSNYNIwljZ-IzxRgbpIhCSErXtvazEAX-JLVSjY,42600
|
|
3
|
+
dynworkflow/cache.py,sha256=W9GLvLq1wMRcq0hIMVk5BO5FIuzaXcRluckvR5XQCdk,10412
|
|
4
|
+
dynworkflow/flow.py,sha256=rEbm8HmZRMCLwE9_QW1_Vq2ei3JW0cXct5UlwtnRTgo,27861
|
|
5
|
+
dynworkflow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
dynworkflow/reporter.py,sha256=5zX7UU0nXj1vpx2RR5R_OgLh9nROCdJUM6Wx2zVXgxQ,10277
|
|
7
|
+
dynworkflow/schemas.py,sha256=-if9rPA1fDZ7wcPGK4Rr9TDOyUxS9O2xLOpPPAgtNXo,1774
|
|
8
|
+
dynworkflow-0.1.0.dist-info/WHEEL,sha256=l3MmIxu8qaet7ng2J9fFoJnYGj8IREj7jXTbgsuzmy4,81
|
|
9
|
+
dynworkflow-0.1.0.dist-info/METADATA,sha256=gdzAfRgWdFBMqXOD_5aVok3mBQDsUEk-ANDMPf_G6hw,3032
|
|
10
|
+
dynworkflow-0.1.0.dist-info/RECORD,,
|