autocrew 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/HAMLETDEER.md +562 -0
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/README_CN.md +190 -0
- package/adapters/openclaw/index.ts +68 -0
- package/bin/autocrew.mjs +23 -0
- package/bin/autocrew.ts +13 -0
- package/openclaw.plugin.json +36 -0
- package/package.json +74 -0
- package/skills/_writing-style/SKILL.md +68 -0
- package/skills/audience-profiler/SKILL.md +241 -0
- package/skills/content-attribution/SKILL.md +128 -0
- package/skills/content-review/SKILL.md +257 -0
- package/skills/cover-generator/SKILL.md +93 -0
- package/skills/humanizer-zh/SKILL.md +75 -0
- package/skills/intel-digest/SKILL.md +57 -0
- package/skills/intel-pull/SKILL.md +74 -0
- package/skills/manage-pipeline/SKILL.md +63 -0
- package/skills/memory-distill/SKILL.md +89 -0
- package/skills/onboarding/SKILL.md +117 -0
- package/skills/pipeline-status/SKILL.md +51 -0
- package/skills/platform-rewrite/SKILL.md +125 -0
- package/skills/pre-publish/SKILL.md +142 -0
- package/skills/publish-content/SKILL.md +500 -0
- package/skills/remix-content/SKILL.md +77 -0
- package/skills/research/SKILL.md +127 -0
- package/skills/setup/SKILL.md +353 -0
- package/skills/spawn-batch-writer/SKILL.md +66 -0
- package/skills/spawn-planner/SKILL.md +72 -0
- package/skills/spawn-writer/SKILL.md +60 -0
- package/skills/teardown/SKILL.md +144 -0
- package/skills/title-craft/SKILL.md +234 -0
- package/skills/topic-ideas/SKILL.md +105 -0
- package/skills/video-timeline/SKILL.md +117 -0
- package/skills/write-script/SKILL.md +232 -0
- package/skills/xhs-cover-review/SKILL.md +48 -0
- package/src/adapters/browser/browser-cdp.ts +260 -0
- package/src/adapters/browser/browser-relay.ts +236 -0
- package/src/adapters/browser/gateway-client.ts +148 -0
- package/src/adapters/browser/types.ts +36 -0
- package/src/adapters/image/gemini.ts +219 -0
- package/src/adapters/research/tikhub.ts +19 -0
- package/src/cli/banner.ts +18 -0
- package/src/cli/bootstrap.ts +33 -0
- package/src/cli/commands/adapt.ts +28 -0
- package/src/cli/commands/advance.ts +28 -0
- package/src/cli/commands/assets.ts +24 -0
- package/src/cli/commands/audit.ts +18 -0
- package/src/cli/commands/contents.ts +18 -0
- package/src/cli/commands/cover.ts +58 -0
- package/src/cli/commands/events.ts +17 -0
- package/src/cli/commands/humanize.ts +27 -0
- package/src/cli/commands/index.ts +80 -0
- package/src/cli/commands/init.ts +28 -0
- package/src/cli/commands/intel.ts +55 -0
- package/src/cli/commands/learn.ts +34 -0
- package/src/cli/commands/memory.ts +18 -0
- package/src/cli/commands/migrate.ts +24 -0
- package/src/cli/commands/open.ts +21 -0
- package/src/cli/commands/pipelines.ts +18 -0
- package/src/cli/commands/pre-publish.ts +27 -0
- package/src/cli/commands/profile.ts +31 -0
- package/src/cli/commands/research.ts +36 -0
- package/src/cli/commands/restore.ts +28 -0
- package/src/cli/commands/review.ts +61 -0
- package/src/cli/commands/start.ts +28 -0
- package/src/cli/commands/status.ts +14 -0
- package/src/cli/commands/templates.ts +15 -0
- package/src/cli/commands/topics.ts +18 -0
- package/src/cli/commands/trash.ts +28 -0
- package/src/cli/commands/upgrade.ts +48 -0
- package/src/cli/commands/versions.ts +24 -0
- package/src/cli/index.ts +40 -0
- package/src/data/sensitive-words-builtin.json +114 -0
- package/src/data/source-presets.yaml +54 -0
- package/src/e2e.test.ts +596 -0
- package/src/modules/auth/cookie-manager.ts +113 -0
- package/src/modules/cards/template-engine.ts +74 -0
- package/src/modules/cards/templates/comparison-table.ts +71 -0
- package/src/modules/cards/templates/data-chart.ts +76 -0
- package/src/modules/cards/templates/flow-chart.ts +49 -0
- package/src/modules/cards/templates/key-points.ts +59 -0
- package/src/modules/cover/prompt-builder.test.ts +157 -0
- package/src/modules/cover/prompt-builder.ts +212 -0
- package/src/modules/cover/ratio-adapter.test.ts +122 -0
- package/src/modules/cover/ratio-adapter.ts +104 -0
- package/src/modules/filter/sensitive-words.test.ts +72 -0
- package/src/modules/filter/sensitive-words.ts +212 -0
- package/src/modules/humanizer/zh.test.ts +75 -0
- package/src/modules/humanizer/zh.ts +175 -0
- package/src/modules/intel/collector.ts +19 -0
- package/src/modules/intel/collectors/competitor.test.ts +71 -0
- package/src/modules/intel/collectors/competitor.ts +65 -0
- package/src/modules/intel/collectors/rss.test.ts +56 -0
- package/src/modules/intel/collectors/rss.ts +70 -0
- package/src/modules/intel/collectors/trends.test.ts +80 -0
- package/src/modules/intel/collectors/trends.ts +107 -0
- package/src/modules/intel/collectors/web-search.test.ts +85 -0
- package/src/modules/intel/collectors/web-search.ts +81 -0
- package/src/modules/intel/integration.test.ts +203 -0
- package/src/modules/intel/intel-engine.test.ts +103 -0
- package/src/modules/intel/intel-engine.ts +96 -0
- package/src/modules/intel/source-config.test.ts +113 -0
- package/src/modules/intel/source-config.ts +131 -0
- package/src/modules/learnings/diff-tracker.test.ts +144 -0
- package/src/modules/learnings/diff-tracker.ts +189 -0
- package/src/modules/learnings/rule-distiller.ts +141 -0
- package/src/modules/memory/distill.ts +208 -0
- package/src/modules/migrate/legacy-migrate.test.ts +169 -0
- package/src/modules/migrate/legacy-migrate.ts +229 -0
- package/src/modules/pro/api-client.ts +192 -0
- package/src/modules/pro/gate.test.ts +110 -0
- package/src/modules/pro/gate.ts +104 -0
- package/src/modules/profile/creator-profile.test.ts +178 -0
- package/src/modules/profile/creator-profile.ts +248 -0
- package/src/modules/publish/douyin-api.ts +34 -0
- package/src/modules/publish/wechat-mp.ts +320 -0
- package/src/modules/publish/xiaohongshu-api.ts +127 -0
- package/src/modules/research/free-engine.ts +360 -0
- package/src/modules/timeline/markup-generator.ts +63 -0
- package/src/modules/timeline/parser.ts +275 -0
- package/src/modules/workflow/templates.ts +124 -0
- package/src/modules/writing/platform-rewrite.ts +190 -0
- package/src/modules/writing/title-hashtag.ts +385 -0
- package/src/runtime/context.test.ts +97 -0
- package/src/runtime/context.ts +129 -0
- package/src/runtime/events.test.ts +83 -0
- package/src/runtime/events.ts +104 -0
- package/src/runtime/hooks.ts +174 -0
- package/src/runtime/tool-runner.test.ts +204 -0
- package/src/runtime/tool-runner.ts +282 -0
- package/src/runtime/workflow-engine.test.ts +455 -0
- package/src/runtime/workflow-engine.ts +391 -0
- package/src/server/index.ts +409 -0
- package/src/server/start.ts +39 -0
- package/src/storage/local-store.test.ts +304 -0
- package/src/storage/local-store.ts +704 -0
- package/src/storage/pipeline-store.test.ts +363 -0
- package/src/storage/pipeline-store.ts +698 -0
- package/src/tools/asset.ts +96 -0
- package/src/tools/content-save.ts +276 -0
- package/src/tools/cover-review.ts +221 -0
- package/src/tools/humanize.ts +54 -0
- package/src/tools/init.ts +133 -0
- package/src/tools/intel.ts +92 -0
- package/src/tools/memory.ts +76 -0
- package/src/tools/pipeline-ops.ts +109 -0
- package/src/tools/pipeline.ts +168 -0
- package/src/tools/pre-publish.ts +232 -0
- package/src/tools/publish.ts +183 -0
- package/src/tools/registry.ts +198 -0
- package/src/tools/research.ts +304 -0
- package/src/tools/review.ts +305 -0
- package/src/tools/rewrite.ts +165 -0
- package/src/tools/status.ts +30 -0
- package/src/tools/timeline.ts +234 -0
- package/src/tools/topic-create.ts +50 -0
- package/src/types/providers.ts +69 -0
- package/src/types/timeline.test.ts +147 -0
- package/src/types/timeline.ts +83 -0
- package/src/utils/retry.test.ts +97 -0
- package/src/utils/retry.ts +85 -0
- package/templates/AGENTS.md +99 -0
- package/templates/SOUL.md +31 -0
- package/templates/TOOLS.md +76 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkflowEngine — stateful workflow orchestration for AutoCrew.
|
|
3
|
+
*
|
|
4
|
+
* Runs multi-step tool pipelines with:
|
|
5
|
+
* - Sequential step execution via ToolRunner
|
|
6
|
+
* - Approval gates (pauses workflow for user confirmation)
|
|
7
|
+
* - Retry logic per step
|
|
8
|
+
* - Parameter interpolation (${stepId.field} references)
|
|
9
|
+
* - Persistent state to disk after every step
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from "node:fs/promises";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import type { ToolRunner, ToolResult } from "./tool-runner.js";
|
|
15
|
+
|
|
16
|
+
// --- Interfaces ---
|
|
17
|
+
|
|
18
|
+
export interface WorkflowStep {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
/** Tool to call */
|
|
22
|
+
tool: string;
|
|
23
|
+
/** Parameters (can reference previous step outputs via ${stepId.field}) */
|
|
24
|
+
params: Record<string, unknown>;
|
|
25
|
+
/** If true, workflow pauses here for user approval */
|
|
26
|
+
requiresApproval?: boolean;
|
|
27
|
+
/** Condition: only run if this evaluates true */
|
|
28
|
+
condition?: string;
|
|
29
|
+
/** Retry config */
|
|
30
|
+
retry?: { maxAttempts: number; delayMs: number };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WorkflowDefinition {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
description: string;
|
|
37
|
+
steps: WorkflowStep[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type WorkflowStatus = "pending" | "running" | "paused" | "completed" | "failed" | "cancelled";
|
|
41
|
+
|
|
42
|
+
export interface WorkflowInstance {
|
|
43
|
+
id: string;
|
|
44
|
+
definitionId: string;
|
|
45
|
+
status: WorkflowStatus;
|
|
46
|
+
currentStepIndex: number;
|
|
47
|
+
stepResults: Record<string, unknown>;
|
|
48
|
+
createdAt: string;
|
|
49
|
+
updatedAt: string;
|
|
50
|
+
error?: string;
|
|
51
|
+
/** Initial params passed at creation (merged into each step) */
|
|
52
|
+
params?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Helpers ---
|
|
56
|
+
|
|
57
|
+
function generateId(): string {
|
|
58
|
+
return `wf-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve parameter references like ${stepId.field} against stepResults.
|
|
63
|
+
*/
|
|
64
|
+
function resolveParams(
|
|
65
|
+
params: Record<string, unknown>,
|
|
66
|
+
stepResults: Record<string, unknown>,
|
|
67
|
+
instanceParams?: Record<string, unknown>,
|
|
68
|
+
): Record<string, unknown> {
|
|
69
|
+
const resolved: Record<string, unknown> = {};
|
|
70
|
+
for (const [key, value] of Object.entries(params)) {
|
|
71
|
+
if (typeof value === "string") {
|
|
72
|
+
resolved[key] = value.replace(/\$\{([^}]+)\}/g, (_match, ref: string) => {
|
|
73
|
+
const [stepId, ...fieldParts] = ref.split(".");
|
|
74
|
+
const field = fieldParts.join(".");
|
|
75
|
+
// Try stepResults first, then instanceParams
|
|
76
|
+
const stepResult = stepResults[stepId] as Record<string, unknown> | undefined;
|
|
77
|
+
if (stepResult && field in stepResult) {
|
|
78
|
+
return String(stepResult[field]);
|
|
79
|
+
}
|
|
80
|
+
if (instanceParams && stepId in instanceParams && !field) {
|
|
81
|
+
return String(instanceParams[stepId]);
|
|
82
|
+
}
|
|
83
|
+
return `\${${ref}}`; // Leave unresolved
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
resolved[key] = value;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return resolved;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Evaluate a simple condition string against stepResults.
|
|
94
|
+
* Supports: "stepId.field === value", "stepId.ok", "stepId.field !== value"
|
|
95
|
+
*/
|
|
96
|
+
function evaluateCondition(condition: string, stepResults: Record<string, unknown>): boolean {
|
|
97
|
+
try {
|
|
98
|
+
// Simple evaluator for "stepId.field" truthiness
|
|
99
|
+
const trimmed = condition.trim();
|
|
100
|
+
|
|
101
|
+
// Handle "stepId.field === value"
|
|
102
|
+
const eqMatch = trimmed.match(/^(\S+)\s*===?\s*(.+)$/);
|
|
103
|
+
if (eqMatch) {
|
|
104
|
+
const val = resolveFieldPath(eqMatch[1], stepResults);
|
|
105
|
+
const expected = eqMatch[2].replace(/^["']|["']$/g, "");
|
|
106
|
+
return String(val) === expected;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Handle "stepId.field !== value"
|
|
110
|
+
const neqMatch = trimmed.match(/^(\S+)\s*!==?\s*(.+)$/);
|
|
111
|
+
if (neqMatch) {
|
|
112
|
+
const val = resolveFieldPath(neqMatch[1], stepResults);
|
|
113
|
+
const expected = neqMatch[2].replace(/^["']|["']$/g, "");
|
|
114
|
+
return String(val) !== expected;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Simple truthiness: "stepId.field"
|
|
118
|
+
const val = resolveFieldPath(trimmed, stepResults);
|
|
119
|
+
return !!val;
|
|
120
|
+
} catch {
|
|
121
|
+
return true; // If condition can't be evaluated, proceed
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolveFieldPath(fieldPath: string, stepResults: Record<string, unknown>): unknown {
|
|
126
|
+
const [stepId, ...parts] = fieldPath.split(".");
|
|
127
|
+
let current: unknown = stepResults[stepId];
|
|
128
|
+
for (const part of parts) {
|
|
129
|
+
if (current && typeof current === "object" && current !== null) {
|
|
130
|
+
current = (current as Record<string, unknown>)[part];
|
|
131
|
+
} else {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return current;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function delay(ms: number): Promise<void> {
|
|
139
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- WorkflowEngine ---
|
|
143
|
+
|
|
144
|
+
export class WorkflowEngine {
|
|
145
|
+
private toolRunner: ToolRunner;
|
|
146
|
+
private dataDir: string;
|
|
147
|
+
private definitions = new Map<string, WorkflowDefinition>();
|
|
148
|
+
|
|
149
|
+
constructor(toolRunner: ToolRunner, dataDir: string) {
|
|
150
|
+
this.toolRunner = toolRunner;
|
|
151
|
+
this.dataDir = dataDir;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Register a workflow definition (template) */
|
|
155
|
+
registerDefinition(def: WorkflowDefinition): void {
|
|
156
|
+
this.definitions.set(def.id, def);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Get a registered definition */
|
|
160
|
+
getDefinition(id: string): WorkflowDefinition | undefined {
|
|
161
|
+
return this.definitions.get(id);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** List all registered definitions */
|
|
165
|
+
listDefinitions(): WorkflowDefinition[] {
|
|
166
|
+
return Array.from(this.definitions.values());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private async workflowsDir(): Promise<string> {
|
|
170
|
+
const dir = path.join(this.dataDir, "workflows");
|
|
171
|
+
await fs.mkdir(dir, { recursive: true });
|
|
172
|
+
return dir;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async persistInstance(instance: WorkflowInstance): Promise<void> {
|
|
176
|
+
const dir = await this.workflowsDir();
|
|
177
|
+
await fs.writeFile(
|
|
178
|
+
path.join(dir, `${instance.id}.json`),
|
|
179
|
+
JSON.stringify(instance, null, 2),
|
|
180
|
+
"utf-8",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private async loadInstance(id: string): Promise<WorkflowInstance | null> {
|
|
185
|
+
const dir = await this.workflowsDir();
|
|
186
|
+
try {
|
|
187
|
+
const raw = await fs.readFile(path.join(dir, `${id}.json`), "utf-8");
|
|
188
|
+
return JSON.parse(raw);
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Create a workflow instance from a registered definition */
|
|
195
|
+
async create(definitionId: string, params?: Record<string, unknown>): Promise<WorkflowInstance> {
|
|
196
|
+
const def = this.definitions.get(definitionId);
|
|
197
|
+
if (!def) {
|
|
198
|
+
throw new Error(`Workflow definition not found: ${definitionId}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const now = new Date().toISOString();
|
|
202
|
+
const instance: WorkflowInstance = {
|
|
203
|
+
id: generateId(),
|
|
204
|
+
definitionId,
|
|
205
|
+
status: "pending",
|
|
206
|
+
currentStepIndex: 0,
|
|
207
|
+
stepResults: {},
|
|
208
|
+
createdAt: now,
|
|
209
|
+
updatedAt: now,
|
|
210
|
+
params,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
await this.persistInstance(instance);
|
|
214
|
+
return instance;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Start (or resume) a workflow instance */
|
|
218
|
+
async start(instanceId: string): Promise<WorkflowInstance> {
|
|
219
|
+
const instance = await this.loadInstance(instanceId);
|
|
220
|
+
if (!instance) {
|
|
221
|
+
throw new Error(`Workflow instance not found: ${instanceId}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (instance.status === "completed" || instance.status === "cancelled") {
|
|
225
|
+
return instance;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const def = this.definitions.get(instance.definitionId);
|
|
229
|
+
if (!def) {
|
|
230
|
+
instance.status = "failed";
|
|
231
|
+
instance.error = `Definition not found: ${instance.definitionId}`;
|
|
232
|
+
instance.updatedAt = new Date().toISOString();
|
|
233
|
+
await this.persistInstance(instance);
|
|
234
|
+
return instance;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
instance.status = "running";
|
|
238
|
+
instance.updatedAt = new Date().toISOString();
|
|
239
|
+
await this.persistInstance(instance);
|
|
240
|
+
|
|
241
|
+
return this.executeSteps(instance, def);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Approve a paused workflow step and continue execution */
|
|
245
|
+
async approve(instanceId: string): Promise<WorkflowInstance> {
|
|
246
|
+
const instance = await this.loadInstance(instanceId);
|
|
247
|
+
if (!instance) {
|
|
248
|
+
throw new Error(`Workflow instance not found: ${instanceId}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (instance.status !== "paused") {
|
|
252
|
+
throw new Error(`Workflow is not paused (status: ${instance.status})`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const def = this.definitions.get(instance.definitionId);
|
|
256
|
+
if (!def) {
|
|
257
|
+
instance.status = "failed";
|
|
258
|
+
instance.error = `Definition not found: ${instance.definitionId}`;
|
|
259
|
+
instance.updatedAt = new Date().toISOString();
|
|
260
|
+
await this.persistInstance(instance);
|
|
261
|
+
return instance;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Move past the approval step
|
|
265
|
+
instance.currentStepIndex++;
|
|
266
|
+
instance.status = "running";
|
|
267
|
+
instance.updatedAt = new Date().toISOString();
|
|
268
|
+
await this.persistInstance(instance);
|
|
269
|
+
|
|
270
|
+
return this.executeSteps(instance, def);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Cancel a workflow */
|
|
274
|
+
async cancel(instanceId: string): Promise<WorkflowInstance> {
|
|
275
|
+
const instance = await this.loadInstance(instanceId);
|
|
276
|
+
if (!instance) {
|
|
277
|
+
throw new Error(`Workflow instance not found: ${instanceId}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
instance.status = "cancelled";
|
|
281
|
+
instance.updatedAt = new Date().toISOString();
|
|
282
|
+
await this.persistInstance(instance);
|
|
283
|
+
return instance;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Get current status of a workflow */
|
|
287
|
+
async getStatus(instanceId: string): Promise<WorkflowInstance | null> {
|
|
288
|
+
return this.loadInstance(instanceId);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** List all workflow instances */
|
|
292
|
+
async list(): Promise<WorkflowInstance[]> {
|
|
293
|
+
const dir = await this.workflowsDir();
|
|
294
|
+
const files = await fs.readdir(dir);
|
|
295
|
+
const instances: WorkflowInstance[] = [];
|
|
296
|
+
for (const f of files) {
|
|
297
|
+
if (!f.endsWith(".json")) continue;
|
|
298
|
+
try {
|
|
299
|
+
const raw = await fs.readFile(path.join(dir, f), "utf-8");
|
|
300
|
+
instances.push(JSON.parse(raw));
|
|
301
|
+
} catch {
|
|
302
|
+
// skip corrupt files
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return instances.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// --- Internal execution ---
|
|
309
|
+
|
|
310
|
+
private async executeSteps(
|
|
311
|
+
instance: WorkflowInstance,
|
|
312
|
+
def: WorkflowDefinition,
|
|
313
|
+
): Promise<WorkflowInstance> {
|
|
314
|
+
while (instance.currentStepIndex < def.steps.length) {
|
|
315
|
+
if (instance.status === "cancelled") break;
|
|
316
|
+
|
|
317
|
+
const step = def.steps[instance.currentStepIndex];
|
|
318
|
+
|
|
319
|
+
// Check condition
|
|
320
|
+
if (step.condition && !evaluateCondition(step.condition, instance.stepResults)) {
|
|
321
|
+
// Skip this step
|
|
322
|
+
instance.currentStepIndex++;
|
|
323
|
+
instance.updatedAt = new Date().toISOString();
|
|
324
|
+
await this.persistInstance(instance);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Check approval gate BEFORE executing the step
|
|
329
|
+
if (step.requiresApproval) {
|
|
330
|
+
instance.status = "paused";
|
|
331
|
+
instance.updatedAt = new Date().toISOString();
|
|
332
|
+
await this.persistInstance(instance);
|
|
333
|
+
return instance;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Execute step with retries
|
|
337
|
+
const result = await this.executeStepWithRetry(step, instance);
|
|
338
|
+
|
|
339
|
+
// Store result
|
|
340
|
+
instance.stepResults[step.id] = result;
|
|
341
|
+
instance.updatedAt = new Date().toISOString();
|
|
342
|
+
await this.persistInstance(instance); // Persist result before advancing
|
|
343
|
+
|
|
344
|
+
if (result.ok === false) {
|
|
345
|
+
instance.status = "failed";
|
|
346
|
+
instance.error = `Step "${step.name}" failed: ${(result as ToolResult).error || "unknown error"}`;
|
|
347
|
+
await this.persistInstance(instance);
|
|
348
|
+
return instance;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Advance
|
|
352
|
+
instance.currentStepIndex++;
|
|
353
|
+
await this.persistInstance(instance);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (instance.status !== "cancelled") {
|
|
357
|
+
instance.status = "completed";
|
|
358
|
+
instance.updatedAt = new Date().toISOString();
|
|
359
|
+
await this.persistInstance(instance);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return instance;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private async executeStepWithRetry(
|
|
366
|
+
step: WorkflowStep,
|
|
367
|
+
instance: WorkflowInstance,
|
|
368
|
+
): Promise<ToolResult> {
|
|
369
|
+
const maxAttempts = step.retry?.maxAttempts ?? 1;
|
|
370
|
+
const delayMs = step.retry?.delayMs ?? 0;
|
|
371
|
+
|
|
372
|
+
const resolvedParams = resolveParams(step.params, instance.stepResults, instance.params);
|
|
373
|
+
|
|
374
|
+
let lastResult: ToolResult = { ok: false, error: "No attempts made" };
|
|
375
|
+
|
|
376
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
377
|
+
lastResult = await this.toolRunner.execute(step.tool, resolvedParams);
|
|
378
|
+
|
|
379
|
+
if (lastResult.ok !== false) {
|
|
380
|
+
return lastResult;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Don't delay after the last failed attempt
|
|
384
|
+
if (attempt < maxAttempts && delayMs > 0) {
|
|
385
|
+
await delay(delayMs);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return lastResult;
|
|
390
|
+
}
|
|
391
|
+
}
|