@zhushanwen/pi-subagent-workflow 0.1.0
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.
- package/agents/context-builder.md +17 -0
- package/agents/general-purpose.md +16 -0
- package/agents/oracle.md +17 -0
- package/agents/planner.md +17 -0
- package/agents/researcher.md +17 -0
- package/agents/reviewer.md +17 -0
- package/agents/scout.md +17 -0
- package/agents/worker.md +16 -0
- package/examples/README.md +43 -0
- package/examples/chain.example.js +92 -0
- package/examples/map-reduce.example.js +99 -0
- package/examples/parallel.example.js +82 -0
- package/examples/scatter-gather.example.js +106 -0
- package/index.ts +1 -0
- package/package.json +66 -0
- package/skills/workflow-script-format/SKILL.md +328 -0
- package/src/execution/__tests__/agent-registry.test.ts +164 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
- package/src/execution/__tests__/alive-store.test.ts +147 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
- package/src/execution/__tests__/config.test.ts +110 -0
- package/src/execution/__tests__/crash-recovery.test.ts +311 -0
- package/src/execution/__tests__/execute-nesting.test.ts +359 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
- package/src/execution/__tests__/execution-record.test.ts +959 -0
- package/src/execution/__tests__/finalized-marker.test.ts +82 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
- package/src/execution/__tests__/format.test.ts +320 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
- package/src/execution/__tests__/list-component.test.ts +347 -0
- package/src/execution/__tests__/model-resolver.test.ts +356 -0
- package/src/execution/__tests__/output-collector.test.ts +61 -0
- package/src/execution/__tests__/path-encoding.test.ts +75 -0
- package/src/execution/__tests__/pi-invocation.test.ts +73 -0
- package/src/execution/__tests__/record-store.test.ts +545 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
- package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
- package/src/execution/__tests__/sdk-contract.test.ts +272 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
- package/src/execution/__tests__/session-file-gc.test.ts +247 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
- package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
- package/src/execution/__tests__/spawn-args.test.ts +244 -0
- package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
- package/src/execution/__tests__/subagent-service.test.ts +678 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
- package/src/execution/__tests__/temp-prompt.test.ts +53 -0
- package/src/execution/__tests__/timeout-integration.test.ts +381 -0
- package/src/execution/__tests__/tombstone-store.test.ts +73 -0
- package/src/execution/__tests__/tool-action.test.ts +330 -0
- package/src/execution/__tests__/turn-limiter.test.ts +65 -0
- package/src/execution/__tests__/worktree-manager.test.ts +423 -0
- package/src/execution/__tests__/worktree-registry.test.ts +161 -0
- package/src/execution/agent-registry.ts +252 -0
- package/src/execution/agent-result-mapper.ts +84 -0
- package/src/execution/alive-store.ts +92 -0
- package/src/execution/best-effort.ts +30 -0
- package/src/execution/concurrency-pool.ts +84 -0
- package/src/execution/config.ts +73 -0
- package/src/execution/execute-options-mapper.ts +86 -0
- package/src/execution/execution-record.ts +778 -0
- package/src/execution/finalized-marker.ts +51 -0
- package/src/execution/model-config-service.ts +225 -0
- package/src/execution/model-resolver.ts +247 -0
- package/src/execution/notifier.ts +168 -0
- package/src/execution/output-collector.ts +88 -0
- package/src/execution/path-encoding.ts +34 -0
- package/src/execution/pi-invocation.ts +70 -0
- package/src/execution/record-store.ts +350 -0
- package/src/execution/session-context-resolver.ts +64 -0
- package/src/execution/session-file-gc.ts +98 -0
- package/src/execution/session-reconstructor.ts +450 -0
- package/src/execution/session-runner.ts +725 -0
- package/src/execution/spawn-event-adapter.ts +150 -0
- package/src/execution/subagent-service.ts +973 -0
- package/src/execution/subprocess-agent-runner.ts +108 -0
- package/src/execution/temp-prompt.ts +57 -0
- package/src/execution/tombstone-store.ts +72 -0
- package/src/execution/turn-limiter.ts +88 -0
- package/src/execution/types.ts +634 -0
- package/src/execution/worktree-manager.ts +285 -0
- package/src/execution/worktree-registry.ts +144 -0
- package/src/index.ts +454 -0
- package/src/interface/bg-notify-render.ts +286 -0
- package/src/interface/commands.ts +157 -0
- package/src/interface/format.ts +501 -0
- package/src/interface/gui-adapter.ts +136 -0
- package/src/interface/helpers.ts +110 -0
- package/src/interface/list-component.ts +643 -0
- package/src/interface/list-shared.ts +84 -0
- package/src/interface/list-view.ts +373 -0
- package/src/interface/reentry-guard.ts +30 -0
- package/src/interface/subagent-actions.ts +294 -0
- package/src/interface/subagent-tool.ts +294 -0
- package/src/interface/subagents.ts +30 -0
- package/src/interface/tool-render.ts +333 -0
- package/src/interface/tool-workflow-script.ts +351 -0
- package/src/interface/tool-workflow.ts +485 -0
- package/src/interface/views/WorkflowsView.ts +944 -0
- package/src/interface/views/detail-content.ts +298 -0
- package/src/interface/views/format.ts +320 -0
- package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
- package/src/orchestration/__tests__/config-loader.test.ts +381 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
- package/src/orchestration/__tests__/script-lint.test.ts +347 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
- package/src/orchestration/agent-opts-resolver.ts +128 -0
- package/src/orchestration/concurrency-gate.ts +69 -0
- package/src/orchestration/config-loader.ts +313 -0
- package/src/orchestration/error-recovery.ts +578 -0
- package/src/orchestration/execute-agent-call.ts +174 -0
- package/src/orchestration/jsonl-run-store.ts +292 -0
- package/src/orchestration/launcher.ts +368 -0
- package/src/orchestration/lifecycle.ts +373 -0
- package/src/orchestration/models/__tests__/budget.test.ts +367 -0
- package/src/orchestration/models/agent-call.ts +76 -0
- package/src/orchestration/models/budget.ts +148 -0
- package/src/orchestration/models/ports.ts +165 -0
- package/src/orchestration/models/run-runtime.ts +91 -0
- package/src/orchestration/models/run-spec.ts +54 -0
- package/src/orchestration/models/run-state.ts +44 -0
- package/src/orchestration/models/trace.ts +102 -0
- package/src/orchestration/models/types.ts +242 -0
- package/src/orchestration/models/workflow-run.ts +275 -0
- package/src/orchestration/models/workflow-script-registry.ts +32 -0
- package/src/orchestration/models/workflow-script.ts +90 -0
- package/src/orchestration/node-ops.ts +192 -0
- package/src/orchestration/script-lint.ts +387 -0
- package/src/orchestration/skill-discovery.ts +60 -0
- package/src/orchestration/worker-handle.ts +115 -0
- package/src/orchestration/worker-host.ts +93 -0
- package/src/orchestration/worker-script-builder.ts +281 -0
- package/src/orchestration/workflow-files.ts +85 -0
- package/src/orchestration/workflow-script-registry-impl.ts +128 -0
- package/src/shared/__tests__/resource-discovery.test.ts +226 -0
- package/src/shared/agent-event.ts +13 -0
- package/src/shared/resource-discovery.ts +535 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Extension — Engine 共享类型
|
|
3
|
+
*
|
|
4
|
+
* Engine 层全局基础类型。零 infra 依赖——不 import 任何 infra 文件,
|
|
5
|
+
* 可独立编译测试(D-12 三层架构,AC-1)。
|
|
6
|
+
*
|
|
7
|
+
* 核心内容:
|
|
8
|
+
* - 状态机:RunStatus = "running" | "paused" | "done"(3 态,FR-3)
|
|
9
|
+
* + DoneReason(completed/failed/aborted/budget_limited/time_limited)
|
|
10
|
+
* - AgentCallOpts / AgentResult / AgentUsage(单次 agent 调用的输入/输出)
|
|
11
|
+
* - ExecutionTraceNode / TracePatch / ToolCallEntry / WorkerLogEntry(trace 数据)
|
|
12
|
+
*
|
|
13
|
+
* 层归属:Engine(数据结构 + 不变式守卫)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ExecutionRecord } from "../../execution/types.ts";
|
|
17
|
+
|
|
18
|
+
// ── 状态机 ────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 状态机:3 态(D-12 / FR-3)。
|
|
22
|
+
*
|
|
23
|
+
* running ↔ paused → done
|
|
24
|
+
*
|
|
25
|
+
* `done` 是唯一终态,具体原因由 DoneReason 区分。
|
|
26
|
+
*/
|
|
27
|
+
export type RunStatus = "running" | "paused" | "done";
|
|
28
|
+
|
|
29
|
+
/** 终态原因。done 时必有(WorkflowRun 不变式)。 */
|
|
30
|
+
export type DoneReason =
|
|
31
|
+
| "completed"
|
|
32
|
+
| "failed"
|
|
33
|
+
| "aborted"
|
|
34
|
+
| "budget_limited"
|
|
35
|
+
| "time_limited";
|
|
36
|
+
|
|
37
|
+
/** 合法的状态转换。空数组 = 无出边(done 终态)。 */
|
|
38
|
+
export const VALID_RUN_TRANSITIONS: Record<RunStatus, readonly RunStatus[]> = {
|
|
39
|
+
running: ["paused", "done"] as const,
|
|
40
|
+
paused: ["running", "done"] as const,
|
|
41
|
+
done: [] as const,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const ALL_RUN_STATUSES: readonly RunStatus[] = ["running", "paused", "done"] as const;
|
|
45
|
+
|
|
46
|
+
export const ALL_DONE_REASONS: readonly DoneReason[] = [
|
|
47
|
+
"completed",
|
|
48
|
+
"failed",
|
|
49
|
+
"aborted",
|
|
50
|
+
"budget_limited",
|
|
51
|
+
"time_limited",
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
/** done 为终态,无出边。 */
|
|
55
|
+
export function isDone(status: RunStatus): boolean {
|
|
56
|
+
return status === "done";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function canRunTransition(from: RunStatus, to: RunStatus): boolean {
|
|
60
|
+
return (VALID_RUN_TRANSITIONS[from] as readonly RunStatus[]).includes(to);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Agent 调用 ────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 单次 agent 调用的输入选项。
|
|
67
|
+
*
|
|
68
|
+
* D-12 仅重组执行编排,AgentCallOpts 形状保持兼容。
|
|
69
|
+
*/
|
|
70
|
+
export interface AgentCallOpts {
|
|
71
|
+
/** The task prompt to send to the agent. */
|
|
72
|
+
prompt: string;
|
|
73
|
+
/**
|
|
74
|
+
* Optional JSON schema for structured output.
|
|
75
|
+
* When provided, the schema is passed via PI_WORKFLOW_SCHEMA env to the subprocess,
|
|
76
|
+
* which activates the structured-output tool + turn_end hook.
|
|
77
|
+
* The tool's execute validates model output against the schema.
|
|
78
|
+
* On success, `parsedOutput` on the result is set to `tool_execution_end.result.details`
|
|
79
|
+
* (the validated, parsed data object — not the raw tool call args).
|
|
80
|
+
*/
|
|
81
|
+
schema?: Record<string, unknown>;
|
|
82
|
+
/**
|
|
83
|
+
* Model to use (e.g. "router-openai/glm-5.1").
|
|
84
|
+
* When omitted, pi's default model is used.
|
|
85
|
+
*/
|
|
86
|
+
model?: string;
|
|
87
|
+
/** Scene name for model-switch advisor recommendation. */
|
|
88
|
+
scene?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Wall-clock timeout in milliseconds. When > 0, aborts the subprocess
|
|
91
|
+
* if it runs longer than this, regardless of external signal.
|
|
92
|
+
* Per-call,归 AgentCall 实体(G-027)。
|
|
93
|
+
*/
|
|
94
|
+
timeoutMs?: number;
|
|
95
|
+
/**
|
|
96
|
+
* Skill name to load (e.g. "code-review"). Resolved to SKILL.md path
|
|
97
|
+
* and injected via --skill flag in the subprocess.
|
|
98
|
+
*/
|
|
99
|
+
skill?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Resolved absolute path to the skill directory or SKILL.md file.
|
|
102
|
+
* Set by agent-opts-resolver when opts.skill is present.
|
|
103
|
+
*/
|
|
104
|
+
skillPath?: string;
|
|
105
|
+
/** Human-readable description for logging and debugging. */
|
|
106
|
+
description?: string;
|
|
107
|
+
/**
|
|
108
|
+
* Agent name to resolve from AgentRegistry. When set, the resolved
|
|
109
|
+
* agent's systemPrompt is injected via --append-system-prompt.
|
|
110
|
+
*/
|
|
111
|
+
agent?: string;
|
|
112
|
+
/**
|
|
113
|
+
* Absolute paths to temp files containing system prompt injections.
|
|
114
|
+
* Set by agent-opts-resolver: agent systemPrompt + schema injection files.
|
|
115
|
+
* buildArgs injects each via --append-system-prompt.
|
|
116
|
+
*/
|
|
117
|
+
systemPromptFiles?: string[];
|
|
118
|
+
/**
|
|
119
|
+
* Schema JSON for PI_WORKFLOW_SCHEMA env var.
|
|
120
|
+
* Set by agent-opts-resolver when opts.schema is present; passed as env var
|
|
121
|
+
* to activate the structured-output tool + hook.
|
|
122
|
+
*/
|
|
123
|
+
schemaEnv?: string;
|
|
124
|
+
/**
|
|
125
|
+
* Per-call 工作目录(ADR-029 决策 1)。传给 child_process.spawn 的 cwd option。
|
|
126
|
+
*
|
|
127
|
+
* 用于 worktree 隔离:传入 worktree 绝对路径,spawn 的 pi 子进程绑定到该目录,
|
|
128
|
+
* 其内部的 createAgentSession/ResourceLoader/bash 工具都在该目录运行。
|
|
129
|
+
* undefined 时 spawn 继承 workflow 进程的 cwd(向后兼容)。
|
|
130
|
+
*/
|
|
131
|
+
cwd?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 单次 agent 调用的资源用量(FR-7 跨 turn 累积)。
|
|
136
|
+
*/
|
|
137
|
+
export interface AgentUsage {
|
|
138
|
+
input: number;
|
|
139
|
+
output: number;
|
|
140
|
+
cacheRead: number;
|
|
141
|
+
cacheWrite: number;
|
|
142
|
+
cost: number;
|
|
143
|
+
contextTokens: number;
|
|
144
|
+
turns: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 单次 tool 调用记录(FR-7 从 agent JSONL 流采)。
|
|
149
|
+
*/
|
|
150
|
+
export interface ToolCallEntry {
|
|
151
|
+
/** Tool name. */
|
|
152
|
+
name: string;
|
|
153
|
+
/** Args preview string. */
|
|
154
|
+
input: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 单次 agent 调用的结果(统一形态)。
|
|
159
|
+
*
|
|
160
|
+
* Engine 直接消费 SubprocessAgentRunner 返回值;callCache replay 时 worker
|
|
161
|
+
* 取 parsedOutput ?? content(见 worker-script-builder.ts 消息处理)。
|
|
162
|
+
*/
|
|
163
|
+
export interface AgentResult {
|
|
164
|
+
/** Raw text output from the agent. */
|
|
165
|
+
content: string;
|
|
166
|
+
/**
|
|
167
|
+
* Parsed structured output.
|
|
168
|
+
* Present when `schema` was provided and the output was valid JSON.
|
|
169
|
+
* Source: tool_execution_end.result.details(validated data object)。
|
|
170
|
+
*/
|
|
171
|
+
parsedOutput?: unknown;
|
|
172
|
+
/** Token and cost usage accumulated across all assistant turns. */
|
|
173
|
+
usage?: AgentUsage;
|
|
174
|
+
/** Wall-clock duration in milliseconds. */
|
|
175
|
+
durationMs?: number;
|
|
176
|
+
/** True when the pi process exited with code 0. */
|
|
177
|
+
error?: string;
|
|
178
|
+
/**
|
|
179
|
+
* Pi session ID for the subagent process (uuidv7).
|
|
180
|
+
* Present when pi emits a session header (default in --mode json).
|
|
181
|
+
* Can be used to locate the session JSONL file for post-run inspection (G-017)。
|
|
182
|
+
*/
|
|
183
|
+
sessionId?: string;
|
|
184
|
+
/** All tool calls collected from JSONL stream (FR-7). */
|
|
185
|
+
toolCalls?: ToolCallEntry[];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Trace ─────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 执行追踪节点(事件流 D-10 单一来源)。
|
|
192
|
+
*/
|
|
193
|
+
export interface ExecutionTraceNode {
|
|
194
|
+
stepIndex: number;
|
|
195
|
+
agent: string;
|
|
196
|
+
task: string;
|
|
197
|
+
model: string;
|
|
198
|
+
status: "pending" | "running" | "completed" | "failed";
|
|
199
|
+
/** Phase name for TUI grouping. Set from explicit opts.phase or global _currentPhase. */
|
|
200
|
+
phase?: string;
|
|
201
|
+
startedAt?: string;
|
|
202
|
+
completedAt?: string;
|
|
203
|
+
result?: AgentResult;
|
|
204
|
+
error?: string;
|
|
205
|
+
/**
|
|
206
|
+
* Pi session ID (uuidv7) for the subagent process.
|
|
207
|
+
* Used to locate the session JSONL for post-run inspection.
|
|
208
|
+
*/
|
|
209
|
+
sessionId?: string;
|
|
210
|
+
/**
|
|
211
|
+
* Live 执行进度对象(running 时存在,done 时由 dispatchAgentCall 清除)。
|
|
212
|
+
*
|
|
213
|
+
* 挂在 node 上(D-10 单源延伸:AgentCall.traceNode 与 Trace.nodes 共享同一引用)。
|
|
214
|
+
* TUI 通过 trace.toArray() 读 node.live,派生 getEventLog/getCurrentActivity 实时展示。
|
|
215
|
+
* 不持久化(pause/resume 时为 undefined,重跑时重建)。
|
|
216
|
+
*/
|
|
217
|
+
live?: ExecutionRecord;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Trace.update 用的 patch(字段全可选)。
|
|
222
|
+
*
|
|
223
|
+
* 不变式:只改单个 node 的 status/result/error/completedAt/sessionId。
|
|
224
|
+
* callId 不存在时 update 为 no-op(D-10)。
|
|
225
|
+
*/
|
|
226
|
+
export interface TracePatch {
|
|
227
|
+
status?: "pending" | "running" | "completed" | "failed";
|
|
228
|
+
result?: AgentResult;
|
|
229
|
+
error?: string;
|
|
230
|
+
completedAt?: string;
|
|
231
|
+
sessionId?: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Worker 诊断 ───────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Worker console.* 捕获条目(run 级诊断,仅展示在 TUI widget,不泄漏到 input area)。
|
|
238
|
+
*/
|
|
239
|
+
export interface WorkerLogEntry {
|
|
240
|
+
level: "log" | "warn" | "error" | "info";
|
|
241
|
+
message: string;
|
|
242
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Extension — WorkflowRun
|
|
3
|
+
*
|
|
4
|
+
* 单次 workflow run 的聚合根。封装状态机 + runtime 生命周期 + 不变式守卫。
|
|
5
|
+
* 架构核心——所有字段变更通过方法(transition/assignRuntime/releaseRuntime/
|
|
6
|
+
* replaceRuntime),engine 模块不直接打洞(AC-3)。
|
|
7
|
+
*
|
|
8
|
+
* 层归属:Engine。依赖 RunRuntime(具体类,D-12 允许)+ RunSpec/RunState + 类型。
|
|
9
|
+
*
|
|
10
|
+
* 关键不变式(必须全测):
|
|
11
|
+
* I1: state.status === "running" ⟺ runtime !== undefined
|
|
12
|
+
* I2: state.status === "done" ⟹ state.reason !== undefined
|
|
13
|
+
*
|
|
14
|
+
* 状态机(FR-3,3 态):
|
|
15
|
+
* paused ──assignRuntime──→ running
|
|
16
|
+
* running ──transition("paused")──→ paused (releaseRuntime, G3-001)
|
|
17
|
+
* running ──transition("done", reason)──→ done (releaseRuntime + completedAt)
|
|
18
|
+
* paused ──transition("done", reason)──→ done (completedAt)
|
|
19
|
+
* done ──(no out edges, zombie)
|
|
20
|
+
*
|
|
21
|
+
* pause/resume 生命周期(G3-001):
|
|
22
|
+
* - transition("paused") 调 releaseRuntime,整个 RunRuntime 被丢弃
|
|
23
|
+
* (runtime=undefined)。AbortController 一次性无法复用。
|
|
24
|
+
* - resume 走 assignRuntime(new RunRuntime(...)),重建 worker/gate/controller。
|
|
25
|
+
*
|
|
26
|
+
* retryNode / worker-error-retry(G5-001 + G6-001):
|
|
27
|
+
* - replaceRuntime(newRt): 前置 status==="running"(G6-001),原子释放前一个 runtime
|
|
28
|
+
* + 绑定新 runtime,全程保持不变式 I1(中间不经过 runtime===undefined 的可见状态)。
|
|
29
|
+
* - paused 状态下 retry 被拒(要 retry 先 resume)。
|
|
30
|
+
*
|
|
31
|
+
* 参考:domain-models.md §1(聚合根定义)、clarification.md G3-001/G5-001/G6-001。
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { RunRuntime } from "./run-runtime.ts";
|
|
35
|
+
import type { RunSpec } from "./run-spec.ts";
|
|
36
|
+
import type { RunState } from "./run-state.ts";
|
|
37
|
+
import type { DoneReason, RunStatus } from "./types.ts";
|
|
38
|
+
import { canRunTransition } from "./types.ts";
|
|
39
|
+
|
|
40
|
+
// ── WorkflowRunMeta ──────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 聚合根级 meta(非 RunState 的一部分,不随 trace 持久化到 worker JSONL)。
|
|
44
|
+
*
|
|
45
|
+
* workerErrorCount/scriptErrorCount 跨 runtime 存活(C.5:error-recovery 重试计数载体),
|
|
46
|
+
* 因为 retry 会 replaceRuntime,但计数是 run 级而非 runtime 级。
|
|
47
|
+
*/
|
|
48
|
+
export interface WorkflowRunMeta {
|
|
49
|
+
/** ISO 时间戳,run 创建/启动时刻。 */
|
|
50
|
+
startedAt: string;
|
|
51
|
+
/** ISO 时间戳,transition("done") 时设置。 */
|
|
52
|
+
completedAt?: string;
|
|
53
|
+
/** ISO 时间戳,transition("paused") 时设置(最近一次 pause)。 */
|
|
54
|
+
pausedAt?: string;
|
|
55
|
+
/** Worker 线程错误计数(C.5:跨 runtime 存活,重试计数载体)。 */
|
|
56
|
+
workerErrorCount?: number;
|
|
57
|
+
/** 脚本错误计数(C.5:跨 runtime 存活)。 */
|
|
58
|
+
scriptErrorCount?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── WorkflowRun ──────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export class WorkflowRun {
|
|
64
|
+
readonly runId: string;
|
|
65
|
+
readonly spec: RunSpec;
|
|
66
|
+
state: RunState;
|
|
67
|
+
runtime?: RunRuntime;
|
|
68
|
+
meta: WorkflowRunMeta;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 创建聚合根。初始状态通常为 "paused"(runtime=undefined,符合不变式 I1),
|
|
72
|
+
* 随后 assignRuntime 进入 "running"。也可传入 done 状态用于 reconstruct
|
|
73
|
+
* 已完成的 run(loadAll 后的只读聚合)。
|
|
74
|
+
*
|
|
75
|
+
* 不变式 I1 由构造函数校验——**不可用于 reconstruct 持久化的 running 快照**
|
|
76
|
+
*(持久化的 running run 没有 worker,违反 I1;进程被杀后 worker 不可能还活着)。
|
|
77
|
+
* 重水合用 `WorkflowRun.reconstruct`,它跳过 I1 校验(快照是可信状态)。
|
|
78
|
+
*
|
|
79
|
+
* @param reconstructMode 内部用——true 时跳过 I1 校验(仅校验 I2)。
|
|
80
|
+
* 调用方用 `WorkflowRun.reconstruct` 静态工厂,不直接传此 flag。
|
|
81
|
+
*/
|
|
82
|
+
constructor(
|
|
83
|
+
runId: string,
|
|
84
|
+
spec: RunSpec,
|
|
85
|
+
state: RunState,
|
|
86
|
+
meta: WorkflowRunMeta,
|
|
87
|
+
reconstructMode = false,
|
|
88
|
+
) {
|
|
89
|
+
this.runId = runId;
|
|
90
|
+
this.spec = spec;
|
|
91
|
+
this.state = state;
|
|
92
|
+
this.meta = meta;
|
|
93
|
+
// runtime 在构造时始终为 undefined——run 创建时无活 worker,resume/loadAll
|
|
94
|
+
// 时也不重水合 runtime(worker 必须由 lifecycle 重新 start)。
|
|
95
|
+
this.runtime = undefined;
|
|
96
|
+
if (reconstructMode) {
|
|
97
|
+
// 重水合:仅校验 I2(done ⟹ reason)。I1 跳过——持久化的 running 状态没有
|
|
98
|
+
// worker,违反 I1;调用方(D-4 kill-9 恢复)负责恢复 I1。
|
|
99
|
+
this.validateInvariantI2();
|
|
100
|
+
} else {
|
|
101
|
+
this.validateInvariants();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 从持久化快照重水合聚合根。跳过 I1 校验——持久化的 running 状态没有 worker
|
|
107
|
+
* (进程被杀后 worker 不可能还活着),违反 I1。调用方(D-4 kill-9 恢复)负责
|
|
108
|
+
* 在 session_start 时把残留 running 转 done,failed,恢复 I1。
|
|
109
|
+
*
|
|
110
|
+
* 与 `new WorkflowRun(...)` 的区别:constructor 校验 I1(适合 live 创建),
|
|
111
|
+
* reconstruct 跳过(适合可信快照重水合)。
|
|
112
|
+
*
|
|
113
|
+
* @throws I2 违反(done 快照缺 reason 仍是 bug,不可跳过)
|
|
114
|
+
*/
|
|
115
|
+
static reconstruct(runId: string, spec: RunSpec, state: RunState, meta: WorkflowRunMeta): WorkflowRun {
|
|
116
|
+
return new WorkflowRun(runId, spec, state, meta, true);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── 不变式校验 ─────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 校验不变式 I1 + I2。违反抛错(聚合根自我保护,fail-fast)。
|
|
123
|
+
* 在每个 mutation 方法末尾调用(防御式编程 + 测试可断言)。
|
|
124
|
+
*/
|
|
125
|
+
private validateInvariants(): void {
|
|
126
|
+
this.validateInvariantI2();
|
|
127
|
+
// I1: status==="running" ⟺ runtime!==undefined
|
|
128
|
+
if (this.state.status === "running" && this.runtime === undefined) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`WorkflowRun invariant I1 violated: status==="running" but runtime is undefined (runId=${this.runId})`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (this.state.status !== "running" && this.runtime !== undefined) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`WorkflowRun invariant I1 violated: status!=="running" but runtime is defined (runId=${this.runId})`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 仅校验不变式 I2(done ⟹ reason)。reconstruct 时用——持久化的 running 快照
|
|
142
|
+
* 违反 I1(无 worker),但 I2 必须保证(done 快照缺 reason 是真 bug)。
|
|
143
|
+
*/
|
|
144
|
+
private validateInvariantI2(): void {
|
|
145
|
+
if (this.state.status === "done" && this.state.reason === undefined) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`WorkflowRun invariant I2 violated: status==="done" but reason is undefined (runId=${this.runId})`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── 状态机转换 ─────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 状态机转换。合法转换:running→{paused,done}, paused→done。
|
|
156
|
+
*
|
|
157
|
+
* paused→running 不走 transition——用 assignRuntime(需注入 runtime)。
|
|
158
|
+
* 调用 transition("running") 抛错,引导调用方用 assignRuntime。
|
|
159
|
+
*
|
|
160
|
+
* 副作用:
|
|
161
|
+
* - →paused: releaseRuntime(G3-001 丢弃 runtime)+ 设 meta.pausedAt
|
|
162
|
+
* - →done: releaseRuntime + 设 state.reason + meta.completedAt
|
|
163
|
+
*
|
|
164
|
+
* @param target 目标状态(不允许 "running"——用 assignRuntime)
|
|
165
|
+
* @param reason →done 时必填(done ⟹ reason,不变式 I2);→paused 时忽略
|
|
166
|
+
* @throws 非法转换 / done 缺 reason / target==="running"
|
|
167
|
+
*/
|
|
168
|
+
transition(target: RunStatus, reason?: DoneReason): void {
|
|
169
|
+
// "running" 必须经 assignRuntime(需 runtime 参数,transition 无法提供)
|
|
170
|
+
if (target === "running") {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`WorkflowRun.transition: cannot transition to "running" directly — use assignRuntime() (runId=${this.runId})`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!canRunTransition(this.state.status, target)) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`WorkflowRun.transition: illegal transition ${this.state.status} → ${target} (runId=${this.runId})`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// →done 需 reason(不变式 I2)
|
|
183
|
+
if (target === "done" && reason === undefined) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`WorkflowRun.transition: transition to "done" requires a reason (runId=${this.runId})`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 副作用:先清理 runtime(releaseRuntime 守不变式 I1),再改 status
|
|
190
|
+
if (target === "paused" || target === "done") {
|
|
191
|
+
this.releaseRuntime();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.state.status = target;
|
|
195
|
+
if (target === "paused") {
|
|
196
|
+
this.meta.pausedAt = new Date().toISOString();
|
|
197
|
+
}
|
|
198
|
+
if (target === "done") {
|
|
199
|
+
this.state.reason = reason;
|
|
200
|
+
this.meta.completedAt = new Date().toISOString();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.validateInvariants();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Runtime 生命周期 ───────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 绑定 runtime 并进入 running 状态。
|
|
210
|
+
*
|
|
211
|
+
* 前置:status==="paused" && runtime===undefined(首次启动或 resume)。
|
|
212
|
+
* 原子地:设 runtime + status="running",保持不变式 I1 全程不违反。
|
|
213
|
+
*
|
|
214
|
+
* @throws runtime 已定义 / status 不是 "paused"
|
|
215
|
+
*/
|
|
216
|
+
assignRuntime(rt: RunRuntime): void {
|
|
217
|
+
if (this.runtime !== undefined) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`WorkflowRun.assignRuntime: runtime already defined (runId=${this.runId})`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (this.state.status !== "paused") {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`WorkflowRun.assignRuntime: requires status==="paused" (current: ${this.state.status}, runId=${this.runId})`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
// 原子绑定:先设 runtime(I1 暂时违反:status!=="running" 但 runtime!==undefined),
|
|
228
|
+
// 紧接着设 status="running",末尾 validateInvariants 通过。
|
|
229
|
+
// 两条赋值间无 await/外部观察点,外部不可见中间状态。
|
|
230
|
+
this.runtime = rt;
|
|
231
|
+
this.state.status = "running";
|
|
232
|
+
this.validateInvariants();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 解绑 runtime(pause/done 时由 transition 调用,也可独立调用)。
|
|
237
|
+
*
|
|
238
|
+
* 前置:无(runtime===undefined 时 no-op,幂等)。
|
|
239
|
+
* 副作用:调 runtime.release("pause") 释放 worker/controller,置 runtime=undefined。
|
|
240
|
+
*/
|
|
241
|
+
releaseRuntime(): void {
|
|
242
|
+
if (this.runtime === undefined) return;
|
|
243
|
+
this.runtime.release("pause");
|
|
244
|
+
this.runtime = undefined;
|
|
245
|
+
// 不改 status——调用方(transition)负责。独立调用时调用方需自行确保
|
|
246
|
+
// status 一致(如 retryNode 用 replaceRuntime 而非 release+assign)。
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 原地替换 runtime(G5-001:retryNode / worker-error-retry)。
|
|
251
|
+
*
|
|
252
|
+
* 前置:status==="running"(G6-001:paused 下拒绝,要 retry 先 resume)。
|
|
253
|
+
* 原子地:释放旧 runtime(worker.terminate + abort)+ 绑定新 runtime,
|
|
254
|
+
* 全程 status 保持 "running",不变式 I1 不违反(中间无 runtime===undefined 可见态)。
|
|
255
|
+
*
|
|
256
|
+
* 与 release+assign 的区别:replaceRuntime 不改 status(避免经过 paused 中间态),
|
|
257
|
+
* 中间同步完成,外部观察不到违反不变式的瞬间。
|
|
258
|
+
*
|
|
259
|
+
* @throws status!=="running"
|
|
260
|
+
*/
|
|
261
|
+
replaceRuntime(rt: RunRuntime): void {
|
|
262
|
+
if (this.state.status !== "running") {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`WorkflowRun.replaceRuntime: requires status==="running" (current: ${this.state.status}, runId=${this.runId})`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
// 原子替换:旧 runtime 释放(terminate+abort),新 runtime 绑定。
|
|
268
|
+
// status 保持 "running",runtime 全程 !== undefined,I1 不违反。
|
|
269
|
+
if (this.runtime !== undefined) {
|
|
270
|
+
this.runtime.release("terminal");
|
|
271
|
+
}
|
|
272
|
+
this.runtime = rt;
|
|
273
|
+
this.validateInvariants();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Extension — WorkflowScriptRegistry 仓库接口
|
|
3
|
+
*
|
|
4
|
+
* workflow 脚本的仓库(repository)接口——Engine 定义、Infra 实现。
|
|
5
|
+
*
|
|
6
|
+
* 与 Ports 节的 3 个注入 port(AgentRunner/RunStore/WorkerHost)的区别:
|
|
7
|
+
* - 3 个 port 是"执行依赖"(子进程/文件系统/线程),注入到 LifecycleDeps
|
|
8
|
+
* - WorkflowScriptRegistry 是"发现依赖"(扫描文件系统),是 repository(§8),
|
|
9
|
+
* 不进 LifecycleDeps,由 Interface 层 tool 直接调用(list/get 脚本)
|
|
10
|
+
*
|
|
11
|
+
* 优先级:tmp > project > user(domain-models.md §8)。60s TTL,按 workspaceRoot 分桶。
|
|
12
|
+
* 实现在 Infra 层 WorkflowScriptRegistryImpl(扫描 + 缓存 + 去重)。
|
|
13
|
+
*
|
|
14
|
+
* 层归属:Engine(interface),Infra(impl)。
|
|
15
|
+
*
|
|
16
|
+
* 参考:domain-models.md §8。
|
|
17
|
+
*/
|
|
18
|
+
import type { WorkflowScript } from "./workflow-script.ts";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* workflow 脚本仓库接口(repository,需 mock 文件扫描)。
|
|
22
|
+
*/
|
|
23
|
+
export interface WorkflowScriptRegistry {
|
|
24
|
+
/** 扫描并返回所有 workflow 脚本(含 available=false 的解析失败项)。去重按 tmp>project>user。 */
|
|
25
|
+
loadAll(): Promise<WorkflowScript[]>;
|
|
26
|
+
|
|
27
|
+
/** 按名查单个脚本(含缓存)。返回 undefined 当 name 不存在。 */
|
|
28
|
+
get(name: string): Promise<WorkflowScript | undefined>;
|
|
29
|
+
|
|
30
|
+
/** 失效缓存——下次 loadAll/get 重新扫描文件系统。 */
|
|
31
|
+
invalidate(): void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Extension — WorkflowScript 实体
|
|
3
|
+
*
|
|
4
|
+
* 一个 workflow 脚本文件的数据 + 操作收敛(domain-models.md §7)。
|
|
5
|
+
*
|
|
6
|
+
* 设计:
|
|
7
|
+
* - 将"脚本源 + meta + validate + toExecutable"收敛为实体。
|
|
8
|
+
* - validate 委托 engine/script-lint.ts 的 lintScript。
|
|
9
|
+
* - toExecutable 只做 strip `export const meta`(纯文本变换);worker 线程 wrap
|
|
10
|
+
* (注入 agent/parallel/pipeline globals)由 infra/worker-script-builder.ts
|
|
11
|
+
* 的 buildWorkerScript 承担——那是技术资源模板生成,不属于实体职责(D-12:
|
|
12
|
+
* 模型只管数据+不变式)。
|
|
13
|
+
*
|
|
14
|
+
* 层归属:Engine。
|
|
15
|
+
*
|
|
16
|
+
* 参考:domain-models.md §7(字段/操作)、engine/script-lint.ts(lint 实现)。
|
|
17
|
+
*/
|
|
18
|
+
import { type LintResult,lintScript } from "../script-lint.ts";
|
|
19
|
+
// LintFinding/LintResult 类型规范归属 engine/script-lint.ts(canonical 源)。
|
|
20
|
+
|
|
21
|
+
/** 脚本来源:saved(.pi/workflows/ 固定)或 tmp(.pi/workflows/.tmp/ 临时)。 */
|
|
22
|
+
export type WorkflowSource = "saved" | "tmp";
|
|
23
|
+
|
|
24
|
+
/** 脚本元信息(regex 提取,不执行用户代码)。 */
|
|
25
|
+
export interface WorkflowMeta {
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
phases: (string | { title: string; detail?: string })[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** strip `export const meta` → `const meta`(lifecycle.ts:66 逻辑迁移)。 */
|
|
32
|
+
const EXPORT_META_PATTERN = /\bexport\s+const\s+meta\b/g;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* WorkflowScript 实体。
|
|
36
|
+
*
|
|
37
|
+
* 不变式:
|
|
38
|
+
* - name 非空(meta 提取成功时来自 meta.name,失败时来自文件名 stem)
|
|
39
|
+
* - available=false 时 meta 为空壳(name=stem, description="", phases=[])
|
|
40
|
+
* - sourceCode 为原始文件内容(含 export);toExecutable 返回 strip 后的副本
|
|
41
|
+
*/
|
|
42
|
+
export class WorkflowScript {
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly source: WorkflowSource;
|
|
45
|
+
readonly path: string;
|
|
46
|
+
/** 原始文件内容(可编辑)。toExecutable 返回 strip 后的副本,不改本字段。 */
|
|
47
|
+
sourceCode: string;
|
|
48
|
+
readonly meta: WorkflowMeta;
|
|
49
|
+
/** false 当 meta 提取失败(loader 不抛错,标记不可用但仍列出)。 */
|
|
50
|
+
available: boolean;
|
|
51
|
+
|
|
52
|
+
constructor(opts: {
|
|
53
|
+
name: string;
|
|
54
|
+
source: WorkflowSource;
|
|
55
|
+
path: string;
|
|
56
|
+
sourceCode: string;
|
|
57
|
+
meta: WorkflowMeta;
|
|
58
|
+
available: boolean;
|
|
59
|
+
}) {
|
|
60
|
+
this.name = opts.name;
|
|
61
|
+
this.source = opts.source;
|
|
62
|
+
this.path = opts.path;
|
|
63
|
+
this.sourceCode = opts.sourceCode;
|
|
64
|
+
this.meta = opts.meta;
|
|
65
|
+
this.available = opts.available;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 静态检查脚本合法性。
|
|
70
|
+
*
|
|
71
|
+
* 委托 engine/script-lint.ts 的 lintScript——检查项含:
|
|
72
|
+
* - 必须含 agent/parallel/pipeline 入口之一
|
|
73
|
+
* - agent 选项 outputSchema → schema
|
|
74
|
+
* - result.output/parsedOutput/content 不存在
|
|
75
|
+
* - 文件传状态警告
|
|
76
|
+
*/
|
|
77
|
+
validate(): LintResult {
|
|
78
|
+
return lintScript(this.sourceCode);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 返回可执行源(strip `export const meta` → `const meta`)。
|
|
83
|
+
*
|
|
84
|
+
* 脚本格式不变(AC-4)。Worker 线程 wrap(注入 globals)由 infra
|
|
85
|
+
* infra/worker-script-builder.ts buildWorkerScript 完成——本方法只做纯文本变换。
|
|
86
|
+
*/
|
|
87
|
+
toExecutable(): string {
|
|
88
|
+
return this.sourceCode.replace(EXPORT_META_PATTERN, "const meta");
|
|
89
|
+
}
|
|
90
|
+
}
|