@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,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Extension — lifecycle
|
|
3
|
+
*
|
|
4
|
+
* Workflow run 生命周期 free functions(D-12)。
|
|
5
|
+
*
|
|
6
|
+
* 4 个导出函数:
|
|
7
|
+
* - runWorkflow(spec, deps, signal?) → Promise<runId>
|
|
8
|
+
* - pauseRun(runId, deps) → Promise<void>(A4 原子性)
|
|
9
|
+
* - resumeRun(runId, deps) → Promise<void>(G3-001 整重建)
|
|
10
|
+
* - abortRun(runId, deps, reason?) → Promise<void>(done no-op)
|
|
11
|
+
*
|
|
12
|
+
* 私有 makeHandlers(run, deps) → WorkerHandlers:
|
|
13
|
+
* - onMessage → handleWorkerMessage(run, raw, deps, handlers)
|
|
14
|
+
* - onError → handleWorkerError(run, err, deps, handlers) + workerErrorCount++
|
|
15
|
+
* - onExit(code, handle) → handleWorkerExit(run, code, handle, deps, handlers)
|
|
16
|
+
* (G-025:handle.isCurrent 检查内化在 handleWorkerExit 内)
|
|
17
|
+
*
|
|
18
|
+
* **A4 原子性**:pause/abort 内部 transition 先 releaseRuntime(cleanup before mutate),
|
|
19
|
+
* 失败时 status 不变。transition("paused"/"done") 在 WorkflowRun.transition 内已实现
|
|
20
|
+
* 「releaseRuntime → 改 status」原子顺序。
|
|
21
|
+
*
|
|
22
|
+
* **G3-001**:pause 时整个 RunRuntime 丢弃(AbortController 一次性,无法复用);
|
|
23
|
+
* resume 时 assignRuntime 重建 worker/gate/controller。
|
|
24
|
+
*
|
|
25
|
+
* **D-13**:maxConcurrency=4(ConcurrencyGate 默认值)。
|
|
26
|
+
*
|
|
27
|
+
* 层归属:Engine。依赖 LifecycleDeps + ConcurrencyGate + WorkerHost via port +
|
|
28
|
+
* WorkflowRun + handleWorker* 函数。
|
|
29
|
+
*
|
|
30
|
+
* 参考:domain-models.md §1(聚合根状态机)。
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { ConcurrencyGate, DEFAULT_CONCURRENCY } from "./concurrency-gate.ts";
|
|
34
|
+
import {
|
|
35
|
+
handleWorkerError,
|
|
36
|
+
handleWorkerExit,
|
|
37
|
+
handleWorkerMessage,
|
|
38
|
+
} from "./error-recovery.ts";
|
|
39
|
+
import { Budget } from "./models/budget.ts";
|
|
40
|
+
import type { LifecycleDeps, WorkerHandlers } from "./models/ports.ts";
|
|
41
|
+
import { RunRuntime } from "./models/run-runtime.ts";
|
|
42
|
+
import type { RunSpec } from "./models/run-spec.ts";
|
|
43
|
+
import { Trace } from "./models/trace.ts";
|
|
44
|
+
import type { DoneReason } from "./models/types.ts";
|
|
45
|
+
import { WorkflowRun } from "./models/workflow-run.ts";
|
|
46
|
+
import type { WorkerHandle } from "./worker-handle.ts";
|
|
47
|
+
|
|
48
|
+
// ── 常量 ─────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/** runId 生成:wf-<timestamp>-<base36 random 6 chars>。 */
|
|
51
|
+
const RUNID_RADIX = 36;
|
|
52
|
+
const RUNID_SLICE_START = 2;
|
|
53
|
+
const RUNID_SLICE_END = 8;
|
|
54
|
+
|
|
55
|
+
function generateRunId(): string {
|
|
56
|
+
return `wf-${Date.now()}-${Math.random().toString(RUNID_RADIX).slice(RUNID_SLICE_START, RUNID_SLICE_END)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── makeHandlers(路由 worker 事件到 error-recovery handle* 函数) ──────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 构造 WorkerHandlers——将 worker 的 onMessage/onError/onExit 事件路由到
|
|
63
|
+
* error-recovery 的 handleWorker* 函数。
|
|
64
|
+
*
|
|
65
|
+
* 闭包捕获 run + deps。runtime 重建(replaceRuntime)后 run 实例不变、deps 不变,
|
|
66
|
+
* 故 handlers 对新 worker 仍有效(lifecycle 与 error-recovery 共用 handlers)。
|
|
67
|
+
*
|
|
68
|
+
* **onExit G-025**:handleWorkerExit 内部检查 handle.isCurrent(stale exit 丢弃)。
|
|
69
|
+
* 本函数不在 onExit 里重复检查——error-recovery.handleWorkerExit 是单一守卫点。
|
|
70
|
+
*
|
|
71
|
+
* **workerErrorCount**:onError 触发时递增(C.5 跨 runtime 存活的重试计数载体)。
|
|
72
|
+
* 注意 handleWorkerError 内部也会递增——这里 onError 递增是 worker 事件层面的
|
|
73
|
+
* 「error 事件到达」计数,handleWorkerError 内的是「错误处理决策」计数。
|
|
74
|
+
* 实际 handleWorkerError 会做最终计数(含重试上限判断),onError 不重复递增。
|
|
75
|
+
*/
|
|
76
|
+
function makeHandlers(run: WorkflowRun, deps: LifecycleDeps): WorkerHandlers {
|
|
77
|
+
// 自引用——error-recovery rebuildRuntime 需要 handlers 参数(handlers 引用自身)
|
|
78
|
+
const handlers: WorkerHandlers = {
|
|
79
|
+
async onMessage(raw: unknown): Promise<void> {
|
|
80
|
+
await handleWorkerMessage(run, raw, deps, handlers);
|
|
81
|
+
},
|
|
82
|
+
async onError(err: Error): Promise<void> {
|
|
83
|
+
await handleWorkerError(run, err, deps, handlers);
|
|
84
|
+
},
|
|
85
|
+
async onExit(code: number, handle: WorkerHandle): Promise<void> {
|
|
86
|
+
// H-2:用 worker-host 传入的 handle(即真正触发 exit 的那个 handle),而非
|
|
87
|
+
// run.runtime?.worker——重试竞态下 runtime.worker 可能已被 replaceRuntime 替换
|
|
88
|
+
// 为新 handle,导致 handleWorkerExit 内的 isCurrent 检查误判(漏判 stale exit 或
|
|
89
|
+
// 误杀新 worker)。G-025 检查仍在 handleWorkerExit 内(handle.isCurrent)。
|
|
90
|
+
await handleWorkerExit(run, code, handle, deps, handlers);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
return handlers;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── scheduleTimeBudget(C.7 Run 级时间预算调度) ──────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 启动 run 级墙钟时间预算计时器:到期后 abortRun(doneReason="time_limited")。
|
|
100
|
+
*
|
|
101
|
+
* 恢复旧 orchestrator-budget.ts 的 scheduleTimeBudgetCheck 语义——run/resume 各
|
|
102
|
+
* 启一个 setTimeout(maxTimeMs),到期若 run 仍 running/paused 则转 done,time_limited。
|
|
103
|
+
* 计时器存入 RunRuntime.timeBudgetTimer,release(pause/abort/replaceRuntime)时
|
|
104
|
+
* 自动清理,避免孤儿触发。resume 会调度全新计时器(与旧语义一致:pause+resume
|
|
105
|
+
* 重置墙钟预算)。
|
|
106
|
+
*
|
|
107
|
+
* @returns 计时器句柄(未设预算时 undefined)
|
|
108
|
+
*/
|
|
109
|
+
export function scheduleTimeBudget(
|
|
110
|
+
runId: string,
|
|
111
|
+
deps: LifecycleDeps,
|
|
112
|
+
budgetTimeMs: number,
|
|
113
|
+
): ReturnType<typeof setTimeout> {
|
|
114
|
+
const timer = setTimeout(() => {
|
|
115
|
+
void abortRun(runId, deps, "Time budget exceeded", "time_limited").catch(
|
|
116
|
+
(err: unknown) => {
|
|
117
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
118
|
+
console.error(`[workflow] time budget abort failed: ${msg}`);
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
}, budgetTimeMs);
|
|
122
|
+
// unref:不阻止 Node 退出(workflow 是后台任务,不应因计时器持有事件循环)。
|
|
123
|
+
timer.unref();
|
|
124
|
+
return timer;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── runWorkflow ──────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 启动一个 workflow run。
|
|
131
|
+
*
|
|
132
|
+
* 流程:创建 WorkflowRun(paused)+ makeHandlers + 构建 RunRuntime(worker+gate+controller)
|
|
133
|
+
* + assignRuntime(paused → running)+ 注册到 deps.runs + store.save。
|
|
134
|
+
*
|
|
135
|
+
* @param spec RunSpec(不可变输入,含 scriptSource/args)
|
|
136
|
+
* @param deps LifecycleDeps(store/workerHost/runner/runs)
|
|
137
|
+
* @param signal 外部 abort signal(可选;abort 时调 abortRun)
|
|
138
|
+
* @returns runId(wf-<timestamp>-<random>)
|
|
139
|
+
* @throws signal 已 abort(pre-abort fail fast)
|
|
140
|
+
*/
|
|
141
|
+
export async function runWorkflow(
|
|
142
|
+
spec: RunSpec,
|
|
143
|
+
deps: LifecycleDeps,
|
|
144
|
+
signal?: AbortSignal,
|
|
145
|
+
): Promise<string> {
|
|
146
|
+
const runId = generateRunId();
|
|
147
|
+
deps.log?.("debug", "workflow:lifecycle", "runWorkflow start", { runId, scriptName: spec.scriptName });
|
|
148
|
+
|
|
149
|
+
// P1-2: pre-aborted signal → fail fast
|
|
150
|
+
if (signal?.aborted) {
|
|
151
|
+
throw new Error("Workflow run aborted before start");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const run = new WorkflowRun(
|
|
155
|
+
runId,
|
|
156
|
+
spec,
|
|
157
|
+
{
|
|
158
|
+
status: "paused",
|
|
159
|
+
budget: spec.budgetRef ?? new Budget({
|
|
160
|
+
maxTokens: spec.budgetTokens,
|
|
161
|
+
maxTimeMs: spec.budgetTimeMs,
|
|
162
|
+
}),
|
|
163
|
+
calls: new Map(),
|
|
164
|
+
trace: new Trace(),
|
|
165
|
+
errorLogs: [],
|
|
166
|
+
},
|
|
167
|
+
{ startedAt: new Date().toISOString() },
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// signal abort → abortRun(一次性监听)
|
|
171
|
+
if (signal) {
|
|
172
|
+
signal.addEventListener(
|
|
173
|
+
"abort",
|
|
174
|
+
() => {
|
|
175
|
+
void abortRun(runId, deps, "External signal aborted").catch((err: unknown) => {
|
|
176
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
+
console.error(`[workflow] abortRun on signal failed: ${msg}`);
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
{ once: true },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 注册到 deps.runs(makeHandlers + assignRuntime 需要 run 已在 map)
|
|
185
|
+
deps.runs.set(runId, run);
|
|
186
|
+
|
|
187
|
+
// 构造 handlers + runtime(worker + gate + controller)
|
|
188
|
+
const handlers = makeHandlers(run, deps);
|
|
189
|
+
const controller = new AbortController();
|
|
190
|
+
const gate = new ConcurrencyGate({ maxConcurrency: DEFAULT_CONCURRENCY });
|
|
191
|
+
const worker = deps.workerHost.start(spec, spec.args, handlers);
|
|
192
|
+
// C.7:run 级时间预算计时器(spec.budgetTimeMs > 0 时启用,到期 abortRun time_limited)。
|
|
193
|
+
const timeBudgetTimer =
|
|
194
|
+
spec.budgetTimeMs && spec.budgetTimeMs > 0
|
|
195
|
+
? scheduleTimeBudget(runId, deps, spec.budgetTimeMs)
|
|
196
|
+
: undefined;
|
|
197
|
+
const runtime = new RunRuntime(worker, gate, controller, timeBudgetTimer);
|
|
198
|
+
|
|
199
|
+
// assignRuntime(paused → running,原子绑定 runtime + status)
|
|
200
|
+
run.assignRuntime(runtime);
|
|
201
|
+
|
|
202
|
+
await deps.store.save(run);
|
|
203
|
+
deps.log?.("debug", "workflow:lifecycle", "run saved", { runId, status: run.state.status });
|
|
204
|
+
|
|
205
|
+
// pending-notifications: run 启动 → 注册(所有 workflow 启动路径的单一汇聚点:
|
|
206
|
+
// runAndWait / actionRun / 未来入口全覆盖)
|
|
207
|
+
deps.log?.("debug", "workflow:lifecycle", "emit pending:register", { runId });
|
|
208
|
+
deps.eventBus?.emit("pending:register", {
|
|
209
|
+
id: runId,
|
|
210
|
+
type: "workflow",
|
|
211
|
+
name: spec.scriptName || runId,
|
|
212
|
+
});
|
|
213
|
+
deps.log?.("debug", "workflow:lifecycle", "emit pending:register done", { runId });
|
|
214
|
+
|
|
215
|
+
return runId;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── pauseRun ─────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 暂停 running workflow。
|
|
222
|
+
*
|
|
223
|
+
* **A4 原子性**:WorkflowRun.transition("paused") 内部先 releaseRuntime(cleanup
|
|
224
|
+
* before mutate),再改 status。releaseRuntime 失败时 status 不变(仍 running)。
|
|
225
|
+
*
|
|
226
|
+
* **G3-001**:transition("paused") 调 releaseRuntime,整个 RunRuntime 丢弃
|
|
227
|
+
* (runtime=undefined)。AbortController 一次性无法复用。
|
|
228
|
+
*
|
|
229
|
+
* @throws runId 不存在 / status !== "running"
|
|
230
|
+
*/
|
|
231
|
+
export async function pauseRun(runId: string, deps: LifecycleDeps): Promise<void> {
|
|
232
|
+
const run = deps.runs.get(runId);
|
|
233
|
+
if (!run) {
|
|
234
|
+
throw new Error(`Workflow '${runId}' not found`);
|
|
235
|
+
}
|
|
236
|
+
if (run.state.status !== "running") {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Cannot pause workflow in state '${run.state.status}': only 'running' can be paused`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// A4: transition 内部 releaseRuntime(cleanup before mutate)
|
|
243
|
+
run.transition("paused");
|
|
244
|
+
|
|
245
|
+
// MUST_FIX (round-4 #1): 清理被 abort 的在飞 call(status !== "done")。
|
|
246
|
+
// transition 已同步 abort controller + terminate worker,但 in-flight 的
|
|
247
|
+
// executeAgentCall 会在后续 microtask 被 finalizeCall 标记为 "done"(带 abort
|
|
248
|
+
// 错误结果)。若保留,resume 时 dispatchAgentCall 的 cached 路径会把该 failed
|
|
249
|
+
// 结果原样 replay,静默污染 workflow 输出。此处同步移除在飞 call + 其 trace 节点,
|
|
250
|
+
// 让 resume 重发 agent-call 走全新执行路径。
|
|
251
|
+
//
|
|
252
|
+
// 同步执行(在 await store.save 前):transition 与本清理间无 await,微任务无法
|
|
253
|
+
// 插入,此时在飞 call 仍为 "running"/"pending",可精确清理;genuinely-done 的
|
|
254
|
+
// call 不受影响(resume 时正常 replay)。后续 finalizeCall 在已移除的 orphan call
|
|
255
|
+
// 上运行(markDone 无外部副作用),trace.update 因节点已移除为 no-op。
|
|
256
|
+
discardInFlightCalls(run);
|
|
257
|
+
|
|
258
|
+
await deps.store.save(run);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 移除 run 中未真正完成的在飞 call(status !== "done")及其 trace 节点。
|
|
263
|
+
*
|
|
264
|
+
* 仅 pauseRun 调用——清理被 abort 的在飞 call,避免 resume 时 cached replay
|
|
265
|
+
* 把 abort 产生的 failed 结果当作已完成结果回放(MUST_FIX round-4 #1)。
|
|
266
|
+
* genuinely-done 的 call(成功或失败均 "done")保留,resume 时按原语义 replay。
|
|
267
|
+
*/
|
|
268
|
+
function discardInFlightCalls(run: WorkflowRun): void {
|
|
269
|
+
const inFlight: number[] = [];
|
|
270
|
+
for (const [callId, call] of run.state.calls) {
|
|
271
|
+
if (call.status !== "done") inFlight.push(callId);
|
|
272
|
+
}
|
|
273
|
+
for (const callId of inFlight) {
|
|
274
|
+
run.state.calls.delete(callId);
|
|
275
|
+
run.state.trace.removeByStepIndex(callId);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── resumeRun ────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 恢复 paused workflow。
|
|
283
|
+
*
|
|
284
|
+
* **G3-001**:assignRuntime 重建 worker/gate/controller + transition running。
|
|
285
|
+
* worker 重跑脚本,已完成调用从 RunState.calls replay(callCache 在 calls Map 里)。
|
|
286
|
+
*
|
|
287
|
+
* **A4 mirror**:先 startWorker(副作用可能抛),成功后才 assignRuntime 改 status。
|
|
288
|
+
* 若 Worker 构造抛错,workflow 保持 paused,调用方可重试。
|
|
289
|
+
*
|
|
290
|
+
* @throws runId 不存在 / status !== "paused"
|
|
291
|
+
*/
|
|
292
|
+
export async function resumeRun(runId: string, deps: LifecycleDeps): Promise<void> {
|
|
293
|
+
const run = deps.runs.get(runId);
|
|
294
|
+
if (!run) {
|
|
295
|
+
throw new Error(`Workflow '${runId}' not found`);
|
|
296
|
+
}
|
|
297
|
+
if (run.state.status !== "paused") {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`Cannot resume workflow in state '${run.state.status}': only 'paused' can be resumed`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// A4 mirror: 先 startWorker(副作用),成功后才 assignRuntime
|
|
304
|
+
const handlers = makeHandlers(run, deps);
|
|
305
|
+
const controller = new AbortController();
|
|
306
|
+
const gate = new ConcurrencyGate({ maxConcurrency: DEFAULT_CONCURRENCY });
|
|
307
|
+
const worker = deps.workerHost.start(run.spec, run.spec.args, handlers);
|
|
308
|
+
// C.7:resume 重新调度时间预算计时器(与 run 对称;旧 pause 已清旧计时器)。
|
|
309
|
+
const timeBudgetTimer =
|
|
310
|
+
run.spec.budgetTimeMs && run.spec.budgetTimeMs > 0
|
|
311
|
+
? scheduleTimeBudget(runId, deps, run.spec.budgetTimeMs)
|
|
312
|
+
: undefined;
|
|
313
|
+
const runtime = new RunRuntime(worker, gate, controller, timeBudgetTimer);
|
|
314
|
+
|
|
315
|
+
// assignRuntime(paused → running,原子绑定 runtime + status)
|
|
316
|
+
run.assignRuntime(runtime);
|
|
317
|
+
await deps.store.save(run);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── abortRun ─────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 中止 workflow(running 或 paused)。
|
|
324
|
+
*
|
|
325
|
+
* **done 状态 no-op**:已终态的 run 不重复 abort。
|
|
326
|
+
* **A4 原子性**:transition("done", doneReason) 内部先 releaseRuntime。
|
|
327
|
+
*
|
|
328
|
+
* @param runId
|
|
329
|
+
* @param deps
|
|
330
|
+
* @param reason 可选中止原因(存 run.state.error)
|
|
331
|
+
* @param doneReason 终态原因(默认 "aborted";超时场景传 "time_limited",C.7)
|
|
332
|
+
* @throws runId 不存在
|
|
333
|
+
*/
|
|
334
|
+
export async function abortRun(
|
|
335
|
+
runId: string,
|
|
336
|
+
deps: LifecycleDeps,
|
|
337
|
+
reason?: string,
|
|
338
|
+
doneReason: DoneReason = "aborted",
|
|
339
|
+
): Promise<void> {
|
|
340
|
+
const run = deps.runs.get(runId);
|
|
341
|
+
if (!run) {
|
|
342
|
+
throw new Error(`Workflow '${runId}' not found`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
deps.log?.("debug", "workflow:lifecycle", "abortRun", { runId, status: run.state.status, reason, doneReason });
|
|
346
|
+
|
|
347
|
+
// done 状态 no-op
|
|
348
|
+
if (run.state.status === "done") {
|
|
349
|
+
deps.log?.("debug", "workflow:lifecycle", "abortRun no-op: already done", { runId });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 只允许 running/paused abort(防御)
|
|
354
|
+
if (run.state.status !== "running" && run.state.status !== "paused") {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Cannot abort workflow in state '${run.state.status}': only 'running' or 'paused' can be aborted`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 记录中止原因
|
|
361
|
+
if (reason) {
|
|
362
|
+
run.state.error = reason;
|
|
363
|
+
}
|
|
364
|
+
// A4: transition 内部 releaseRuntime(cleanup before mutate)
|
|
365
|
+
run.transition("done", doneReason);
|
|
366
|
+
await deps.store.save(run);
|
|
367
|
+
deps.log?.("debug", "workflow:lifecycle", "abortRun transition done", { runId, reason: run.state.reason });
|
|
368
|
+
// C-4: run 到达 done 终态 → 注销 pending-notification + 通知 Interface 层
|
|
369
|
+
deps.log?.("debug", "workflow:lifecycle", "emit pending:unregister", { runId, reason: run.state.reason });
|
|
370
|
+
deps.eventBus?.emit("pending:unregister", { id: run.runId, reason: run.state.reason ?? "completed" });
|
|
371
|
+
deps.log?.("debug", "workflow:lifecycle", "emit pending:unregister done", { runId });
|
|
372
|
+
deps.onRunDone?.(run);
|
|
373
|
+
}
|