@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,578 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Extension — error-recovery
|
|
3
|
+
*
|
|
4
|
+
* Worker 失败处理 free functions(D-12)。
|
|
5
|
+
*
|
|
6
|
+
* 4 个导出函数(domain-models.md §失败处理矩阵):
|
|
7
|
+
* - handleWorkerMessage(run, raw, deps, handlers) — 路由 agent_call/return/error
|
|
8
|
+
* - handleWorkerError(run, err, deps, handlers) — worker uncaught error
|
|
9
|
+
* - handleWorkerExit(run, code, handle, deps, handlers) — worker exit
|
|
10
|
+
* - handleScriptError(run, msg, deps, handlers) — type:"error" from worker
|
|
11
|
+
*
|
|
12
|
+
* 重试矩阵(domain-models.md §失败处理矩阵):
|
|
13
|
+
* - worker error/exit(非零)→ 3 次重试 + 指数退避 1s/2s/4s;超限 failed
|
|
14
|
+
* - script error → 3 次重试 + 指数退避;超限 failed
|
|
15
|
+
* - 重试前 rebuildRuntime(G3-001:整个 RunRuntime 重建:worker+gate+controller)
|
|
16
|
+
*
|
|
17
|
+
* 关键不变式:
|
|
18
|
+
* - 重试前必须 rebuildRuntime(worker+gate+controller 整体重建,避免孤儿资源)。
|
|
19
|
+
* - 重试计数载体是 run.meta.workerErrorCount/scriptErrorCount(跨 runtime 存活,
|
|
20
|
+
* retry replaceRuntime 后计数不丢)。
|
|
21
|
+
* - handleWorkerExit 检查 handle.isCurrent(G-025:stale exit 事件丢弃)。
|
|
22
|
+
*
|
|
23
|
+
* 层归属:Engine。依赖 ports + ConcurrencyGate + WorkflowRun + executeAgentCall。
|
|
24
|
+
*
|
|
25
|
+
* 参考:domain-models.md §失败处理矩阵。
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { AgentEvent } from "../shared/agent-event.ts";
|
|
29
|
+
import { resolveAgentOpts } from "./agent-opts-resolver.ts";
|
|
30
|
+
import { ConcurrencyGate, DEFAULT_CONCURRENCY } from "./concurrency-gate.ts";
|
|
31
|
+
import type { WorkerHandle } from "./worker-handle.ts";
|
|
32
|
+
import { executeAgentCall } from "./execute-agent-call.ts";
|
|
33
|
+
import { createRecord, updateFromEvent } from "../execution/execution-record.ts";
|
|
34
|
+
import { AgentCall } from "./models/agent-call.ts";
|
|
35
|
+
import type { LifecycleDeps, WorkerHandlers } from "./models/ports.ts";
|
|
36
|
+
import { RunRuntime } from "./models/run-runtime.ts";
|
|
37
|
+
import type { WorkerLogEntry } from "./models/types.ts";
|
|
38
|
+
import type { AgentCallOpts, AgentResult, ExecutionTraceNode } from "./models/types.ts";
|
|
39
|
+
import type { WorkflowRun } from "./models/workflow-run.ts";
|
|
40
|
+
|
|
41
|
+
// ── 常量 ─────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/** Worker/script 错误最大重试次数(domain-models.md §失败处理矩阵)。 */
|
|
44
|
+
const MAX_WORKER_RETRIES = 3;
|
|
45
|
+
|
|
46
|
+
/** 指数退避基数(ms)。 */
|
|
47
|
+
const RETRY_BACKOFF_BASE_MS = 1000;
|
|
48
|
+
const EXPONENTIAL_BACKOFF_BASE = 2;
|
|
49
|
+
|
|
50
|
+
// ── Worker 消息类型(与 infra/worker-script-builder.ts WorkerInMsg 对齐) ──
|
|
51
|
+
|
|
52
|
+
interface AgentCallMsg {
|
|
53
|
+
type: "agent-call";
|
|
54
|
+
callId: number;
|
|
55
|
+
opts: {
|
|
56
|
+
prompt: string;
|
|
57
|
+
schema?: unknown;
|
|
58
|
+
model?: string;
|
|
59
|
+
scene?: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
agent?: string;
|
|
62
|
+
skill?: string;
|
|
63
|
+
timeoutMs?: number;
|
|
64
|
+
cwd?: string; // ADR-029 决策 1:per-call cwd(worktree 隔离)
|
|
65
|
+
};
|
|
66
|
+
phase?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface ReturnMsg {
|
|
70
|
+
type: "return";
|
|
71
|
+
result: unknown;
|
|
72
|
+
workerLogs?: WorkerLogEntry[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ErrorMsg {
|
|
76
|
+
type: "error";
|
|
77
|
+
error: string;
|
|
78
|
+
workerLogs?: WorkerLogEntry[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface WorkflowCallMsg {
|
|
82
|
+
type: "workflow-call";
|
|
83
|
+
callId: number;
|
|
84
|
+
name: string;
|
|
85
|
+
args: Record<string, unknown>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type WorkerMsg = AgentCallMsg | WorkflowCallMsg | ReturnMsg | ErrorMsg;
|
|
89
|
+
|
|
90
|
+
// ── 内部 helper ──────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
function isTerminal(run: WorkflowRun): boolean {
|
|
93
|
+
return run.state.status === "done";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 计算第 n 次重试前的退避时间(ms):1s, 2s, 4s 指数。 */
|
|
97
|
+
function backoffDelay(retryIndex: number): number {
|
|
98
|
+
return RETRY_BACKOFF_BASE_MS * Math.pow(EXPONENTIAL_BACKOFF_BASE, retryIndex - 1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function delay(ms: number): Promise<void> {
|
|
102
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── rebuildRuntime(G3-001 整重建) ─────────────────────────
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 重建整个 RunRuntime:新 controller + 新 gate + 新 worker。
|
|
109
|
+
*
|
|
110
|
+
* 调 run.replaceRuntime(newRt)(G5-001):原子释放旧 runtime(worker.terminate +
|
|
111
|
+
* abort)+ 绑定新 runtime,全程 status==="running" 不变(不变式 I1 不违反)。
|
|
112
|
+
*
|
|
113
|
+
* handlers 由调用方(lifecycle makeHandlers)构造——它们路由 onMessage/onError/
|
|
114
|
+
* onExit 回本文件的 handle* 函数。handlers 捕获 run + deps 闭包,runtime 重建后
|
|
115
|
+
* 仍有效(run 实例不变,deps 不变)。
|
|
116
|
+
*
|
|
117
|
+
* 前置:run.state.status === "running"(replaceRuntime 要求,G6-001)。
|
|
118
|
+
*
|
|
119
|
+
* @throws status !== "running"(由 replaceRuntime 抛)
|
|
120
|
+
*/
|
|
121
|
+
export function rebuildRuntime(
|
|
122
|
+
run: WorkflowRun,
|
|
123
|
+
deps: LifecycleDeps,
|
|
124
|
+
handlers: WorkerHandlers,
|
|
125
|
+
): void {
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const gate = new ConcurrencyGate({ maxConcurrency: DEFAULT_CONCURRENCY });
|
|
128
|
+
const worker = deps.workerHost.start(run.spec, run.spec.args, handlers);
|
|
129
|
+
// D-12 regression fix (round-2 #2):重新调度 run 级墙钟预算计时器。
|
|
130
|
+
// replaceRuntime 释放旧 runtime 时 clearTimeout 了旧计时器(run-runtime.release),
|
|
131
|
+
// 新 runtime 必须重排,否则带 budgetTimeMs 的 run 命中一次 worker/script 错误重试后
|
|
132
|
+
// 时间预算静默失效(直到 pause/resume 才重排)。deps.scheduleTimeBudget 由 Interface
|
|
133
|
+
// 层注入;未注入时(旧测试)跳过重排(兼容,不影响无时间预算的 run)。
|
|
134
|
+
const timeBudgetTimer =
|
|
135
|
+
run.spec.budgetTimeMs && run.spec.budgetTimeMs > 0 && deps.scheduleTimeBudget
|
|
136
|
+
? deps.scheduleTimeBudget(run.runId, run.spec.budgetTimeMs)
|
|
137
|
+
: undefined;
|
|
138
|
+
run.replaceRuntime(new RunRuntime(worker, gate, controller, timeBudgetTimer));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── handleWorkerMessage(消息路由) ──────────────────────────
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 路由 worker → main 的业务消息。
|
|
145
|
+
*
|
|
146
|
+
* agent_call → 派发 executeAgentCall(异步,不 await——立即返回让 worker 继续发消息)
|
|
147
|
+
* return → transition done,completed(脚本正常返回)
|
|
148
|
+
* error → handleScriptError(脚本主动抛错)
|
|
149
|
+
*
|
|
150
|
+
* 终态/paused 状态下的 stale 消息丢弃(P0-1)。
|
|
151
|
+
*/
|
|
152
|
+
export async function handleWorkerMessage(
|
|
153
|
+
run: WorkflowRun,
|
|
154
|
+
raw: unknown,
|
|
155
|
+
deps: LifecycleDeps,
|
|
156
|
+
handlers: WorkerHandlers,
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
// 终态/paused 状态丢弃 stale 消息(P0-1)
|
|
159
|
+
if (isTerminal(run) || run.state.status === "paused") return;
|
|
160
|
+
|
|
161
|
+
const msg = raw as WorkerMsg;
|
|
162
|
+
switch (msg.type) {
|
|
163
|
+
case "agent-call":
|
|
164
|
+
dispatchAgentCall(run, msg, deps);
|
|
165
|
+
return;
|
|
166
|
+
case "workflow-call":
|
|
167
|
+
dispatchWorkflowCall(run, msg, deps);
|
|
168
|
+
return;
|
|
169
|
+
case "return":
|
|
170
|
+
await handleReturn(run, msg, deps);
|
|
171
|
+
return;
|
|
172
|
+
case "error":
|
|
173
|
+
// M1: 传 handlers(rebuildRuntime 需要)
|
|
174
|
+
await handleScriptError(
|
|
175
|
+
run,
|
|
176
|
+
msg.error,
|
|
177
|
+
msg.workerLogs ?? [],
|
|
178
|
+
deps,
|
|
179
|
+
handlers,
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 派发 agent 调用:构建 AgentCall + trace 节点,异步触发 executeAgentCall。
|
|
187
|
+
*
|
|
188
|
+
* 异步触发(不 await)——立即返回,让 worker 能继续发后续 agent-call(parallel 场景)。
|
|
189
|
+
* executeAgentCall 内部完成 markDone + trace.update。
|
|
190
|
+
*
|
|
191
|
+
* **C-3 修复**:executeAgentCall 通过 `run.runtime.gate.withSlot` 包装——gate 管并发
|
|
192
|
+
* 上限(maxConcurrency=4)+ FIFO 排队,runner 管 spawn。两层职责分离。
|
|
193
|
+
*
|
|
194
|
+
* **C-2 修复**:call 完成后检查 `budget.isExceeded` → abortRun(budget_limited),
|
|
195
|
+
* 终止整个 run(避免烧光预算后继续 spawn 新 call)。
|
|
196
|
+
*
|
|
197
|
+
* **stale 完成守卫**:.then 内 recheck `run.state.status === "running"`,paused 后到达的
|
|
198
|
+
* call 完成不写 run.state.calls / 不 postAgentResult(pause 是干净快照)。
|
|
199
|
+
*/
|
|
200
|
+
function dispatchAgentCall(
|
|
201
|
+
run: WorkflowRun,
|
|
202
|
+
msg: AgentCallMsg,
|
|
203
|
+
deps: LifecycleDeps,
|
|
204
|
+
): void {
|
|
205
|
+
// 已缓存的调用直接 replay(跨 pause/resume)
|
|
206
|
+
const cached = run.state.calls.get(msg.callId);
|
|
207
|
+
if (cached && cached.status === "done") {
|
|
208
|
+
postAgentResult(run, msg.callId, cached.result!, true);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 构建 trace 节点 + live record(TUI 实时进度)
|
|
213
|
+
const agentName = msg.opts.description ?? msg.opts.agent ?? "unknown";
|
|
214
|
+
const now = new Date().toISOString();
|
|
215
|
+
// live record:收口 agent 执行过程中的 text/thinking/toolCalls/usage,
|
|
216
|
+
// 供 TUI 在 agent 运行期间显示进度(getEventLog/getCurrentActivity)。
|
|
217
|
+
// 完成时由下方 .then 清除(终态由 node.result 承载)。
|
|
218
|
+
const liveRecord = createRecord(String(msg.callId), {
|
|
219
|
+
agent: agentName,
|
|
220
|
+
model: msg.opts.model ?? "default",
|
|
221
|
+
mode: "background",
|
|
222
|
+
task: msg.opts.prompt,
|
|
223
|
+
startedAt: Date.now(),
|
|
224
|
+
});
|
|
225
|
+
const node: ExecutionTraceNode = {
|
|
226
|
+
stepIndex: msg.callId,
|
|
227
|
+
agent: agentName,
|
|
228
|
+
task: msg.opts.prompt,
|
|
229
|
+
model: msg.opts.model ?? "default",
|
|
230
|
+
status: "running" as const,
|
|
231
|
+
phase: msg.phase,
|
|
232
|
+
startedAt: now,
|
|
233
|
+
live: liveRecord,
|
|
234
|
+
};
|
|
235
|
+
run.state.trace.append(node);
|
|
236
|
+
|
|
237
|
+
// 构建 AgentCall(opts 形状对齐 AgentCallOpts;schema: unknown → Record)
|
|
238
|
+
// 跨进程 IPC 边界的 schema 为 unknown,窄化前加 typeof guard 兜底。
|
|
239
|
+
const rawSchema = msg.opts.schema;
|
|
240
|
+
const opts: AgentCallOpts = {
|
|
241
|
+
...msg.opts,
|
|
242
|
+
schema:
|
|
243
|
+
typeof rawSchema === "object" && rawSchema !== null
|
|
244
|
+
? (rawSchema as Record<string, unknown>)
|
|
245
|
+
: undefined,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// BL-1:解析 agent/skill/schema → systemPromptFiles / skillPath / schemaEnv。
|
|
249
|
+
// D-12 重构误删 resolveAgentOpts,导致 inline override 静默失效。此处从
|
|
250
|
+
// LifecycleDeps 取 agentRegistry/sessionDir/activeTempFiles(per-session,由
|
|
251
|
+
// Interface 层 session_start 注入),调 resolveAgentOpts 解析 inline override。
|
|
252
|
+
// 解析失败(agent/skill 未找到、临时文件写入错)走 error 路径,不发 slot、不 spawn。
|
|
253
|
+
const hasResolverDeps = deps.agentRegistry && deps.sessionDir && deps.activeTempFiles;
|
|
254
|
+
const resolved = hasResolverDeps
|
|
255
|
+
? resolveAgentOpts(opts, deps.agentRegistry!, deps.sessionDir!, deps.activeTempFiles!)
|
|
256
|
+
: { opts };
|
|
257
|
+
if (resolved.error) {
|
|
258
|
+
const call = new AgentCall(msg.callId, opts, node);
|
|
259
|
+
call.markRunning();
|
|
260
|
+
const errorResult: AgentResult = { content: "", error: resolved.error };
|
|
261
|
+
call.markDone(errorResult);
|
|
262
|
+
run.state.calls.set(msg.callId, call);
|
|
263
|
+
// 无子进程执行,清除空 live record(终态由 result 承载)
|
|
264
|
+
node.live = undefined;
|
|
265
|
+
run.state.trace.update(msg.callId, {
|
|
266
|
+
status: "failed",
|
|
267
|
+
result: errorResult,
|
|
268
|
+
completedAt: new Date().toISOString(),
|
|
269
|
+
});
|
|
270
|
+
postAgentResult(run, msg.callId, errorResult, false);
|
|
271
|
+
void deps.store.save(run);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const call = new AgentCall(msg.callId, resolved.opts, node);
|
|
276
|
+
run.state.calls.set(msg.callId, call);
|
|
277
|
+
|
|
278
|
+
// C-3:经 ConcurrencyGate.withSlot 获取并发槽位后执行。
|
|
279
|
+
// gate 管 maxConcurrency=4 + FIFO;executeAgentCall 管 retry/budget/stale-context;
|
|
280
|
+
// runner(runner.run)管 spawn pi 子进程。
|
|
281
|
+
// assignRuntime/replaceRuntime 保证 status==="running" ⟺ runtime defined,
|
|
282
|
+
// 故 run.runtime 在此必存在(dispatchAgentCall 仅从 handleWorkerMessage 调用,
|
|
283
|
+
// 后者已守 paused/terminal 早期 return)。fallback new AbortController 已移除。
|
|
284
|
+
const runtime = run.runtime!;
|
|
285
|
+
const signal = runtime.controller.signal;
|
|
286
|
+
// D-005: onEvent 签名升级——executeAndAwait 直接出 AgentEvent(强类型,
|
|
287
|
+
// session-runner handleSdkEvent 出口),不再有 raw JSONL 中间层。
|
|
288
|
+
// 删 jsonlToAgentEvent 翻译——直接 updateFromEvent。
|
|
289
|
+
// TUI 靠 tick 轮询 trace.toArray() 读 node.live,无需显式通知。
|
|
290
|
+
const onEvent = (event: AgentEvent): void => {
|
|
291
|
+
updateFromEvent(liveRecord, event);
|
|
292
|
+
};
|
|
293
|
+
void runtime.gate
|
|
294
|
+
.withSlot(() => executeAgentCall(call, deps.runner, run.state.budget, signal, run.state.trace, onEvent), signal)
|
|
295
|
+
.then(() => {
|
|
296
|
+
// pause/abort 后到达的 stale completion 不写 state(pause 是干净快照)
|
|
297
|
+
if (run.state.status !== "running") return;
|
|
298
|
+
// 清除 live record:终态已由 executeAgentCall → finalizeCall 写入 node.result,
|
|
299
|
+
// live 不再需要(且含可变状态,不保留)。无论 stale 与否都清,避免内存泄漏。
|
|
300
|
+
node.live = undefined;
|
|
301
|
+
if (call.result) postAgentResult(run, msg.callId, call.result, false);
|
|
302
|
+
// D-12 regression fix (round-2 #1):executeAgentCall 内 consume/incrementCallCount
|
|
303
|
+
// 后同步 worker $BUDGET(否则 $BUDGET.spent()/remaining() 恒为 0)
|
|
304
|
+
postBudgetUpdate(run);
|
|
305
|
+
void deps.store.save(run);
|
|
306
|
+
|
|
307
|
+
// C-2:budget 超限 → 终止整个 run(避免继续 spawn 烧预算)
|
|
308
|
+
// 内联 terminate(不调 lifecycle.abortRun 避免 engine 内循环依赖):
|
|
309
|
+
// 若 run 仍非终态,transition done,budget_limited + 持久化。
|
|
310
|
+
// 上方 status !== "running" 已保证此处非 done(且 transition 内含 done no-op 守卫)。
|
|
311
|
+
if (run.state.budget.isExceeded()) {
|
|
312
|
+
run.state.error = run.state.error ?? "Budget exceeded";
|
|
313
|
+
deps.log?.("debug", "workflow:error-recovery", "budget exceeded, transition done", { runId: run.runId });
|
|
314
|
+
try {
|
|
315
|
+
run.transition("done", "budget_limited");
|
|
316
|
+
void deps.store.save(run);
|
|
317
|
+
deps.log?.("debug", "workflow:error-recovery", "run saved after budget done", { runId: run.runId, reason: run.state.reason });
|
|
318
|
+
// C-4: budget 终止也触发完成通知
|
|
319
|
+
deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister", { runId: run.runId, reason: run.state.reason });
|
|
320
|
+
deps.eventBus?.emit("pending:unregister", { id: run.runId, reason: run.state.reason ?? "completed" });
|
|
321
|
+
deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister done", { runId: run.runId });
|
|
322
|
+
deps.onRunDone?.(run);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
// run 可能在 budget 检查后、transition 前被并发 abort——忽略
|
|
325
|
+
void err;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
.catch((err: unknown) => {
|
|
330
|
+
// withSlot 在 queued + signal-aborted 时 reject AbortError——预期,不记错。
|
|
331
|
+
// executeAgentCall 本身不 reject(runner.run 不 reject)。
|
|
332
|
+
if (err instanceof Error && err.name === "AbortError") return;
|
|
333
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
334
|
+
console.error(`[workflow] agent call ${msg.callId} failed: ${message}`);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 派发 workflow 嵌套调用:调 deps.onWorkflowCall 获取子 workflow 结果,
|
|
340
|
+
* 异步 postMessage(workflow-result) 回 worker。
|
|
341
|
+
*
|
|
342
|
+
* onWorkflowCall 未注入时(向后兼容),返回 error result 让脚本 soft-fail。
|
|
343
|
+
* 与 dispatchAgentCall 对称:异步触发(不 await),stale 完成守卫(paused/terminal 不发)。
|
|
344
|
+
*/
|
|
345
|
+
function dispatchWorkflowCall(
|
|
346
|
+
run: WorkflowRun,
|
|
347
|
+
msg: WorkflowCallMsg,
|
|
348
|
+
deps: LifecycleDeps,
|
|
349
|
+
): void {
|
|
350
|
+
const postResult = (result: unknown): void => {
|
|
351
|
+
if (run.state.status !== "running") return;
|
|
352
|
+
run.runtime?.worker.postMessage({
|
|
353
|
+
type: "workflow-result",
|
|
354
|
+
callId: msg.callId,
|
|
355
|
+
result,
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
if (!deps.onWorkflowCall) {
|
|
360
|
+
postResult({
|
|
361
|
+
content: "",
|
|
362
|
+
error: `workflow() not supported: onWorkflowCall not injected`,
|
|
363
|
+
});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
void deps
|
|
368
|
+
.onWorkflowCall(msg.name, msg.args, run)
|
|
369
|
+
.then(postResult)
|
|
370
|
+
.catch((err: unknown) => {
|
|
371
|
+
postResult({
|
|
372
|
+
content: "",
|
|
373
|
+
error: err instanceof Error ? err.message : String(err),
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 回发 agent-result 给 worker(worker 内 pending Promise 据此 resolve)。
|
|
380
|
+
*/
|
|
381
|
+
function postAgentResult(
|
|
382
|
+
run: WorkflowRun,
|
|
383
|
+
callId: number,
|
|
384
|
+
result: AgentResult,
|
|
385
|
+
cached: boolean,
|
|
386
|
+
): void {
|
|
387
|
+
run.runtime?.worker.postMessage({ type: "agent-result", callId, result, cached });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* 回发 budget-update 给 worker($BUDGET 据 worker-script-builder 的 budget-update 分支
|
|
392
|
+
* 更新 spent()/remaining())。每次 agent 调用消费 usage 后发送,保持 worker 内 $BUDGET
|
|
393
|
+
* 与主线程 Budget 值对象同步。
|
|
394
|
+
*
|
|
395
|
+
* D-12 regression fix (round-2 #1):重建 budget-update 发送方。被 error-recovery(dispatch
|
|
396
|
+
* 后)和 node-ops(retry/skip 后)共用——单一实现,避免消息形状漂移。
|
|
397
|
+
*/
|
|
398
|
+
export function postBudgetUpdate(run: WorkflowRun): void {
|
|
399
|
+
run.runtime?.worker.postMessage({
|
|
400
|
+
type: "budget-update",
|
|
401
|
+
budget: {
|
|
402
|
+
usedTokens: run.state.budget.usedTokens,
|
|
403
|
+
usedCost: run.state.budget.usedCost,
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 处理脚本的 return 消息:transition done,completed + 持久化。
|
|
410
|
+
*/
|
|
411
|
+
async function handleReturn(
|
|
412
|
+
run: WorkflowRun,
|
|
413
|
+
msg: ReturnMsg,
|
|
414
|
+
deps: LifecycleDeps,
|
|
415
|
+
): Promise<void> {
|
|
416
|
+
deps.log?.("debug", "workflow:error-recovery", "handleReturn", { runId: run.runId, status: run.state.status });
|
|
417
|
+
// 捕获 worker 诊断日志(P2-2)
|
|
418
|
+
if (msg.workerLogs && msg.workerLogs.length > 0) {
|
|
419
|
+
run.state.errorLogs = msg.workerLogs;
|
|
420
|
+
}
|
|
421
|
+
run.state.scriptResult = msg.result;
|
|
422
|
+
run.transition("done", "completed");
|
|
423
|
+
await deps.store.save(run);
|
|
424
|
+
deps.log?.("debug", "workflow:error-recovery", "run saved after return", { runId: run.runId, reason: run.state.reason });
|
|
425
|
+
// C-4: run 到达 done 终态 → 注销 pending-notification + 通知 Interface 层
|
|
426
|
+
deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister", { runId: run.runId, reason: run.state.reason });
|
|
427
|
+
deps.eventBus?.emit("pending:unregister", { id: run.runId, reason: run.state.reason ?? "completed" });
|
|
428
|
+
deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister done", { runId: run.runId });
|
|
429
|
+
deps.onRunDone?.(run);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ── handleWorkerError ────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* 处理 worker 线程 uncaught error。
|
|
436
|
+
*
|
|
437
|
+
* 重试矩阵(domain-models.md §失败处理矩阵):
|
|
438
|
+
* - run.meta.workerErrorCount(C.5,跨 runtime 存活)< MAX → 退避 + rebuildRuntime
|
|
439
|
+
* - >= MAX → transition done,failed
|
|
440
|
+
*
|
|
441
|
+
* @throws 不抛错——所有失败路径转 transition 或日志
|
|
442
|
+
*/
|
|
443
|
+
export async function handleWorkerError(
|
|
444
|
+
run: WorkflowRun,
|
|
445
|
+
err: Error,
|
|
446
|
+
deps: LifecycleDeps,
|
|
447
|
+
handlers: WorkerHandlers,
|
|
448
|
+
): Promise<void> {
|
|
449
|
+
// 与 handleWorkerMessage 对称——paused/terminal 状态丢弃 stale error。
|
|
450
|
+
// 否则 paused 后到达的 worker error 仍会 workerErrorCount++(污染跨 runtime 计数)。
|
|
451
|
+
if (isTerminal(run) || run.state.status === "paused") return;
|
|
452
|
+
|
|
453
|
+
const count = (run.meta.workerErrorCount ?? 0) + 1;
|
|
454
|
+
run.meta.workerErrorCount = count;
|
|
455
|
+
|
|
456
|
+
if (count <= MAX_WORKER_RETRIES) {
|
|
457
|
+
await scheduleRebuild(run, deps, handlers);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// 超限 → failed
|
|
462
|
+
run.state.error = err.message;
|
|
463
|
+
deps.log?.("debug", "workflow:error-recovery", "handleWorkerError retries exceeded, transition done", { runId: run.runId, count });
|
|
464
|
+
run.transition("done", "failed");
|
|
465
|
+
await deps.store.save(run);
|
|
466
|
+
deps.log?.("debug", "workflow:error-recovery", "run saved after worker error", { runId: run.runId, reason: run.state.reason });
|
|
467
|
+
// C-4: run 到达 done 终态 → 注销 pending-notification + 通知 Interface 层
|
|
468
|
+
deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister", { runId: run.runId, reason: run.state.reason });
|
|
469
|
+
deps.eventBus?.emit("pending:unregister", { id: run.runId, reason: run.state.reason ?? "completed" });
|
|
470
|
+
deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister done", { runId: run.runId });
|
|
471
|
+
deps.onRunDone?.(run);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ── handleWorkerExit ─────────────────────────────────────────
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* 处理 worker 线程 exit。
|
|
478
|
+
*
|
|
479
|
+
* code === 0 → 正常退出(脚本主动 return 或自然结束),no-op
|
|
480
|
+
* code !== 0 → 委托 handleWorkerError(非零 exit 视为崩溃)
|
|
481
|
+
*
|
|
482
|
+
* **G-025 竞态防护**:检查 handle.isCurrent——stale exit 事件(已 terminate 的旧
|
|
483
|
+
* worker 的 exit)直接丢弃,不影响当前 runtime 的新 worker。
|
|
484
|
+
*/
|
|
485
|
+
export async function handleWorkerExit(
|
|
486
|
+
run: WorkflowRun,
|
|
487
|
+
code: number,
|
|
488
|
+
handle: WorkerHandle,
|
|
489
|
+
deps: LifecycleDeps,
|
|
490
|
+
handlers: WorkerHandlers,
|
|
491
|
+
): Promise<void> {
|
|
492
|
+
// G-025: stale exit 事件丢弃(handle 已不是当前 runtime 的 worker)
|
|
493
|
+
if (!handle.isCurrent) return;
|
|
494
|
+
if (isTerminal(run) || run.state.status === "paused") return;
|
|
495
|
+
|
|
496
|
+
if (code === 0) return; // 正常退出,no-op
|
|
497
|
+
|
|
498
|
+
// 非零 exit → 委托 handleWorkerError(C.3: onExit 传 handle 用于竞态防护)
|
|
499
|
+
await handleWorkerError(
|
|
500
|
+
run,
|
|
501
|
+
new Error(`Worker exited with code ${code}`),
|
|
502
|
+
deps,
|
|
503
|
+
handlers,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ── handleScriptError ────────────────────────────────────────
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* 处理脚本主动抛出的 error(type:"error" from worker)。
|
|
511
|
+
*
|
|
512
|
+
* 重试矩阵:
|
|
513
|
+
* - run.meta.scriptErrorCount(C.5)< MAX → 退避 + rebuildRuntime(N2: 补全重建)
|
|
514
|
+
* - >= MAX → transition done,failed
|
|
515
|
+
*
|
|
516
|
+
* @param workerLogs worker console.* 捕获(P2-2,存 run.state.errorLogs 供 TUI 展示)
|
|
517
|
+
*/
|
|
518
|
+
export async function handleScriptError(
|
|
519
|
+
run: WorkflowRun,
|
|
520
|
+
errorMsg: string,
|
|
521
|
+
workerLogs: WorkerLogEntry[],
|
|
522
|
+
deps: LifecycleDeps,
|
|
523
|
+
handlers: WorkerHandlers,
|
|
524
|
+
): Promise<void> {
|
|
525
|
+
// 与 handleWorkerMessage/handleWorkerError 对称——paused/terminal 守卫前置。
|
|
526
|
+
if (isTerminal(run) || run.state.status === "paused") return;
|
|
527
|
+
|
|
528
|
+
// P2-2: 捕获 worker 诊断日志
|
|
529
|
+
if (workerLogs.length > 0) {
|
|
530
|
+
run.state.errorLogs = workerLogs;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const count = (run.meta.scriptErrorCount ?? 0) + 1;
|
|
534
|
+
run.meta.scriptErrorCount = count;
|
|
535
|
+
|
|
536
|
+
if (count <= MAX_WORKER_RETRIES) {
|
|
537
|
+
await scheduleRebuild(run, deps, handlers);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// 超限 → failed
|
|
542
|
+
run.state.error = `Workflow failed after ${MAX_WORKER_RETRIES} retries: ${errorMsg}`;
|
|
543
|
+
deps.log?.("debug", "workflow:error-recovery", "handleScriptError retries exceeded, transition done", { runId: run.runId, count });
|
|
544
|
+
run.transition("done", "failed");
|
|
545
|
+
await deps.store.save(run);
|
|
546
|
+
deps.log?.("debug", "workflow:error-recovery", "run saved after script error", { runId: run.runId, reason: run.state.reason });
|
|
547
|
+
// C-4: run 到达 done 终态 → 注销 pending-notification + 通知 Interface 层
|
|
548
|
+
deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister", { runId: run.runId, reason: run.state.reason });
|
|
549
|
+
deps.eventBus?.emit("pending:unregister", { id: run.runId, reason: run.state.reason ?? "completed" });
|
|
550
|
+
deps.log?.("debug", "workflow:error-recovery", "emit pending:unregister done", { runId: run.runId });
|
|
551
|
+
deps.onRunDone?.(run);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ── scheduleRebuild(退避 + 重建) ──────────────────────────
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* 退避后重建 RunRuntime(G3-001 整重建)。
|
|
558
|
+
*
|
|
559
|
+
* 退避期间 run 可能被 pause/abort——rebuildRuntime 前重检状态,paused/terminal 时
|
|
560
|
+
* 跳过重建(避免给已暂停的 run 启新 worker)。
|
|
561
|
+
*/
|
|
562
|
+
async function scheduleRebuild(
|
|
563
|
+
run: WorkflowRun,
|
|
564
|
+
deps: LifecycleDeps,
|
|
565
|
+
handlers: WorkerHandlers,
|
|
566
|
+
): Promise<void> {
|
|
567
|
+
// 用当前重试计数算退避(workerErrorCount 或 scriptErrorCount 已递增)
|
|
568
|
+
const retryIndex = Math.max(
|
|
569
|
+
run.meta.workerErrorCount ?? 0,
|
|
570
|
+
run.meta.scriptErrorCount ?? 0,
|
|
571
|
+
);
|
|
572
|
+
await delay(backoffDelay(retryIndex));
|
|
573
|
+
|
|
574
|
+
// 退避期间状态可能变化——重检
|
|
575
|
+
if (isTerminal(run) || run.state.status !== "running") return;
|
|
576
|
+
|
|
577
|
+
rebuildRuntime(run, deps, handlers);
|
|
578
|
+
}
|