minimal-agent 0.2.0 → 0.3.1

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.
Files changed (108) hide show
  1. package/README.md +54 -72
  2. package/package.json +18 -13
  3. package/plugins/ralph-wiggum/plugin.js +205 -0
  4. package/plugins/ralph-wiggum/src/goalState.js +260 -0
  5. package/plugins/ralph-wiggum/src/{sentinels.ts → sentinels.js} +4 -7
  6. package/plugins/ralph-wiggum/src/stopHookRunner.js +104 -0
  7. package/plugins/ralph-wiggum/src/verificationGate.js +202 -0
  8. package/plugins/workflow-runner/commands/workflow.md +13 -3
  9. package/plugins/workflow-runner/{plugin.ts → plugin.js} +20 -26
  10. package/plugins/workflow-runner/src/expressions.js +369 -0
  11. package/plugins/workflow-runner/src/index.js +216 -0
  12. package/plugins/workflow-runner/src/loader.js +183 -0
  13. package/plugins/workflow-runner/src/runner.js +290 -0
  14. package/plugins/workflow-runner/src/stepExecutors/assert.js +28 -0
  15. package/plugins/workflow-runner/src/stepExecutors/llm.js +44 -0
  16. package/plugins/workflow-runner/src/stepExecutors/skill.js +103 -0
  17. package/plugins/workflow-runner/src/stepExecutors/{tool.ts → tool.js} +19 -25
  18. package/plugins/workflow-runner/src/types.js +59 -0
  19. package/plugins/workflow-runner/src/{workflowState.ts → workflowState.js} +21 -40
  20. package/src/bootstrap/cwdArg.js +22 -0
  21. package/src/bootstrap/workingDir.js +31 -0
  22. package/src/cli/configWizard.js +272 -0
  23. package/src/cli/print.js +197 -0
  24. package/src/config/configFile.js +78 -0
  25. package/src/config.js +118 -0
  26. package/src/context/compact.js +357 -0
  27. package/src/context/microCompactLite.js +151 -0
  28. package/src/context/persistContext.js +109 -0
  29. package/src/context/reactiveCompact.js +121 -0
  30. package/src/context/sessionPath.js +58 -0
  31. package/src/context/snipCompact.js +112 -0
  32. package/src/context/tokenCounter.js +66 -0
  33. package/src/llm/client.js +182 -0
  34. package/src/loop.js +230 -0
  35. package/src/main.js +116 -0
  36. package/src/plugin-sdk.js +24 -0
  37. package/src/plugins/commandRouter.js +169 -0
  38. package/src/plugins/hookEngine.js +258 -0
  39. package/src/plugins/pluginApi.js +23 -0
  40. package/src/plugins/pluginLoader.js +71 -0
  41. package/src/plugins/pluginRunner.js +65 -0
  42. package/src/plugins/transcript.js +171 -0
  43. package/src/prompts/projectInstructions.js +48 -0
  44. package/src/prompts/skillList.js +126 -0
  45. package/src/prompts/system.js +155 -0
  46. package/src/session/runTurn.js +41 -0
  47. package/src/session/sessionState.js +19 -0
  48. package/src/tools/bash/bash.js +352 -0
  49. package/src/tools/bash/semantics.js +85 -0
  50. package/src/tools/bash/warnings.js +98 -0
  51. package/src/tools/edit/edit.js +253 -0
  52. package/src/tools/edit/multi-edit.js +155 -0
  53. package/src/tools/glob/glob.js +97 -0
  54. package/src/tools/grep/grep.js +185 -0
  55. package/src/tools/grep/rgPath.js +173 -0
  56. package/src/tools/index.js +94 -0
  57. package/src/tools/read/read.js +209 -0
  58. package/src/tools/shared/fileState.js +61 -0
  59. package/src/tools/shared/fileUtils.js +281 -0
  60. package/src/tools/shared/schemas.js +16 -0
  61. package/src/tools/types.js +21 -0
  62. package/src/tools/webbrowser/browser.js +55 -0
  63. package/src/tools/webbrowser/webbrowser.js +194 -0
  64. package/src/tools/webfetch/preapproved.js +267 -0
  65. package/src/tools/webfetch/webfetch.js +317 -0
  66. package/src/tools/websearch/websearch.js +161 -0
  67. package/src/tools/write/write.js +125 -0
  68. package/src/types/turndown.d.ts +23 -0
  69. package/src/types.js +16 -0
  70. package/src/ui/App.js +37 -0
  71. package/src/ui/InputBox.js +240 -0
  72. package/src/ui/MessageList.js +28 -0
  73. package/src/ui/Root.js +70 -0
  74. package/src/ui/StatusLine.js +41 -0
  75. package/src/ui/ToolStatus.js +11 -0
  76. package/src/ui/hooks/useChat.js +234 -0
  77. package/src/ui/hooks/usePasteHandler.js +137 -0
  78. package/src/ui/hooks/useTextBuffer.js +55 -0
  79. package/src/ui/hooks/useTokenUsage.js +30 -0
  80. package/src/ui/textBuffer.js +217 -0
  81. package/src/utils/packageRoot.js +37 -0
  82. package/src/utils/resourcePaths.js +49 -0
  83. package/src/utils/zodToJson.js +29 -0
  84. package/dist/main.js +0 -5315
  85. package/plugins/ralph-wiggum/plugin.ts +0 -275
  86. package/plugins/ralph-wiggum/scripts/setup-ralph-loop.sh +0 -203
  87. package/plugins/ralph-wiggum/src/goalState.ts +0 -310
  88. package/plugins/ralph-wiggum/src/stopHookRunner.ts +0 -136
  89. package/plugins/ralph-wiggum/src/verificationGate.ts +0 -252
  90. package/plugins/ralph-wiggum/test/goalState.test.ts +0 -410
  91. package/plugins/ralph-wiggum/test/verificationGate.test.ts +0 -122
  92. package/plugins/workflow-runner/src/expressions.ts +0 -371
  93. package/plugins/workflow-runner/src/index.ts +0 -194
  94. package/plugins/workflow-runner/src/loader.ts +0 -193
  95. package/plugins/workflow-runner/src/runner.ts +0 -313
  96. package/plugins/workflow-runner/src/stepExecutors/assert.ts +0 -30
  97. package/plugins/workflow-runner/src/stepExecutors/llm.ts +0 -54
  98. package/plugins/workflow-runner/src/stepExecutors/skill.ts +0 -115
  99. package/plugins/workflow-runner/src/types.ts +0 -183
  100. package/plugins/workflow-runner/test/cli.e2e.test.ts +0 -114
  101. package/plugins/workflow-runner/test/e2e.test.ts +0 -268
  102. package/plugins/workflow-runner/test/expressions.test.ts +0 -140
  103. package/plugins/workflow-runner/test/fixtures/cli-e2e.yaml +0 -27
  104. package/plugins/workflow-runner/test/fixtures/hello-workflow.yaml +0 -49
  105. package/plugins/workflow-runner/test/graceful.test.ts +0 -139
  106. package/plugins/workflow-runner/test/loader.test.ts +0 -216
  107. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +0 -230
  108. package/plugins/workflow-runner/test/runner.test.ts +0 -511
