@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,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Extension — workflow tool(7 actions,FR-5 tool 收口)。
|
|
3
|
+
*
|
|
4
|
+
* 合并原 tool-workflow.ts + tool-workflow-run.ts 为单 tool。
|
|
5
|
+
*
|
|
6
|
+
* Actions:
|
|
7
|
+
* - run: registry.get → runWorkflow(直接启动,无需用户确认)
|
|
8
|
+
* - status: 列出 runs(deps.runs)
|
|
9
|
+
* - pause: 调 pauseRun
|
|
10
|
+
* - resume: 调 resumeRun
|
|
11
|
+
* - abort: 调 abortRun
|
|
12
|
+
* - retry-node: 调 retryNode
|
|
13
|
+
* - skip-node: 调 skipNode
|
|
14
|
+
*
|
|
15
|
+
* **restart 不包含**(D-9 废弃)。
|
|
16
|
+
*
|
|
17
|
+
* 层归属:Interface。依赖 Pi SDK + Engine lifecycle/node-ops/launcher + helpers。
|
|
18
|
+
*
|
|
19
|
+
* 参考:domain-models.md §FR-5(tool 收口 4→2)。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
23
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
24
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
25
|
+
import { type Static, Type } from "typebox";
|
|
26
|
+
|
|
27
|
+
import type { LauncherDeps } from "../orchestration/launcher.ts";
|
|
28
|
+
import { abortRun, pauseRun, resumeRun, runWorkflow } from "../orchestration/lifecycle.ts";
|
|
29
|
+
import type { WorkflowRun } from "../orchestration/models/workflow-run.ts";
|
|
30
|
+
import { retryNode, skipNode } from "../orchestration/node-ops.ts";
|
|
31
|
+
import {
|
|
32
|
+
guiComponent,
|
|
33
|
+
type GuiContext,
|
|
34
|
+
guiResult,
|
|
35
|
+
isGuiCapable,
|
|
36
|
+
} from "./gui-adapter.ts";
|
|
37
|
+
import {
|
|
38
|
+
acquireReentryGuard,
|
|
39
|
+
REENTRY_BUSY_MESSAGE,
|
|
40
|
+
type ReentryGuardRef,
|
|
41
|
+
releaseReentryGuard,
|
|
42
|
+
} from "./reentry-guard.ts";
|
|
43
|
+
import { formatElapsed, renderTextFallback } from "./views/format.ts";
|
|
44
|
+
|
|
45
|
+
// ── Parameter schema ─────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/** workflow tool 的全部 action 枚举值(单一真相源)。 */
|
|
48
|
+
export type WorkflowAction =
|
|
49
|
+
| "run"
|
|
50
|
+
| "status"
|
|
51
|
+
| "pause"
|
|
52
|
+
| "resume"
|
|
53
|
+
| "abort"
|
|
54
|
+
| "retry-node"
|
|
55
|
+
| "skip-node";
|
|
56
|
+
|
|
57
|
+
const WORKFLOW_ACTIONS: readonly WorkflowAction[] = [
|
|
58
|
+
"run",
|
|
59
|
+
"status",
|
|
60
|
+
"pause",
|
|
61
|
+
"resume",
|
|
62
|
+
"abort",
|
|
63
|
+
"retry-node",
|
|
64
|
+
"skip-node",
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const WorkflowParams = Type.Object({
|
|
68
|
+
action: StringEnum(WORKFLOW_ACTIONS, { description: "Workflow action to execute" }),
|
|
69
|
+
name: Type.Optional(
|
|
70
|
+
Type.String({ description: "Workflow name (run action)" }),
|
|
71
|
+
),
|
|
72
|
+
runId: Type.Optional(
|
|
73
|
+
Type.String({ description: "Workflow run ID (pause/resume/abort/retry-node/skip-node)" }),
|
|
74
|
+
),
|
|
75
|
+
callId: Type.Optional(
|
|
76
|
+
Type.Number({ description: "Agent call ID (retry-node/skip-node)" }),
|
|
77
|
+
),
|
|
78
|
+
args: Type.Optional(
|
|
79
|
+
Type.Record(Type.String(), Type.Unknown(), {
|
|
80
|
+
description: "Arguments passed to workflow as key-value pairs (run action)",
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
83
|
+
tokens: Type.Optional(Type.Number({ description: "Maximum token budget (run action)" })),
|
|
84
|
+
time: Type.Optional(Type.Number({ description: "Maximum time budget in ms (run action)" })),
|
|
85
|
+
error: Type.Optional(
|
|
86
|
+
Type.String({ description: "Error/reason message (optional, used with abort)" }),
|
|
87
|
+
),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
type WorkflowToolParams = Static<typeof WorkflowParams>;
|
|
91
|
+
|
|
92
|
+
// ── Constants ────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/** runId 截断长度(显示用)。 */
|
|
95
|
+
const RUNID_SHORT = 8;
|
|
96
|
+
|
|
97
|
+
// ── Types ────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
interface RunSummary {
|
|
100
|
+
runId: string;
|
|
101
|
+
name: string;
|
|
102
|
+
status: string;
|
|
103
|
+
reason?: string;
|
|
104
|
+
startedAt?: string;
|
|
105
|
+
completedAt?: string;
|
|
106
|
+
error?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Tool result types ──
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Discriminated union of `workflow` tool `details` payloads.
|
|
113
|
+
*
|
|
114
|
+
* Discriminant: `action`. Each action's details shape is explicitly typed so
|
|
115
|
+
* downstream consumers (GUI task-list renderer, structured-output) can narrow
|
|
116
|
+
* without unsafe casts.
|
|
117
|
+
*/
|
|
118
|
+
export type WorkflowToolDetails =
|
|
119
|
+
| { action: "run"; runId: string; status: "running" | "not_found"; name: string }
|
|
120
|
+
| { action: "status"; runs: RunSummary[] }
|
|
121
|
+
| { action: "pause" | "resume" | "abort"; runId: string; status: string; reason?: string }
|
|
122
|
+
| { action: "retry-node" | "skip-node"; runId: string; callId: number };
|
|
123
|
+
|
|
124
|
+
/** Result returned by the `workflow` tool's execute. */
|
|
125
|
+
export interface ToolResult {
|
|
126
|
+
content: Array<{ type: "text"; text: string }>;
|
|
127
|
+
details: WorkflowToolDetails | undefined;
|
|
128
|
+
isError?: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── GUI 协议 helpers ───────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/** 为 details 附加 __gui__(RPC 模式下)。 */
|
|
134
|
+
function withGui<T extends WorkflowToolDetails | undefined>(
|
|
135
|
+
details: T,
|
|
136
|
+
ctx?: GuiContext,
|
|
137
|
+
): Record<string, unknown> {
|
|
138
|
+
const out: Record<string, unknown> = details ? { ...details } : {};
|
|
139
|
+
if (ctx && isGuiCapable(ctx) && details) {
|
|
140
|
+
out.__gui__ = guiResult(buildWorkflowGui(details));
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 按 WorkflowToolDetails 构造对应的 GuiComponent。 */
|
|
146
|
+
function buildWorkflowGui(details: WorkflowToolDetails) {
|
|
147
|
+
if (details.action === "run") {
|
|
148
|
+
return guiComponent("workflow-runs", {
|
|
149
|
+
runs: [{ runId: details.runId, name: details.name, status: details.status }],
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (details.action === "status") {
|
|
153
|
+
return guiComponent("workflow-runs", {
|
|
154
|
+
runs: details.runs.map((r) => ({
|
|
155
|
+
runId: r.runId,
|
|
156
|
+
name: r.name,
|
|
157
|
+
status: r.status,
|
|
158
|
+
reason: r.reason,
|
|
159
|
+
error: r.error,
|
|
160
|
+
})),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// pause/resume/abort/retry-node/skip-node
|
|
164
|
+
return guiComponent("stats-line", {
|
|
165
|
+
items: [{
|
|
166
|
+
label: details.action,
|
|
167
|
+
value: details.runId.slice(0, 8),
|
|
168
|
+
severity: "ok" as const,
|
|
169
|
+
}],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Tool registration ────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 注册 workflow tool(7 actions)。
|
|
177
|
+
*
|
|
178
|
+
* @param pi ExtensionAPI
|
|
179
|
+
* @param deps LauncherDeps(LifecycleDeps + registry)
|
|
180
|
+
* @param reentryRef reentry guard。仅 workflow tool 使用——workflow-script tool
|
|
181
|
+
* 有独立的 isScriptRunning flag(见 registerWorkflowScriptTool),不共用此 guard。
|
|
182
|
+
* 注意:reentryRef 是 factory 级单例(index.ts 在 factory 内创建),跨 session 共享。
|
|
183
|
+
* 当前 Pi 运行时单 session 串行(同一时刻只有一个 active session),跨 session
|
|
184
|
+
* 不会并发触发 workflow action,故共享无竞态。若未来支持多 session 并发,需改为
|
|
185
|
+
* per-session guard。
|
|
186
|
+
*/
|
|
187
|
+
export function registerWorkflowTool(
|
|
188
|
+
pi: ExtensionAPI,
|
|
189
|
+
deps: LauncherDeps,
|
|
190
|
+
reentryRef: ReentryGuardRef,
|
|
191
|
+
): void {
|
|
192
|
+
pi.registerTool({
|
|
193
|
+
name: "workflow",
|
|
194
|
+
label: "Workflow",
|
|
195
|
+
description:
|
|
196
|
+
"Execute and control workflows: run (start), status, pause, resume, abort, " +
|
|
197
|
+
"retry-node (re-run a failed agent call to refresh its trace; does NOT resume the " +
|
|
198
|
+
"workflow script or change its output — see promptGuidelines), skip-node (mark a call as skipped).\n" +
|
|
199
|
+
"Replaces workflow + workflow-run tools.",
|
|
200
|
+
promptSnippet: "Run, pause, resume, abort, or check workflow status",
|
|
201
|
+
promptGuidelines: [
|
|
202
|
+
"PRIORITY: When user says 'workflow', 'run workflow', try run action FIRST.",
|
|
203
|
+
"run: discover by name/description, then start in background (no user confirmation needed).",
|
|
204
|
+
"Do NOT poll status after starting — results appear automatically via notifyDone.",
|
|
205
|
+
"retry-node/skip-node: for specific failed agent calls (requires runId + callId). " +
|
|
206
|
+
"retry-node only re-runs the call and refreshes the trace — the workflow script has " +
|
|
207
|
+
"already moved past the failed call, so the new result does NOT feed back into the " +
|
|
208
|
+
"script flow. Use retry-node for diagnostics, not to resume the workflow.",
|
|
209
|
+
],
|
|
210
|
+
parameters: WorkflowParams,
|
|
211
|
+
|
|
212
|
+
async execute(
|
|
213
|
+
_toolCallId: string,
|
|
214
|
+
params: WorkflowToolParams,
|
|
215
|
+
signal: AbortSignal | undefined,
|
|
216
|
+
_onUpdate: unknown,
|
|
217
|
+
_ctx: ExtensionContext,
|
|
218
|
+
): Promise<ToolResult> {
|
|
219
|
+
// P1-2: Honor abort signal up-front
|
|
220
|
+
if (signal?.aborted) {
|
|
221
|
+
return textResult("Operation aborted before start", true);
|
|
222
|
+
}
|
|
223
|
+
// P1-6: Reentry guard
|
|
224
|
+
if (!acquireReentryGuard(reentryRef)) {
|
|
225
|
+
return textResult(REENTRY_BUSY_MESSAGE, true);
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
let result: ToolResult;
|
|
229
|
+
// 断言为 WorkflowAction 联合——typebox Static 推断为 any,显式标注让 default
|
|
230
|
+
// 分支的 never 穷尽检查生效(新增 action 时 tsc 报错强制补 case)。
|
|
231
|
+
const action = params.action as WorkflowAction;
|
|
232
|
+
switch (action) {
|
|
233
|
+
case "run":
|
|
234
|
+
result = await actionRun(params, deps, signal);
|
|
235
|
+
break;
|
|
236
|
+
case "status":
|
|
237
|
+
result = actionStatus(deps);
|
|
238
|
+
break;
|
|
239
|
+
case "pause":
|
|
240
|
+
result = await actionLifecycle("pause", params, deps);
|
|
241
|
+
break;
|
|
242
|
+
case "resume":
|
|
243
|
+
result = await actionLifecycle("resume", params, deps);
|
|
244
|
+
break;
|
|
245
|
+
case "abort":
|
|
246
|
+
result = await actionLifecycle("abort", params, deps);
|
|
247
|
+
break;
|
|
248
|
+
case "retry-node":
|
|
249
|
+
result = await actionRetryNode(params, deps);
|
|
250
|
+
break;
|
|
251
|
+
case "skip-node":
|
|
252
|
+
result = await actionSkipNode(params, deps);
|
|
253
|
+
break;
|
|
254
|
+
default: {
|
|
255
|
+
// Exhaustiveness check — 新增 WorkflowAction 成员时未补 case,tsc 在此报错。
|
|
256
|
+
const _exhaustive: never = action;
|
|
257
|
+
return textResult(`Unknown action: ${String(_exhaustive)}`, true);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// GUI 协议:RPC 模式下附加 __gui__ 到 details
|
|
261
|
+
return {
|
|
262
|
+
...result,
|
|
263
|
+
details: withGui(result.details, _ctx as GuiContext) as unknown as WorkflowToolDetails,
|
|
264
|
+
};
|
|
265
|
+
} finally {
|
|
266
|
+
releaseReentryGuard(reentryRef);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
renderCall(args: Record<string, unknown>, theme: Theme, _context?: unknown) {
|
|
271
|
+
const action = String(args.action ?? "");
|
|
272
|
+
const name = args.name ? ` ${String(args.name)}` : "";
|
|
273
|
+
const runId = args.runId ? ` ${String(args.runId).slice(0, RUNID_SHORT)}` : "";
|
|
274
|
+
return new Text(
|
|
275
|
+
theme.fg("toolTitle", theme.bold("workflow ")) +
|
|
276
|
+
theme.fg("muted", action) +
|
|
277
|
+
theme.fg("accent", name) +
|
|
278
|
+
theme.fg("dim", runId),
|
|
279
|
+
0,
|
|
280
|
+
0,
|
|
281
|
+
);
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
renderResult(result: { content?: Array<{ type: string; text?: string }> }, _options: unknown, _theme: Theme, _context?: unknown) {
|
|
285
|
+
return new Text(renderTextFallback(result), 0, 0);
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── run action ───────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
async function actionRun(
|
|
293
|
+
params: WorkflowToolParams,
|
|
294
|
+
deps: LauncherDeps,
|
|
295
|
+
signal: AbortSignal | undefined,
|
|
296
|
+
): Promise<ToolResult> {
|
|
297
|
+
const name = params.name;
|
|
298
|
+
if (!name) {
|
|
299
|
+
return textResult("run requires 'name' parameter", true);
|
|
300
|
+
}
|
|
301
|
+
const args = params.args ?? {};
|
|
302
|
+
const tokens = params.tokens;
|
|
303
|
+
const time = params.time;
|
|
304
|
+
|
|
305
|
+
const script = await deps.registry.get(name);
|
|
306
|
+
if (!script) {
|
|
307
|
+
// 模糊匹配建议
|
|
308
|
+
const all = await deps.registry.loadAll();
|
|
309
|
+
const available = all.filter((wf) => wf.available);
|
|
310
|
+
const suggestions = available
|
|
311
|
+
.map((wf) => ` - ${wf.name}: ${wf.meta.description || "(no description)"}`)
|
|
312
|
+
.join("\n");
|
|
313
|
+
return {
|
|
314
|
+
content: [
|
|
315
|
+
{
|
|
316
|
+
type: "text",
|
|
317
|
+
text: `Workflow '${name}' not found. Available:\n${suggestions || " (none)"}`,
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
details: { action: "run", runId: "", status: "not_found", name },
|
|
321
|
+
isError: true,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 构建 RunSpec + 启动
|
|
326
|
+
const runId = await runWorkflow(
|
|
327
|
+
{
|
|
328
|
+
scriptSource: script.toExecutable(),
|
|
329
|
+
args,
|
|
330
|
+
budgetTokens: tokens,
|
|
331
|
+
budgetTimeMs: time,
|
|
332
|
+
scriptName: script.name,
|
|
333
|
+
scriptPath: script.path,
|
|
334
|
+
description: script.meta.description,
|
|
335
|
+
},
|
|
336
|
+
deps,
|
|
337
|
+
signal,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
content: [
|
|
342
|
+
{
|
|
343
|
+
type: "text",
|
|
344
|
+
text: `Started workflow '${script.name}' (${runId}). Running in background — do NOT poll status.`,
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
details: { action: "run", runId, status: "running", name: script.name },
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── status action ────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
function actionStatus(deps: LauncherDeps): ToolResult {
|
|
354
|
+
const runs = Array.from(deps.runs.values());
|
|
355
|
+
if (runs.length === 0) {
|
|
356
|
+
return {
|
|
357
|
+
content: [{ type: "text", text: "No workflows in current session." }],
|
|
358
|
+
details: { action: "status", runs: [] },
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const summaries = runs.map(toRunSummary);
|
|
362
|
+
const lines = summaries.map((s) => {
|
|
363
|
+
const duration = s.startedAt ? ` (${formatElapsed(s.startedAt)})` : "";
|
|
364
|
+
const reasonSuffix = s.reason && s.reason !== "completed" ? ` [${s.reason}]` : "";
|
|
365
|
+
return `[${s.status}${reasonSuffix}] ${s.name} (${s.runId.slice(0, RUNID_SHORT)})${duration}${s.error ? ` error: ${s.error}` : ""}`;
|
|
366
|
+
});
|
|
367
|
+
return {
|
|
368
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
369
|
+
details: { action: "status", runs: summaries },
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── pause/resume/abort lifecycle actions ─────────────────────
|
|
374
|
+
|
|
375
|
+
async function actionLifecycle(
|
|
376
|
+
action: "pause" | "resume" | "abort",
|
|
377
|
+
params: WorkflowToolParams,
|
|
378
|
+
deps: LauncherDeps,
|
|
379
|
+
): Promise<ToolResult> {
|
|
380
|
+
const runId = params.runId;
|
|
381
|
+
if (!runId) {
|
|
382
|
+
return textResult(`'runId' is required for ${action}`, true);
|
|
383
|
+
}
|
|
384
|
+
const run = deps.runs.get(runId);
|
|
385
|
+
if (!run) {
|
|
386
|
+
return textResult(`Workflow '${runId}' not found`, true);
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const oldStatus = run.state.status;
|
|
390
|
+
if (action === "pause") {
|
|
391
|
+
await pauseRun(runId, deps);
|
|
392
|
+
} else if (action === "resume") {
|
|
393
|
+
await resumeRun(runId, deps);
|
|
394
|
+
} else {
|
|
395
|
+
await abortRun(runId, deps, params.error);
|
|
396
|
+
}
|
|
397
|
+
const newStatus = run.state.status;
|
|
398
|
+
const reasonSuffix = run.state.reason ? ` (${run.state.reason})` : "";
|
|
399
|
+
return {
|
|
400
|
+
content: [
|
|
401
|
+
{
|
|
402
|
+
type: "text",
|
|
403
|
+
text: `Workflow '${run.spec.scriptName}' (${runId}): ${oldStatus} → ${newStatus}${reasonSuffix}`,
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
details: { action, runId, status: newStatus, reason: run.state.reason },
|
|
407
|
+
};
|
|
408
|
+
} catch (err) {
|
|
409
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
410
|
+
return textResult(`Error: ${msg}`, true);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ── retry-node / skip-node ───────────────────────────────────
|
|
415
|
+
|
|
416
|
+
async function actionRetryNode(params: WorkflowToolParams, deps: LauncherDeps): Promise<ToolResult> {
|
|
417
|
+
const runId = params.runId;
|
|
418
|
+
const callId = params.callId;
|
|
419
|
+
if (!runId || callId === undefined) {
|
|
420
|
+
return textResult("retry-node requires 'runId' and 'callId'", true);
|
|
421
|
+
}
|
|
422
|
+
const run = deps.runs.get(runId);
|
|
423
|
+
if (!run) {
|
|
424
|
+
return textResult(`Workflow '${runId}' not found`, true);
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
await retryNode(run, callId, deps);
|
|
428
|
+
return {
|
|
429
|
+
content: [
|
|
430
|
+
{ type: "text", text: `Retried call ${callId} in run ${runId.slice(0, RUNID_SHORT)}.` },
|
|
431
|
+
],
|
|
432
|
+
details: { action: "retry-node", runId, callId },
|
|
433
|
+
};
|
|
434
|
+
} catch (err) {
|
|
435
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
436
|
+
return textResult(`Error: ${msg}`, true);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function actionSkipNode(params: WorkflowToolParams, deps: LauncherDeps): Promise<ToolResult> {
|
|
441
|
+
const runId = params.runId;
|
|
442
|
+
const callId = params.callId;
|
|
443
|
+
if (!runId || callId === undefined) {
|
|
444
|
+
return textResult("skip-node requires 'runId' and 'callId'", true);
|
|
445
|
+
}
|
|
446
|
+
const run = deps.runs.get(runId);
|
|
447
|
+
if (!run) {
|
|
448
|
+
return textResult(`Workflow '${runId}' not found`, true);
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
await skipNode(run, callId, deps);
|
|
452
|
+
return {
|
|
453
|
+
content: [
|
|
454
|
+
{ type: "text", text: `Skipped call ${callId} in run ${runId.slice(0, RUNID_SHORT)}.` },
|
|
455
|
+
],
|
|
456
|
+
details: { action: "skip-node", runId, callId },
|
|
457
|
+
};
|
|
458
|
+
} catch (err) {
|
|
459
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
460
|
+
return textResult(`Error: ${msg}`, true);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ── helpers ──────────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
/** WorkflowRun → 摘要(status action 用)。 */
|
|
467
|
+
function toRunSummary(run: WorkflowRun): RunSummary {
|
|
468
|
+
return {
|
|
469
|
+
runId: run.runId,
|
|
470
|
+
name: run.spec.scriptName,
|
|
471
|
+
status: run.state.status,
|
|
472
|
+
reason: run.state.reason,
|
|
473
|
+
startedAt: run.meta.startedAt,
|
|
474
|
+
completedAt: run.meta.completedAt,
|
|
475
|
+
error: run.state.error,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function textResult(text: string, isError = false): ToolResult {
|
|
480
|
+
return {
|
|
481
|
+
content: [{ type: "text", text }],
|
|
482
|
+
details: undefined,
|
|
483
|
+
isError: isError || undefined,
|
|
484
|
+
};
|
|
485
|
+
}
|