@zhushanwen/pi-subagent-workflow 0.3.3 → 0.4.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/explorer.md +2 -2
- package/agents/orchestrator.md +7 -2
- package/agents/researcher.md +3 -3
- package/package.json +1 -1
- package/src/execution/__tests__/agent-registry.test.ts +19 -2
- package/src/execution/__tests__/format.test.ts +15 -1
- package/src/execution/__tests__/sdk-contract.test.ts +9 -11
- package/src/execution/__tests__/spawn-args.test.ts +18 -1
- package/src/execution/__tests__/subagent-service.test.ts +4 -1
- package/src/execution/__tests__/tool-action.test.ts +10 -5
- package/src/execution/model-resolver.ts +1 -1
- package/src/execution/subagent-service.ts +1 -1
- package/src/interface/__tests__/detectors.test.ts +3 -28
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +54 -11
- package/src/interface/__tests__/tool-render.test.ts +122 -0
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +1 -1
- package/src/interface/format.ts +11 -7
- package/src/interface/subagent-actions.ts +16 -5
- package/src/interface/subagent-tool.ts +81 -98
- package/src/interface/tool-render.ts +9 -11
- package/src/interface/tool-workflow.ts +10 -90
- package/src/orchestration/error-recovery.ts +2 -2
- package/src/orchestration/models/ports.ts +3 -3
- package/src/orchestration/models/workflow-run.ts +3 -3
- package/src/orchestration/worker-script-builder.ts +1 -1
- package/src/orchestration/node-ops.ts +0 -194
|
@@ -31,23 +31,6 @@ import { type RenderContext,renderSubagentCall, renderSubagentResult } from "./t
|
|
|
31
31
|
* 无法从 SubagentParams schema 反向推断参数类型)。
|
|
32
32
|
* action 与对应 param 不匹配时 handler 内 throw。
|
|
33
33
|
*/
|
|
34
|
-
interface StartParam {
|
|
35
|
-
task: string;
|
|
36
|
-
/** 短标签(≤35 字符,kebab-case),必填。展示在 TUI 标题行/列表。 */
|
|
37
|
-
slug: string;
|
|
38
|
-
agent?: string;
|
|
39
|
-
model?: string;
|
|
40
|
-
thinkingLevel?: string;
|
|
41
|
-
skillPath?: string;
|
|
42
|
-
appendSystemPrompt?: string[];
|
|
43
|
-
schema?: Record<string, unknown>;
|
|
44
|
-
maxTurns?: number;
|
|
45
|
-
graceTurns?: number;
|
|
46
|
-
fork?: boolean;
|
|
47
|
-
worktree?: boolean;
|
|
48
|
-
cwd?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
34
|
interface ListParam {
|
|
52
35
|
includeFinished?: boolean;
|
|
53
36
|
limit?: number;
|
|
@@ -59,7 +42,22 @@ interface CancelParam {
|
|
|
59
42
|
|
|
60
43
|
interface SubagentExecuteParams {
|
|
61
44
|
action: "start" | "list" | "cancel";
|
|
62
|
-
startParam
|
|
45
|
+
// action:"start" 的 13 字段拍平到顶层(弱模型常省略 startParam 嵌套层导致调用失败)。
|
|
46
|
+
// 拍平后这些字段直接在顶层(全部 optional——schema flat 无法表达「action 条件必填」,
|
|
47
|
+
// 由 startHandler runtime 校验 task/slug 必填)。
|
|
48
|
+
task?: string;
|
|
49
|
+
slug?: string;
|
|
50
|
+
agent?: string;
|
|
51
|
+
model?: string;
|
|
52
|
+
thinkingLevel?: string;
|
|
53
|
+
skillPath?: string;
|
|
54
|
+
appendSystemPrompt?: string[];
|
|
55
|
+
schema?: Record<string, unknown>;
|
|
56
|
+
maxTurns?: number;
|
|
57
|
+
graceTurns?: number;
|
|
58
|
+
fork?: boolean;
|
|
59
|
+
worktree?: boolean;
|
|
60
|
+
cwd?: string;
|
|
63
61
|
listParam?: ListParam;
|
|
64
62
|
cancelParam?: CancelParam;
|
|
65
63
|
}
|
|
@@ -89,54 +87,56 @@ type SubagentRenderResultCb = (
|
|
|
89
87
|
|
|
90
88
|
// Params schema(模块内消费,未导出)。
|
|
91
89
|
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
90
|
+
// action:"start" 的 13 字段(task/slug/agent/model/...)拍平在顶层,不再用 startParam
|
|
91
|
+
// 嵌套容器包。原因:弱模型(GLM/DeepSeek)信任 schema 结构信号 > 文本信号,经常省略
|
|
92
|
+
// startParam 嵌套层把 task/slug 直接平铺到顶层导致调用失败。拍平后 schema 结构与模型
|
|
93
|
+
// 的自然倾向一致,消除这层误用。task/slug 必填性由 startHandler runtime 校验(flat
|
|
94
|
+
// JSON Schema 无法表达「action 条件必填」)。
|
|
95
|
+
//
|
|
96
|
+
// TODO(long-term, option-A): listParam/cancelParam 仍标 Optional 也是 flat JSON Schema
|
|
97
|
+
// 表达「action 分发条件必填」的妥协——长期方案是拆成 3 个独立 tool
|
|
98
|
+
// (subagent_start / subagent_list / subagent_cancel),让每个 tool 的 schema 真实
|
|
99
|
+
// 反映必填性。勿在此基础上继续堆 action 条件逻辑——要加就拆 tool。
|
|
99
100
|
const SubagentParams = Type.Object({
|
|
100
101
|
action: StringEnum(["start", "list", "cancel"], {
|
|
101
102
|
description: "Operation: 'start' runs a subagent, 'list' shows running subagents (optional includeFinished), 'cancel' stops a background subagent by id.",
|
|
102
103
|
}),
|
|
103
|
-
// action:"start"
|
|
104
|
+
// ── action:"start" fields (flattened to top level). task/slug REQUIRED for start. ──
|
|
105
|
+
// Missing/empty task or slug throws at runtime (startHandler).
|
|
104
106
|
// (flat JSON Schema can't express conditional requirement — see file-level TODO.)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
agent:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
worktree:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
description: 'Override the working directory for the subagent execution. Must be an absolute path. Defaults to the parent session\'s cwd.',
|
|
139
|
-
})),
|
|
107
|
+
task: Type.Optional(Type.String({
|
|
108
|
+
description: "REQUIRED for action:'start'. The task for the subagent to execute. Throws if missing or whitespace-only.",
|
|
109
|
+
})),
|
|
110
|
+
slug: Type.Optional(Type.String({
|
|
111
|
+
description:
|
|
112
|
+
"REQUIRED for action:'start'. Short label (≤35 chars) for this subagent, e.g. 'fix-login', 'extract-urls'. " +
|
|
113
|
+
"Shown in TUI to distinguish concurrent subagents.",
|
|
114
|
+
maxLength: SLUG_MAX_LENGTH,
|
|
115
|
+
})),
|
|
116
|
+
agent: Type.Optional(Type.String({
|
|
117
|
+
description: 'Agent name (system prompt + tools). If omitted, defaults to "general-purpose" — a generic agent that inherits the main agent\'s model and project context. Available: general-purpose (default fallback), worker, researcher, explorer, planner, reviewer, oracle, context-builder, orchestrator. Custom agents configurable.',
|
|
118
|
+
})),
|
|
119
|
+
model: Type.Optional(Type.String({
|
|
120
|
+
description: 'Model override in "provider/modelId" format. Resolution order (top wins): (1) this param, (2) agent .md frontmatter model, (3) the main agent\'s current model (zero-config default). An explicit model (param or frontmatter) that is missing or unauthorized THROWS — there is no silent fallback to the main model. Omit this param to inherit the main model.',
|
|
121
|
+
})),
|
|
122
|
+
thinkingLevel: Type.Optional(StringEnum(["off", "minimal", "low", "medium", "high", "xhigh"] as const)),
|
|
123
|
+
skillPath: Type.Optional(Type.String()),
|
|
124
|
+
appendSystemPrompt: Type.Optional(Type.Array(Type.String())),
|
|
125
|
+
schema: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
126
|
+
maxTurns: Type.Optional(Type.Number({
|
|
127
|
+
description: "Turn limit. The subagent is terminated via SIGTERM after maxTurns turn_end events + graceTurns of slack. There is no graceful wrap-up message — the process is killed. 0 or omitted = unlimited.",
|
|
128
|
+
})),
|
|
129
|
+
graceTurns: Type.Optional(Type.Number({
|
|
130
|
+
description: "Extra turns allowed after maxTurns is reached before SIGTERM (default 2). Only meaningful when maxTurns is set.",
|
|
131
|
+
})),
|
|
132
|
+
fork: Type.Optional(Type.Boolean({
|
|
133
|
+
description: "Fork mode: inherit the parent's conversation context. When true, the subagent receives the parent's session file via --fork and builds a branched conversation (it sees prior turns/messages). The subagent still runs in a separate spawned child process (process isolation) — fork is about context inheritance, not process sharing. Use worktree:true (requires fork:true) for file-system isolation.",
|
|
134
|
+
})),
|
|
135
|
+
worktree: Type.Optional(Type.Boolean({
|
|
136
|
+
description: "Worktree isolation (requires fork:true): run the subagent in a dedicated git worktree, providing file-system level isolation from the parent session. Prevents concurrent file-write conflicts between parent and subagent. Only takes effect when fork:true; passing worktree:true without fork:true throws an error.",
|
|
137
|
+
})),
|
|
138
|
+
cwd: Type.Optional(Type.String({
|
|
139
|
+
description: 'Override the working directory for the subagent execution. Must be an absolute path. Defaults to the parent session\'s cwd.',
|
|
140
140
|
})),
|
|
141
141
|
// action:"list" → listParam OPTIONAL (all fields optional, defaults apply). Ignored by other actions.
|
|
142
142
|
listParam: Type.Optional(Type.Object({
|
|
@@ -171,22 +171,8 @@ function isModelOverrideObj(a: unknown): a is { model?: unknown; thinkingLevel?:
|
|
|
171
171
|
return typeof a === "object" && a !== null;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
/** unknown args
|
|
175
|
-
|
|
176
|
-
return typeof a === "object" && a !== null && "startParam" in a;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** action:'start' 入参是否把 task/slug 平铺到顶层(弱模型常见误用:缺 startParam 嵌套)。 */
|
|
180
|
-
/**
|
|
181
|
-
* action:'start' 入参是否把 task/slug 平铺到顶层(弱模型常见误用:缺 startParam 嵌套)。
|
|
182
|
-
* export 供 behavioral 测试(trigger/no-trigger),不改变运行时行为。
|
|
183
|
-
*/
|
|
184
|
-
export function hasFlattenedStartFields(a: unknown): boolean {
|
|
185
|
-
if (typeof a !== "object" || a === null) return false;
|
|
186
|
-
return "task" in a || "slug" in a;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** 从 unknown args 安全提取 model/thinkingLevel override(传给 resolveModel)。 */
|
|
174
|
+
/** 从 unknown args 安全提取 model/thinkingLevel override(传给 resolveModel)。
|
|
175
|
+
* 拍平后 args 已是顶层平铺结构(model/thinkingLevel 直接在 args 上)。 */
|
|
190
176
|
function extractModelOverride(args: unknown): { model?: string; thinkingLevel?: string } | undefined {
|
|
191
177
|
if (!isModelOverrideObj(args)) return undefined;
|
|
192
178
|
const override: { model?: string; thinkingLevel?: string } = {};
|
|
@@ -214,17 +200,17 @@ Delegate when the task needs a distinct role (researcher/worker), context isolat
|
|
|
214
200
|
|
|
215
201
|
## Actions
|
|
216
202
|
|
|
217
|
-
- action:"start" — run a subagent. REQUIRED
|
|
203
|
+
- action:"start" — run a subagent. Pass task and slug as top-level fields (REQUIRED). Optional: agent, model, thinkingLevel, skillPath, appendSystemPrompt, schema, maxTurns, graceTurns, fork, worktree, cwd. Background only: returns a subagentId immediately, notifies on completion.
|
|
218
204
|
- action:"list" — list subagents. Pass listParam: { includeFinished?, limit? } (all optional). Read an item's sessionFile for full detail.
|
|
219
205
|
- action:"cancel" — cancel a background subagent. REQUIRED cancelParam: { subagentId }.
|
|
220
206
|
|
|
221
207
|
## Examples
|
|
222
208
|
|
|
223
209
|
\`\`\`
|
|
224
|
-
{"action":"start","
|
|
225
|
-
{"action":"start","
|
|
210
|
+
{"action":"start","task":"<your task>","slug":"<kebab-case>"}
|
|
211
|
+
{"action":"start","task":"...","slug":"fix-login","agent":"worker","model":"anthropic/claude-3.5-sonnet","fork":true}
|
|
226
212
|
{"action":"list","listParam":{"includeFinished":false,"limit":20}}
|
|
227
|
-
{"action":"cancel","cancelParam":{"subagentId":"
|
|
213
|
+
{"action":"cancel","cancelParam":{"subagentId":"sa-550e8400"}}
|
|
228
214
|
\`\`\`
|
|
229
215
|
|
|
230
216
|
## After launching — do NOT wait
|
|
@@ -237,7 +223,8 @@ Completion auto-notifies you (steer wakes next turn, even mid-poll). So:
|
|
|
237
223
|
|
|
238
224
|
## Anti-patterns
|
|
239
225
|
|
|
240
|
-
-
|
|
226
|
+
- Forgetting the REQUIRED top-level task/slug fields for action:"start" — both must be present at the top level (not nested).
|
|
227
|
+
- Over-generalizing the flatten: ONLY start fields are top-level. list and cancel params stay nested under listParam / cancelParam (e.g. {"action":"list","listParam":{"includeFinished":true}}, NOT {"action":"list","includeFinished":true}).
|
|
241
228
|
- Launching background, then sleeping/polling instead of working or stopping.
|
|
242
229
|
- Treating subagent results as authoritative without verification.
|
|
243
230
|
- Delegating trivial tasks you could do faster yourself.
|
|
@@ -278,9 +265,10 @@ const subagentRenderCall: SubagentRenderCallCb = (args, theme, ctx) => {
|
|
|
278
265
|
// 主 agent model 由 ModelConfigService 缓存(session_start 注入,model_select 刷新),
|
|
279
266
|
// 补偿 renderCall 的 ToolRenderContext 不含 model 的 SDK 限制。
|
|
280
267
|
// service 未就绪 / 缓存为空 / 解析失败 → 降级不显示 model。
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
268
|
+
// 拍平后 args 已是顶层平铺结构(agent/model/thinkingLevel 直接在 args 上),
|
|
269
|
+
// extractAgentName / extractModelOverride 都是 unknown-safe 顶层读取,对平铺形态天然兼容。
|
|
270
|
+
const agent = extractAgentName(args);
|
|
271
|
+
const override = extractModelOverride(args);
|
|
284
272
|
let resolved: { model: string; thinkingLevel?: string } | undefined;
|
|
285
273
|
try {
|
|
286
274
|
const service = getSubagentService();
|
|
@@ -309,7 +297,7 @@ const subagentRenderResult: SubagentRenderResultCb = (result, options, theme, ct
|
|
|
309
297
|
* ║ service = getSubagentService() —— 未初始化 throw ║
|
|
310
298
|
* ║ ║
|
|
311
299
|
* ║ switch(params.action): ║
|
|
312
|
-
* ║ "start" → startHandler(service, params
|
|
300
|
+
* ║ "start" → startHandler(service, params, signal) → 领域对象 ║
|
|
313
301
|
* ║ "list" → listHandler(service, params.listParam) → 领域对象 ║
|
|
314
302
|
* ║ "cancel" → cancelHandler(service, params.cancelParam) → 领域对象║
|
|
315
303
|
* ║ ║
|
|
@@ -317,6 +305,10 @@ const subagentRenderResult: SubagentRenderResultCb = (result, options, theme, ct
|
|
|
317
305
|
* ║ return { content: [{text: JSON.stringify(result)}], details: result }║
|
|
318
306
|
* ╚══════════════════════════════════════════════════════════════════╝
|
|
319
307
|
*
|
|
308
|
+
* 拍平后 startHandler 直接接收顶层 params(13 字段已在顶层)。startHandler 的入参
|
|
309
|
+
* 类型 StartHandlerInput 是 SubagentExecuteParams 的子集(13 字段全 optional),
|
|
310
|
+
* 结构兼容——SubagentExecuteParams 多出的 action/listParam/cancelParam 被忽略。
|
|
311
|
+
*
|
|
320
312
|
* handler 返回纯领域对象(不碰 {content, details}),adapter 唯一包装。
|
|
321
313
|
* content(JSON 字符串)给 LLM,details(领域对象 + action)给 renderResult,同源。
|
|
322
314
|
*/
|
|
@@ -332,20 +324,11 @@ const executeSubagent: SubagentExecuteCb = async (
|
|
|
332
324
|
const service = getSubagentService();
|
|
333
325
|
if (!service) throw new Error("subagents runtime not initialized");
|
|
334
326
|
|
|
335
|
-
// 弱模型常见误用:action:'start' 时把 task/slug 平铺到顶层(缺 startParam 嵌套层)。
|
|
336
|
-
// schema 用 Type.Optional 表达条件必填(flat JSON Schema 无法表达),弱模型信任
|
|
337
|
-
// 结构信号 > 文本信号,倾向省略嵌套层。这里在进 startHandler 之前拦截平铺形态,
|
|
338
|
-
// throw 带 Correct 正例,让弱模型撞错后第二次能直接照抄。
|
|
339
|
-
if (params.action === "start" && !params.startParam && hasFlattenedStartFields(params)) {
|
|
340
|
-
throw new Error(
|
|
341
|
-
"startParam is required for action:'start' — wrap task/slug inside startParam. " +
|
|
342
|
-
"Correct: {\"action\":\"start\",\"startParam\":{\"task\":\"<your task>\",\"slug\":\"<kebab-case>\"}}",
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
327
|
switch (params.action) {
|
|
347
328
|
case "start":
|
|
348
|
-
|
|
329
|
+
// 拍平后直接传顶层 params(StartHandlerInput 是 SubagentExecuteParams 子集,
|
|
330
|
+
// action/listParam/cancelParam 被忽略;task/slug 必填性由 startHandler 校验)。
|
|
331
|
+
return adapter({ action: "start", domain: await startHandler(service, params, signal, _ctx?.model) }, toGuiCtx(_ctx));
|
|
349
332
|
case "list":
|
|
350
333
|
return adapter({ action: "list", domain: listHandler(service, params.listParam) }, toGuiCtx(_ctx));
|
|
351
334
|
case "cancel":
|
|
@@ -92,15 +92,13 @@ export function renderSubagentCall(
|
|
|
92
92
|
resolved?: { model: string; thinkingLevel?: string },
|
|
93
93
|
): Component {
|
|
94
94
|
const t = theme as ThemeLike;
|
|
95
|
-
// args
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const slug = typeof startParam === "object" && startParam !== null && "slug" in startParam
|
|
103
|
-
? (startParam as { slug?: unknown }).slug
|
|
95
|
+
// args 结构(拍平后):{ action:"start", agent, task, slug, ... }(见 subagent-tool.ts schema)。
|
|
96
|
+
// 13 字段直接在顶层,extractAgentName / slug / task 都从 args 顶层提取,
|
|
97
|
+
// 对齐 nicobailon 的 renderCall 多行布局。
|
|
98
|
+
const agent = extractAgentName(args);
|
|
99
|
+
// slug:从顶层 args 提取(必填字段),非空时在 agent 后用 · 分隔展示。
|
|
100
|
+
const slug = typeof args === "object" && args !== null && "slug" in args
|
|
101
|
+
? (args as { slug?: unknown }).slug
|
|
104
102
|
: undefined;
|
|
105
103
|
const slugStr = typeof slug === "string" ? slug.trim() : "";
|
|
106
104
|
const parts = slugStr
|
|
@@ -122,8 +120,8 @@ export function renderSubagentCall(
|
|
|
122
120
|
// task preview 行——对齐 nicobailon:renderCall 输出多行(标题 + \n + task 预览)。
|
|
123
121
|
// 实验假设:call 多行让首帧(无 result)与后续帧(有 result)的高度跳变模式
|
|
124
122
|
// 与 nicobailon 一致,可能影响 pi diff 引擎的行对齐路径。preview 截断到 60 字符。
|
|
125
|
-
const task = typeof
|
|
126
|
-
? (
|
|
123
|
+
const task = typeof args === "object" && args !== null && "task" in args
|
|
124
|
+
? (args as { task?: unknown }).task
|
|
127
125
|
: undefined;
|
|
128
126
|
if (typeof task === "string" && task.length > 0) {
|
|
129
127
|
// task 取首行——prompt 常含换行(多行指令),直接 slice 会保留 \n,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workflow Extension — workflow tool(
|
|
2
|
+
* Workflow Extension — workflow tool(5 actions,FR-5 tool 收口)。
|
|
3
3
|
*
|
|
4
4
|
* 合并原 tool-workflow.ts + tool-workflow-run.ts 为单 tool。
|
|
5
5
|
*
|
|
@@ -9,12 +9,10 @@
|
|
|
9
9
|
* - pause: 调 pauseRun
|
|
10
10
|
* - resume: 调 resumeRun
|
|
11
11
|
* - abort: 调 abortRun
|
|
12
|
-
* - retry-node: 调 retryNode
|
|
13
|
-
* - skip-node: 调 skipNode
|
|
14
12
|
*
|
|
15
13
|
* **restart 不包含**(D-9 废弃)。
|
|
16
14
|
*
|
|
17
|
-
* 层归属:Interface。依赖 Pi SDK + Engine lifecycle/
|
|
15
|
+
* 层归属:Interface。依赖 Pi SDK + Engine lifecycle/launcher + helpers。
|
|
18
16
|
*
|
|
19
17
|
* 参考:domain-models.md §FR-5(tool 收口 4→2)。
|
|
20
18
|
*/
|
|
@@ -36,7 +34,6 @@ import type { LauncherDeps } from "../orchestration/launcher.ts";
|
|
|
36
34
|
import { abortRun, pauseRun, resumeRun, runWorkflow } from "../orchestration/lifecycle.ts";
|
|
37
35
|
import type { RunStore } from "../orchestration/models/ports.ts";
|
|
38
36
|
import type { WorkflowRun } from "../orchestration/models/workflow-run.ts";
|
|
39
|
-
import { retryNode, skipNode } from "../orchestration/node-ops.ts";
|
|
40
37
|
import { mapRunIcon, mapRunStatus, toGuiCtx } from "./gui-mappers.ts";
|
|
41
38
|
import {
|
|
42
39
|
acquireReentryGuard,
|
|
@@ -54,9 +51,7 @@ export type WorkflowAction =
|
|
|
54
51
|
| "status"
|
|
55
52
|
| "pause"
|
|
56
53
|
| "resume"
|
|
57
|
-
| "abort"
|
|
58
|
-
| "retry-node"
|
|
59
|
-
| "skip-node";
|
|
54
|
+
| "abort";
|
|
60
55
|
|
|
61
56
|
const WORKFLOW_ACTIONS: readonly WorkflowAction[] = [
|
|
62
57
|
"run",
|
|
@@ -64,8 +59,6 @@ const WORKFLOW_ACTIONS: readonly WorkflowAction[] = [
|
|
|
64
59
|
"pause",
|
|
65
60
|
"resume",
|
|
66
61
|
"abort",
|
|
67
|
-
"retry-node",
|
|
68
|
-
"skip-node",
|
|
69
62
|
];
|
|
70
63
|
|
|
71
64
|
const WorkflowParams = Type.Object({
|
|
@@ -82,10 +75,7 @@ const WorkflowParams = Type.Object({
|
|
|
82
75
|
}),
|
|
83
76
|
),
|
|
84
77
|
runId: Type.Optional(
|
|
85
|
-
Type.String({ description: "Workflow run ID (pause/resume/abort
|
|
86
|
-
),
|
|
87
|
-
callId: Type.Optional(
|
|
88
|
-
Type.Number({ description: "Agent call ID (retry-node/skip-node)" }),
|
|
78
|
+
Type.String({ description: "Workflow run ID (pause/resume/abort)" }),
|
|
89
79
|
),
|
|
90
80
|
args: Type.Optional(
|
|
91
81
|
Type.Record(Type.String(), Type.Unknown(), {
|
|
@@ -151,8 +141,7 @@ interface RunSummary {
|
|
|
151
141
|
export type WorkflowToolDetails =
|
|
152
142
|
| { action: "run"; runId: string; status: "running" | "not_found"; name: string; slug?: string; stateFile?: string; __gui__?: GuiRenderResult }
|
|
153
143
|
| { action: "status"; runs: RunSummary[]; __gui__?: GuiRenderResult }
|
|
154
|
-
| { action: "pause" | "resume" | "abort"; runId: string; status: string; reason?: string; __gui__?: GuiRenderResult }
|
|
155
|
-
| { action: "retry-node" | "skip-node"; runId: string; callId: number; __gui__?: GuiRenderResult };
|
|
144
|
+
| { action: "pause" | "resume" | "abort"; runId: string; status: string; reason?: string; __gui__?: GuiRenderResult };
|
|
156
145
|
|
|
157
146
|
/** Result returned by the `workflow` tool's execute. */
|
|
158
147
|
export interface ToolResult {
|
|
@@ -206,8 +195,8 @@ export function buildWorkflowGui(details: WorkflowToolDetails) {
|
|
|
206
195
|
}),
|
|
207
196
|
});
|
|
208
197
|
}
|
|
209
|
-
// pause/resume/abort
|
|
210
|
-
// abort 是破坏性终止、pause 是挂起(非成功完成),用 warn 区分;resume
|
|
198
|
+
// pause/resume/abort
|
|
199
|
+
// abort 是破坏性终止、pause 是挂起(非成功完成),用 warn 区分;resume 保留 ok
|
|
211
200
|
const severity = details.action === "abort" || details.action === "pause" ? "warn" as const : "ok" as const;
|
|
212
201
|
return guiComponent("stats-line", {
|
|
213
202
|
items: [{
|
|
@@ -221,7 +210,7 @@ export function buildWorkflowGui(details: WorkflowToolDetails) {
|
|
|
221
210
|
// ── Tool registration ────────────────────────────────────────
|
|
222
211
|
|
|
223
212
|
/**
|
|
224
|
-
* 注册 workflow tool(
|
|
213
|
+
* 注册 workflow tool(5 actions: run / status / pause / resume / abort)。
|
|
225
214
|
*
|
|
226
215
|
* @param pi ExtensionAPI
|
|
227
216
|
* @param deps LauncherDeps(LifecycleDeps + registry)
|
|
@@ -241,9 +230,7 @@ export function registerWorkflowTool(
|
|
|
241
230
|
name: "workflow",
|
|
242
231
|
label: "Workflow",
|
|
243
232
|
description:
|
|
244
|
-
"Execute and control workflows: run (start), status, pause, resume, abort
|
|
245
|
-
"retry-node (re-run a failed agent call to refresh its trace; does NOT resume the " +
|
|
246
|
-
"workflow script or change its output — see promptGuidelines), skip-node (mark a call as skipped).\n" +
|
|
233
|
+
"Execute and control workflows: run (start), status, pause, resume, abort.\n" +
|
|
247
234
|
"Replaces workflow + workflow-run tools.",
|
|
248
235
|
promptSnippet: "Run, pause, resume, abort, or check workflow status",
|
|
249
236
|
promptGuidelines: [
|
|
@@ -259,15 +246,10 @@ export function registerWorkflowTool(
|
|
|
259
246
|
"with source tags and descriptions. Then use this tool's run action to start one.",
|
|
260
247
|
"run: discover by name/description, then start in background (no user confirmation needed).",
|
|
261
248
|
"Do NOT poll status after starting — results appear automatically via notifyDone.",
|
|
262
|
-
"retry-node/skip-node: for specific failed agent calls (requires runId + callId). " +
|
|
263
|
-
"retry-node only re-runs the call and refreshes the trace — the workflow script has " +
|
|
264
|
-
"already moved past the failed call, so the new result does NOT feed back into the " +
|
|
265
|
-
"script flow. Use retry-node for diagnostics, not to resume the workflow.",
|
|
266
249
|
"Call shapes (JSON): " +
|
|
267
250
|
"- run: {\"action\":\"run\",\"name\":\"<script>\",\"args\":{...},\"tokens\":N,\"time\":N}. " +
|
|
268
251
|
"- status: {\"action\":\"status\"}. " +
|
|
269
|
-
"- pause/resume/abort: {\"action\":\"pause\",\"runId\":\"<id>\"} (abort optional: ,\"error\":\"<reason>\"}).
|
|
270
|
-
"- retry-node/skip-node: {\"action\":\"retry-node\",\"runId\":\"<id>\",\"callId\":N}.",
|
|
252
|
+
"- pause/resume/abort: {\"action\":\"pause\",\"runId\":\"<id>\"} (abort optional: ,\"error\":\"<reason>\"}).",
|
|
271
253
|
"Anti-patterns: Flattening args sub-fields (task/items/...) to the top level — they belong inside args. Calling {\"action\":\"run\"} without name.",
|
|
272
254
|
],
|
|
273
255
|
parameters: WorkflowParams,
|
|
@@ -308,12 +290,6 @@ export function registerWorkflowTool(
|
|
|
308
290
|
case "abort":
|
|
309
291
|
result = await actionLifecycle("abort", params, deps);
|
|
310
292
|
break;
|
|
311
|
-
case "retry-node":
|
|
312
|
-
result = await actionRetryNode(params, deps);
|
|
313
|
-
break;
|
|
314
|
-
case "skip-node":
|
|
315
|
-
result = await actionSkipNode(params, deps);
|
|
316
|
-
break;
|
|
317
293
|
default: {
|
|
318
294
|
// Exhaustiveness check — 新增 WorkflowAction 成员时未补 case,tsc 在此报错。
|
|
319
295
|
const _exhaustive: never = action;
|
|
@@ -503,62 +479,6 @@ async function actionLifecycle(
|
|
|
503
479
|
}
|
|
504
480
|
}
|
|
505
481
|
|
|
506
|
-
// ── retry-node / skip-node ───────────────────────────────────
|
|
507
|
-
|
|
508
|
-
async function actionRetryNode(params: WorkflowToolParams, deps: LauncherDeps): Promise<ToolResult> {
|
|
509
|
-
const runId = params.runId;
|
|
510
|
-
const callId = params.callId;
|
|
511
|
-
if (!runId || callId === undefined) {
|
|
512
|
-
return textResult("retry-node requires 'runId' and 'callId'. Correct: {\"action\":\"retry-node\",\"runId\":\"<id>\",\"callId\":<number>}", true);
|
|
513
|
-
}
|
|
514
|
-
const run = deps.runs.get(runId);
|
|
515
|
-
if (!run) {
|
|
516
|
-
return textResult(
|
|
517
|
-
`Workflow '${runId}' not found. Use action:status to list active runs and their runIds.`,
|
|
518
|
-
true,
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
try {
|
|
522
|
-
await retryNode(run, callId, deps);
|
|
523
|
-
return {
|
|
524
|
-
content: [
|
|
525
|
-
{ type: "text", text: `Retried call ${callId} in run ${runId.slice(0, RUNID_SHORT)}.` },
|
|
526
|
-
],
|
|
527
|
-
details: { action: "retry-node", runId, callId },
|
|
528
|
-
};
|
|
529
|
-
} catch (err) {
|
|
530
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
531
|
-
return textResult(`Error: ${msg}`, true);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
async function actionSkipNode(params: WorkflowToolParams, deps: LauncherDeps): Promise<ToolResult> {
|
|
536
|
-
const runId = params.runId;
|
|
537
|
-
const callId = params.callId;
|
|
538
|
-
if (!runId || callId === undefined) {
|
|
539
|
-
return textResult("skip-node requires 'runId' and 'callId'. Correct: {\"action\":\"skip-node\",\"runId\":\"<id>\",\"callId\":<number>}", true);
|
|
540
|
-
}
|
|
541
|
-
const run = deps.runs.get(runId);
|
|
542
|
-
if (!run) {
|
|
543
|
-
return textResult(
|
|
544
|
-
`Workflow '${runId}' not found. Use action:status to list active runs and their runIds.`,
|
|
545
|
-
true,
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
try {
|
|
549
|
-
await skipNode(run, callId, deps);
|
|
550
|
-
return {
|
|
551
|
-
content: [
|
|
552
|
-
{ type: "text", text: `Skipped call ${callId} in run ${runId.slice(0, RUNID_SHORT)}.` },
|
|
553
|
-
],
|
|
554
|
-
details: { action: "skip-node", runId, callId },
|
|
555
|
-
};
|
|
556
|
-
} catch (err) {
|
|
557
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
558
|
-
return textResult(`Error: ${msg}`, true);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
482
|
// ── helpers ──────────────────────────────────────────────────
|
|
563
483
|
|
|
564
484
|
/** WorkflowRun → 摘要(status action 用)。 */
|
|
@@ -492,8 +492,8 @@ function postAgentResult(
|
|
|
492
492
|
* 更新 spent()/remaining())。每次 agent 调用消费 usage 后发送,保持 worker 内 $BUDGET
|
|
493
493
|
* 与主线程 Budget 值对象同步。
|
|
494
494
|
*
|
|
495
|
-
* D-12 regression fix (round-2 #1):重建 budget-update 发送方。被 error-recovery
|
|
496
|
-
*
|
|
495
|
+
* D-12 regression fix (round-2 #1):重建 budget-update 发送方。被 error-recovery 主路径调用
|
|
496
|
+
* (dispatch 后同步 worker $BUDGET)——单一实现,避免消息形状漂移。
|
|
497
497
|
*/
|
|
498
498
|
export function postBudgetUpdate(run: WorkflowRun): void {
|
|
499
499
|
run.runtime?.worker.postMessage({
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 是真需要 mock 测试的依赖(子进程/文件系统/线程)。
|
|
6
6
|
*
|
|
7
7
|
* 编排层共享类型(WorkerHandlers / LifecycleDeps)——打破 lifecycle ↔
|
|
8
|
-
* error-recovery
|
|
8
|
+
* error-recovery 循环依赖:2 个 engine 函数文件各自独立,共用同一组
|
|
9
9
|
* 依赖签名(D-12)。
|
|
10
10
|
*
|
|
11
11
|
* 层归属:Engine。零 infra 依赖(AC-1)。
|
|
@@ -72,7 +72,7 @@ export interface WorkerHost {
|
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Worker 线程事件回调集合——WorkerHost.start 的入参,由 lifecycle
|
|
75
|
-
* 构造并注入。
|
|
75
|
+
* 构造并注入。2 个 engine 文件(lifecycle / error-recovery)共用此签名,
|
|
76
76
|
* 避免各自定义形状不一致的 handler bag(打破循环依赖)。
|
|
77
77
|
*
|
|
78
78
|
* 所有回调返回 Promise——允许 engine 层在回调内做 await persistState 等异步操作。
|
|
@@ -89,7 +89,7 @@ export interface WorkerHandlers {
|
|
|
89
89
|
// ── 编排层共享类型 2: LifecycleDeps ────────────────────────────
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
* lifecycle / error-recovery
|
|
92
|
+
* lifecycle / error-recovery 2 个 engine 函数文件的共同依赖 bag。
|
|
93
93
|
*
|
|
94
94
|
* 取代旧 4 个 Context factory(errorHandlerContext / agentCallContext /
|
|
95
95
|
* budgetCallbacks / 旧 terminate bag,AC-2 目标)。函数签名 `(deps: LifecycleDeps, ...)`
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* (runtime=undefined)。AbortController 一次性无法复用。
|
|
24
24
|
* - resume 走 assignRuntime(new RunRuntime(...)),重建 worker/gate/controller。
|
|
25
25
|
*
|
|
26
|
-
*
|
|
26
|
+
* worker-error-retry(G5-001 + G6-001):
|
|
27
27
|
* - replaceRuntime(newRt): 前置 status==="running"(G6-001),原子释放前一个 runtime
|
|
28
28
|
* + 绑定新 runtime,全程保持不变式 I1(中间不经过 runtime===undefined 的可见状态)。
|
|
29
29
|
* - paused 状态下 retry 被拒(要 retry 先 resume)。
|
|
@@ -243,11 +243,11 @@ export class WorkflowRun {
|
|
|
243
243
|
this.runtime.release("pause");
|
|
244
244
|
this.runtime = undefined;
|
|
245
245
|
// 不改 status——调用方(transition)负责。独立调用时调用方需自行确保
|
|
246
|
-
// status 一致(如
|
|
246
|
+
// status 一致(如 worker-error-retry 用 replaceRuntime 而非 release+assign)。
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
/**
|
|
250
|
-
* 原地替换 runtime(G5-001:
|
|
250
|
+
* 原地替换 runtime(G5-001:worker-error-retry)。
|
|
251
251
|
*
|
|
252
252
|
* 前置:status==="running"(G6-001:paused 下拒绝,要 retry 先 resume)。
|
|
253
253
|
* 原子地:释放旧 runtime(worker.terminate + abort)+ 绑定新 runtime,
|
|
@@ -116,7 +116,7 @@ export function buildWorkerScript(userScript: string): string {
|
|
|
116
116
|
' // 把单点失败放大成整批崩溃。改为始终 resolve(错误时回退到 content 文本),',
|
|
117
117
|
' // 让 parallel() 下的脚本容错循环(parseResult → null → skip)自然接管。',
|
|
118
118
|
' // 错误原因已由主线程 executeAgentCall → trace.update(result.error) 保留在 trace/TUI,',
|
|
119
|
-
' //
|
|
119
|
+
' // 不丢失。失败 resolve 为空字符串是既定容错策略。',
|
|
120
120
|
' // parsedOutput: validated data object from structured-output execute().',
|
|
121
121
|
' // Fallback to content (raw text) when no schema was requested or on error.',
|
|
122
122
|
' pending.resolve(msg.result.parsedOutput ?? msg.result.content);',
|