@@ -1,193 +0,0 @@
1
- /**
2
- * ============================================================
3
- * src/workflows/loader.ts —— workflow yaml 双源加载 + 最小校验
4
- * ------------------------------------------------------------
5
- * - 扫描 cwd/workflows/ 与 packageRoot/workflows/(cwd 优先,复用
6
- * getResourceSearchPaths 契约)
7
- * - 一个坏 yaml 不应拖累其它工作流(catch 单文件错误,继续扫描)
8
- * - 目录不存在 / 为空 → 静默返回 [](默认 T-A-O-R 不受影响)
9
- *
10
- * P0 不引 AJV;下面手写 ≤80 行的最小校验器,只挡死硬性错误:
11
- * - name / description / steps 必填且类型正确
12
- * - steps 非空
13
- * - 每个 step 必须有且只有一个动作字段(tool / skill / llm / type)
14
- *
15
- * P1 切 AJV 做完整 schema。
16
- * ============================================================
17
- */
18
-
19
- import { readFile, readdir } from 'node:fs/promises';
20
- import { join } from 'node:path';
21
-
22
- import { parse as parseYamlLib } from 'yaml';
23
-
24
- import { getResourceSearchPaths } from '../../../src/plugin-sdk.ts';
25
- import type { StepDef, WorkflowDef } from './types.ts';
26
-
27
- export class WorkflowLoadError extends Error {
28
- constructor(message: string, public readonly file?: string) {
29
- super(message);
30
- this.name = 'WorkflowLoadError';
31
- }
32
- }
33
-
34
- /**
35
- * 扫描双源目录,返回所有 *.yaml / *.yml 解析后的 workflow。
36
- * 永远不抛;坏文件 console.warn 并跳过。
37
- */
38
- export async function listWorkflows(): Promise<WorkflowDef[]> {
39
- const out: WorkflowDef[] = [];
40
- const seen = new Set<string>();
41
- const roots = getResourceSearchPaths('workflows', import.meta.url);
42
-
43
- for (const root of roots) {
44
- let entries: string[];
45
- try {
46
- entries = await readdir(root);
47
- } catch {
48
- continue;
49
- }
50
- for (const f of entries) {
51
- if (!f.endsWith('.yaml') && !f.endsWith('.yml')) continue;
52
- const full = join(root, f);
53
- try {
54
- const def = await parseWorkflowFile(full);
55
- if (!seen.has(def.name)) {
56
- seen.add(def.name);
57
- out.push(def);
58
- }
59
- } catch (e) {
60
- // eslint-disable-next-line no-console
61
- console.warn(`[workflows] 忽略损坏的 yaml ${full}: ${(e as Error).message}`);
62
- }
63
- }
64
- }
65
- return out;
66
- }
67
-
68
- /** 按 name 查找。找不到返回 null —— 调用方决定如何提示用户。 */
69
- export async function findWorkflowByName(name: string): Promise<WorkflowDef | null> {
70
- const all = await listWorkflows();
71
- return all.find((w) => w.name === name) ?? null;
72
- }
73
-
74
- /** 读单个 yaml 文件 → 解析 → 校验。失败抛 WorkflowLoadError。 */
75
- export async function parseWorkflowFile(file: string): Promise<WorkflowDef> {
76
- let raw: string;
77
- try {
78
- raw = await readFile(file, 'utf8');
79
- } catch (e) {
80
- throw new WorkflowLoadError(`无法读取 ${file}: ${(e as Error).message}`, file);
81
- }
82
- let data: unknown;
83
- try {
84
- data = parseYamlLib(raw);
85
- } catch (e) {
86
- throw new WorkflowLoadError(`yaml 语法错误: ${(e as Error).message}`, file);
87
- }
88
- if (!data || typeof data !== 'object') {
89
- throw new WorkflowLoadError('yaml 根节点必须是对象', file);
90
- }
91
- validate(data as Record<string, unknown>, file);
92
- const def = data as WorkflowDef;
93
- def.__source = file;
94
- return def;
95
- }
96
-
97
- // ---------------- 手写最小校验 ----------------
98
-
99
- function validate(obj: Record<string, unknown>, file: string): void {
100
- if (typeof obj.name !== 'string' || obj.name.length === 0) {
101
- throw new WorkflowLoadError("缺少必填字段 'name'(string)", file);
102
- }
103
- if (typeof obj.description !== 'string' || obj.description.length === 0) {
104
- throw new WorkflowLoadError("缺少必填字段 'description'(string)", file);
105
- }
106
- if (!Array.isArray(obj.steps) || obj.steps.length === 0) {
107
- throw new WorkflowLoadError("缺少必填字段 'steps' 或为空数组", file);
108
- }
109
- validateSteps(obj.steps as unknown[], file, '');
110
- if (obj.inputs !== undefined) {
111
- if (!Array.isArray(obj.inputs)) {
112
- throw new WorkflowLoadError("'inputs' 必须是数组", file);
113
- }
114
- for (const [i, item] of (obj.inputs as unknown[]).entries()) {
115
- if (!item || typeof item !== 'object') {
116
- throw new WorkflowLoadError(`inputs[${i}] 必须是对象`, file);
117
- }
118
- const r = item as Record<string, unknown>;
119
- if (typeof r.name !== 'string' || r.name.length === 0) {
120
- throw new WorkflowLoadError(`inputs[${i}].name 必填`, file);
121
- }
122
- if (r.type !== undefined && r.type !== 'string' && r.type !== 'number' && r.type !== 'enum') {
123
- throw new WorkflowLoadError(`inputs[${i}].type 必须是 string/number/enum`, file);
124
- }
125
- if (r.type === 'enum' && (!Array.isArray(r.values) || r.values.length === 0)) {
126
- throw new WorkflowLoadError(`inputs[${i}].values 必填(type=enum 时)`, file);
127
- }
128
- }
129
- }
130
- }
131
-
132
- function validateSteps(steps: unknown[], file: string, pathPrefix: string): void {
133
- const seenIds = new Set<string>();
134
- for (const [i, item] of steps.entries()) {
135
- const p = pathPrefix ? `${pathPrefix}.steps[${i}]` : `steps[${i}]`;
136
- if (!item || typeof item !== 'object') {
137
- throw new WorkflowLoadError(`${p} 必须是对象`, file);
138
- }
139
- const s = item as Record<string, unknown>;
140
- if (typeof s.id !== 'string' || s.id.length === 0) {
141
- throw new WorkflowLoadError(`${p}.id 必填`, file);
142
- }
143
- // id 唯一仅在同级 steps 数组内强制
144
- if (seenIds.has(s.id)) {
145
- throw new WorkflowLoadError(`${p}.id "${s.id}" 在当前 steps 列表中重复`, file);
146
- }
147
- seenIds.add(s.id);
148
-
149
- // 动作字段恰好一个
150
- const actionFields: (keyof StepDef)[] = ['tool', 'skill', 'llm', 'type'];
151
- const present = actionFields.filter((k) => s[k] !== undefined);
152
- if (present.length === 0) {
153
- throw new WorkflowLoadError(
154
- `${p} 必须有以下字段之一: tool / skill / llm / type`,
155
- file,
156
- );
157
- }
158
- if (present.length > 1) {
159
- throw new WorkflowLoadError(
160
- `${p} 同时设置了 ${present.join(', ')};只允许其中一个`,
161
- file,
162
- );
163
- }
164
-
165
- // 控制流字段一致性
166
- if (s.type === 'branch' && s.condition === undefined) {
167
- throw new WorkflowLoadError(`${p} type=branch 必须带 'condition'`, file);
168
- }
169
- if (s.type === 'loop') {
170
- if (s.over === undefined) throw new WorkflowLoadError(`${p} type=loop 必须带 'over'`, file);
171
- if (typeof s.as !== 'string' || s.as.length === 0) {
172
- throw new WorkflowLoadError(`${p} type=loop 必须带 'as'`, file);
173
- }
174
- if (!Array.isArray(s.do)) {
175
- throw new WorkflowLoadError(`${p} type=loop 必须带 'do' 数组`, file);
176
- }
177
- validateSteps(s.do as unknown[], file, `${p}.do`);
178
- }
179
- if (s.type === 'assert' && s.condition === undefined) {
180
- throw new WorkflowLoadError(`${p} type=assert 必须带 'condition'`, file);
181
- }
182
- if (s.type === 'branch') {
183
- if (s.then !== undefined && !Array.isArray(s.then)) {
184
- throw new WorkflowLoadError(`${p}.then 必须是数组`, file);
185
- }
186
- if (s.else !== undefined && !Array.isArray(s.else)) {
187
- throw new WorkflowLoadError(`${p}.else 必须是数组`, file);
188
- }
189
- if (Array.isArray(s.then)) validateSteps(s.then as unknown[], file, `${p}.then`);
190
- if (Array.isArray(s.else)) validateSteps(s.else as unknown[], file, `${p}.else`);
191
- }
192
- }
193
- }
@@ -1,313 +0,0 @@
1
- /**
2
- * ============================================================
3
- * plugins/workflow-runner/src/runner.ts —— workflow 顺序执行器
4
- * ------------------------------------------------------------
5
- * AsyncGenerator,按 yaml.steps[] 顺序执行:
6
- * - when: 不满足 → yield workflow_step_skipped
7
- * - 命中动作字段 → 调对应 step executor
8
- * - capture: 把 result.raw 上的字段绑到 VarStack
9
- * - onError: halt(默认)→ yield error 后 return;continue → 记录后跳过
10
- * - 信号 abort → yield interrupted 后 return
11
- *
12
- * 支持的 step kind(loader 已校验):
13
- * - tool / llm / assert —— 原子动作(execXxxStep 返回 StepResult)
14
- * - skill —— 迷你 ReAct 子循环(透传子事件 + 累计 text)
15
- * - branch / loop / pause —— 控制流,直接在 runSteps 内处理(不走 executor)
16
- *
17
- * 控制流嵌套通过 runSteps 递归实现:branch.then/else / loop.do 子数组
18
- * 是 StepDef[],调用方语义与顶层 def.steps 一致;变量栈用 VarStack.push/pop
19
- * 在 loop 迭代间隔离 `${as}` / `${as}_idx`。
20
- * ============================================================
21
- */
22
-
23
- import { getWorkingDir, type LoopEvent } from '../../../src/plugin-sdk.ts';
24
- import { evalExpr, interpolate } from './expressions.ts';
25
- import { execAssertStep } from './stepExecutors/assert.ts';
26
- import { execLlmStep } from './stepExecutors/llm.ts';
27
- import { execSkillStep } from './stepExecutors/skill.ts';
28
- import { execToolStep } from './stepExecutors/tool.ts';
29
- import type {
30
- RunContext,
31
- StepDef,
32
- StepKind,
33
- StepResult,
34
- WorkflowDef,
35
- WorkflowEvent,
36
- } from './types.ts';
37
- import { VarStack } from './types.ts';
38
- import { WorkflowState } from './workflowState.ts';
39
-
40
- function stepKind(step: StepDef): StepKind {
41
- if (step.type) return step.type;
42
- if (step.tool) return 'tool';
43
- if (step.skill) return 'skill';
44
- if (step.llm) return 'llm';
45
- return 'assert'; // 不会到这里(loader 已校验),兜底
46
- }
47
-
48
- function readPath(root: Record<string, unknown>, path: string): unknown {
49
- const parts = path.split('.');
50
- let cur: unknown = root;
51
- for (const p of parts) {
52
- if (cur == null || typeof cur !== 'object') return undefined;
53
- cur = (cur as Record<string, unknown>)[p];
54
- }
55
- return cur;
56
- }
57
-
58
- function bindCapture(
59
- captureMap: Record<string, string>,
60
- result: StepResult,
61
- vars: VarStack,
62
- ): void {
63
- for (const [resultField, varName] of Object.entries(captureMap)) {
64
- const value = resultField.includes('.')
65
- ? readPath(result.raw, resultField)
66
- : result.raw[resultField];
67
- vars.set(varName, value);
68
- }
69
- }
70
-
71
- /**
72
- * 分发"动作"型 step(tool / llm / skill / assert)到对应 executor。
73
- * 控制流 step(branch / loop / pause)由 runSteps 直接处理,不走这里。
74
- */
75
- async function* dispatchActionStep(
76
- step: StepDef,
77
- vars: VarStack,
78
- ctx: RunContext,
79
- ): AsyncGenerator<LoopEvent, StepResult, void> {
80
- if (step.type === 'assert') return execAssertStep(step, vars);
81
- if (step.tool) return await execToolStep(step, vars, ctx);
82
- if (step.llm) return await execLlmStep(step, vars, ctx);
83
- if (step.skill) return yield* execSkillStep(step, vars, ctx);
84
- throw new Error(`step ${step.id}: 无可执行字段(tool/llm/skill/assert)`);
85
- }
86
-
87
- /**
88
- * 递归执行 steps[]。返回 true 表示该 block 已 halt(上游应中止)。
89
- *
90
- * yields 混合事件:WorkflowEvent + LoopEvent(skill 子循环转发 / error / interrupted)。
91
- * index/total 字段相对**当前 block**,嵌套 block 自有计数(branch.then 内 3 步就报 total=3)。
92
- */
93
- async function* runSteps(
94
- steps: StepDef[],
95
- vars: VarStack,
96
- ctx: RunContext,
97
- state: WorkflowState,
98
- ): AsyncGenerator<WorkflowEvent | LoopEvent, boolean, void> {
99
- for (let i = 0; i < steps.length; i++) {
100
- const step = steps[i];
101
- if (ctx.signal?.aborted) {
102
- yield { type: 'interrupted' };
103
- return true;
104
- }
105
-
106
- // when 条件
107
- if (step.when) {
108
- let pass = false;
109
- try {
110
- pass = Boolean(evalExpr(interpolate(step.when, vars), vars));
111
- } catch (e) {
112
- const errMsg = `when 表达式错误: ${(e as Error).message}`;
113
- yield {
114
- type: 'workflow_step_end',
115
- id: step.id,
116
- ok: false,
117
- error: errMsg,
118
- };
119
- yield {
120
- type: 'error',
121
- error: `Workflow halted at step "${step.id}": ${errMsg}`,
122
- };
123
- return true;
124
- }
125
- if (!pass) {
126
- yield {
127
- type: 'workflow_step_skipped',
128
- id: step.id,
129
- reason: 'when=false',
130
- };
131
- await state.appendProgress(`- ${step.id} skipped (when=false)`);
132
- continue;
133
- }
134
- }
135
-
136
- const kind = stepKind(step);
137
- yield {
138
- type: 'workflow_step_start',
139
- id: step.id,
140
- kind,
141
- index: i,
142
- total: steps.length,
143
- };
144
-
145
- // ---------- 控制流:branch ----------
146
- if (step.type === 'branch') {
147
- let cond = false;
148
- try {
149
- cond = Boolean(evalExpr(interpolate(step.condition ?? 'false', vars), vars));
150
- } catch (e) {
151
- const errMsg = `branch 条件错误: ${(e as Error).message}`;
152
- await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
153
- yield { type: 'workflow_step_end', id: step.id, ok: false, error: errMsg };
154
- yield { type: 'error', error: `Workflow halted at step "${step.id}": ${errMsg}` };
155
- return true;
156
- }
157
- const branch = cond ? step.then : step.else;
158
- if (Array.isArray(branch) && branch.length > 0) {
159
- const childHalted = yield* runSteps(branch, vars, ctx, state);
160
- if (childHalted) return true;
161
- }
162
- await state.appendProgress(`✓ ${step.id} (branch=${cond})`);
163
- await state.writeVars(vars.snapshot());
164
- yield {
165
- type: 'workflow_step_end',
166
- id: step.id,
167
- ok: true,
168
- output: `branch=${cond}`,
169
- };
170
- continue;
171
- }
172
-
173
- // ---------- 控制流:loop ----------
174
- if (step.type === 'loop') {
175
- let arr: unknown;
176
- try {
177
- arr = evalExpr(interpolate(step.over ?? '[]', vars), vars);
178
- } catch (e) {
179
- const errMsg = `loop over 表达式错误: ${(e as Error).message}`;
180
- await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
181
- yield { type: 'workflow_step_end', id: step.id, ok: false, error: errMsg };
182
- yield { type: 'error', error: `Workflow halted at step "${step.id}": ${errMsg}` };
183
- return true;
184
- }
185
- if (!Array.isArray(arr)) {
186
- const errMsg = `loop over 必须求值为数组,得到 ${typeof arr}`;
187
- await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
188
- yield { type: 'workflow_step_end', id: step.id, ok: false, error: errMsg };
189
- yield { type: 'error', error: `Workflow halted at step "${step.id}": ${errMsg}` };
190
- return true;
191
- }
192
- const asName = step.as ?? 'item';
193
- let iterCount = 0;
194
- for (let idx = 0; idx < arr.length; idx++) {
195
- if (ctx.signal?.aborted) {
196
- yield { type: 'interrupted' };
197
- return true;
198
- }
199
- vars.push();
200
- vars.set(asName, arr[idx]);
201
- vars.set(`${asName}_idx`, idx);
202
- const childHalted = yield* runSteps(step.do ?? [], vars, ctx, state);
203
- vars.pop();
204
- if (childHalted) return true;
205
- iterCount++;
206
- }
207
- await state.appendProgress(`✓ ${step.id} (loop ×${iterCount})`);
208
- await state.writeVars(vars.snapshot());
209
- yield {
210
- type: 'workflow_step_end',
211
- id: step.id,
212
- ok: true,
213
- output: `loop ${iterCount} 次`,
214
- };
215
- continue;
216
- }
217
-
218
- // ---------- 控制流:pause ----------
219
- //
220
- // 非交互模式(CLI -p / 测试)下无法真的暂停等待人工,直接当成 skipped。
221
- // 交互模式(TUI)需要 UI 层接住 step_skipped 把 reason 渲染出来 —— P0 已
222
- // 走通这条 UI 路径。step.prompt 用于把"暂停理由"传给上层呈现。
223
- if (step.type === 'pause') {
224
- const msg = step.prompt
225
- ? interpolate(step.prompt, vars)
226
- : 'pause(非交互模式直接跳过)';
227
- await state.appendProgress(`- ${step.id} pause: ${msg}`);
228
- yield {
229
- type: 'workflow_step_skipped',
230
- id: step.id,
231
- reason: `pause: ${msg}`,
232
- };
233
- yield {
234
- type: 'workflow_step_end',
235
- id: step.id,
236
- ok: true,
237
- output: 'paused (skipped in non-interactive mode)',
238
- };
239
- continue;
240
- }
241
-
242
- // ---------- 普通动作 step:tool / llm / skill / assert ----------
243
- try {
244
- const result = yield* dispatchActionStep(step, vars, ctx);
245
- if (step.capture) bindCapture(step.capture, result, vars);
246
- await state.appendProgress(`✓ ${step.id} (${kind})`);
247
- await state.writeVars(vars.snapshot());
248
- yield {
249
- type: 'workflow_step_end',
250
- id: step.id,
251
- ok: true,
252
- output: result.preview,
253
- };
254
- } catch (e) {
255
- const errMsg = (e as Error).message ?? String(e);
256
- await state.appendProgress(`✗ ${step.id}: ${errMsg}`);
257
- if (step.onError === 'continue') {
258
- yield {
259
- type: 'workflow_step_end',
260
- id: step.id,
261
- ok: false,
262
- error: errMsg,
263
- };
264
- continue;
265
- }
266
- yield {
267
- type: 'workflow_step_end',
268
- id: step.id,
269
- ok: false,
270
- error: errMsg,
271
- };
272
- yield {
273
- type: 'error',
274
- error: `Workflow halted at step "${step.id}": ${errMsg}`,
275
- };
276
- return true;
277
- }
278
- }
279
- return false;
280
- }
281
-
282
- /**
283
- * 运行一个 workflow。
284
- *
285
- * yields 混合事件:本地 WorkflowEvent(workflow_* 系列,UI 静默忽略不渲染)
286
- * + 通用 LoopEvent(error / interrupted 等,UI 识别并渲染)。
287
- * 经 plugin.ts -> pluginRunner 透传给 UI;workflow_* 由 PluginEvent 开放契约承载。
288
- */
289
- export async function* runWorkflow(
290
- def: WorkflowDef,
291
- inputs: Record<string, unknown>,
292
- ctx: RunContext,
293
- ): AsyncGenerator<WorkflowEvent | LoopEvent, void, void> {
294
- const vars = new VarStack();
295
- vars.setGlobal('inputs', inputs);
296
-
297
- const state = new WorkflowState(getWorkingDir());
298
- await state.init(def.name, inputs);
299
-
300
- yield {
301
- type: 'workflow_start',
302
- name: def.name,
303
- totalSteps: def.steps.length,
304
- };
305
-
306
- try {
307
- const halted = yield* runSteps(def.steps, vars, ctx, state);
308
- if (halted) return;
309
- yield { type: 'workflow_done', name: def.name, vars: vars.snapshot() };
310
- } finally {
311
- await state.cleanup();
312
- }
313
- }
@@ -1,30 +0,0 @@
1
- /**
2
- * ============================================================
3
- * src/workflows/stepExecutors/assert.ts —— assert step 执行
4
- * ------------------------------------------------------------
5
- * evalExpr(condition) → 真值通过;假值抛错(带 onFail 自定义信息)。
6
- * ============================================================
7
- */
8
-
9
- import { evalExpr, interpolate } from '../expressions.ts';
10
- import type { StepDef, StepResult, VarStack } from '../types.ts';
11
-
12
- export function execAssertStep(step: StepDef, vars: VarStack): StepResult {
13
- if (typeof step.condition !== 'string') {
14
- throw new Error(`step ${step.id}: assert 缺少 condition`);
15
- }
16
- let ok: boolean;
17
- try {
18
- const expanded = interpolate(step.condition, vars);
19
- ok = Boolean(evalExpr(expanded, vars));
20
- } catch (e) {
21
- throw new Error(`assert 表达式求值失败: ${(e as Error).message}`);
22
- }
23
- if (!ok) {
24
- const msg = step.onFail
25
- ? interpolate(step.onFail, vars)
26
- : `assertion failed: ${step.condition}`;
27
- throw new Error(msg);
28
- }
29
- return { raw: { ok: true }, preview: 'assert passed' };
30
- }
@@ -1,54 +0,0 @@
1
- /**
2
- * ============================================================
3
- * src/workflows/stepExecutors/llm.ts —— llm step 执行
4
- * ------------------------------------------------------------
5
- * 单轮 LLM 生成。关键设计:
6
- * - tools: [] —— 不开任何工具(工具调用应该写成 tool: step)
7
- * - 自带独立 system + user 两条消息,不污染主 history
8
- * - 流式累积文本(reasoning 字段在这里忽略;只关心最终文本)
9
- *
10
- * capture:
11
- * - `{ text: var_name }` 绑生成文本
12
- * - `{ result: var_name }` 同义
13
- * ============================================================
14
- */
15
-
16
- import { chat, type Message } from '../../../../src/plugin-sdk.ts';
17
- import { interpolate } from '../expressions.ts';
18
- import type { RunContext, StepDef, StepResult, VarStack } from '../types.ts';
19
-
20
- const LLM_STEP_SYSTEM =
21
- '你正在被一个 workflow 调用。只输出本步骤要求的内容本身,不要寒暄、不要列要求复述、不要工具调用语法。';
22
-
23
- export async function execLlmStep(
24
- step: StepDef,
25
- vars: VarStack,
26
- ctx: RunContext,
27
- ): Promise<StepResult> {
28
- if (typeof step.llm !== 'string') throw new Error(`step ${step.id}: 缺少 llm 字段`);
29
- const prompt = interpolate(step.llm, vars);
30
-
31
- const messages: Message[] = [
32
- { role: 'system', content: LLM_STEP_SYSTEM },
33
- { role: 'user', content: prompt },
34
- ];
35
-
36
- let text = '';
37
- for await (const ev of chat({
38
- provider: ctx.provider,
39
- messages,
40
- tools: [],
41
- signal: ctx.signal,
42
- })) {
43
- if (ev.type === 'text_delta') text += ev.delta;
44
- if (ev.type === 'done' && ev.stopReason === 'aborted') {
45
- throw new Error('用户中断');
46
- }
47
- }
48
-
49
- const trimmed = text.trim();
50
- return {
51
- raw: { text: trimmed, result: trimmed },
52
- preview: trimmed.length > 200 ? `${trimmed.slice(0, 200)}...` : trimmed,
53
- };
54
- }