keystone-cli 1.0.2 → 1.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/README.md +288 -24
- package/package.json +8 -4
- package/src/cli.ts +538 -419
- package/src/commands/doc.ts +31 -0
- package/src/commands/event.ts +29 -0
- package/src/commands/graph.ts +37 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/init.ts +185 -0
- package/src/commands/run.ts +124 -0
- package/src/commands/schema.ts +40 -0
- package/src/commands/utils.ts +78 -0
- package/src/commands/validate.ts +111 -0
- package/src/db/memory-db.ts +50 -2
- package/src/db/workflow-db.test.ts +314 -0
- package/src/db/workflow-db.ts +810 -210
- package/src/expression/evaluator-audit.test.ts +4 -2
- package/src/expression/evaluator.test.ts +14 -1
- package/src/expression/evaluator.ts +166 -19
- package/src/parser/config-schema.ts +18 -0
- package/src/parser/schema.ts +153 -22
- package/src/parser/test-schema.ts +6 -6
- package/src/parser/workflow-parser.test.ts +24 -0
- package/src/parser/workflow-parser.ts +65 -3
- package/src/runner/auto-heal.test.ts +5 -6
- package/src/runner/blueprint-executor.test.ts +2 -2
- package/src/runner/debug-repl.test.ts +5 -8
- package/src/runner/debug-repl.ts +59 -16
- package/src/runner/durable-timers.test.ts +11 -2
- package/src/runner/engine-executor.test.ts +1 -1
- package/src/runner/events.ts +57 -0
- package/src/runner/executors/artifact-executor.ts +166 -0
- package/src/runner/{blueprint-executor.ts → executors/blueprint-executor.ts} +15 -7
- package/src/runner/{engine-executor.ts → executors/engine-executor.ts} +55 -7
- package/src/runner/executors/file-executor.test.ts +48 -0
- package/src/runner/executors/file-executor.ts +324 -0
- package/src/runner/{foreach-executor.ts → executors/foreach-executor.ts} +168 -80
- package/src/runner/executors/human-executor.ts +144 -0
- package/src/runner/executors/join-executor.ts +75 -0
- package/src/runner/executors/llm-executor.ts +1266 -0
- package/src/runner/executors/memory-executor.ts +71 -0
- package/src/runner/executors/plan-executor.ts +104 -0
- package/src/runner/executors/request-executor.ts +265 -0
- package/src/runner/executors/script-executor.ts +43 -0
- package/src/runner/executors/shell-executor.ts +403 -0
- package/src/runner/executors/subworkflow-executor.ts +114 -0
- package/src/runner/executors/types.ts +69 -0
- package/src/runner/executors/wait-executor.ts +59 -0
- package/src/runner/join-scheduling.test.ts +197 -0
- package/src/runner/llm-adapter-runtime.test.ts +209 -0
- package/src/runner/llm-adapter.test.ts +419 -24
- package/src/runner/llm-adapter.ts +414 -17
- package/src/runner/llm-clarification.test.ts +2 -1
- package/src/runner/llm-executor.test.ts +532 -17
- package/src/runner/mcp-client-audit.test.ts +1 -2
- package/src/runner/mcp-client.ts +136 -46
- package/src/runner/mcp-manager.test.ts +4 -0
- package/src/runner/mcp-server.test.ts +58 -0
- package/src/runner/mcp-server.ts +26 -0
- package/src/runner/memoization.test.ts +190 -0
- package/src/runner/optimization-runner.ts +4 -9
- package/src/runner/quality-gate.test.ts +69 -0
- package/src/runner/reflexion.test.ts +6 -17
- package/src/runner/resource-pool.ts +102 -14
- package/src/runner/services/context-builder.ts +144 -0
- package/src/runner/services/secret-manager.ts +105 -0
- package/src/runner/services/workflow-validator.ts +131 -0
- package/src/runner/shell-executor.test.ts +28 -4
- package/src/runner/standard-tools-ast.test.ts +196 -0
- package/src/runner/standard-tools-execution.test.ts +27 -0
- package/src/runner/standard-tools-integration.test.ts +6 -10
- package/src/runner/standard-tools.ts +339 -102
- package/src/runner/step-executor.test.ts +216 -4
- package/src/runner/step-executor.ts +69 -941
- package/src/runner/stream-utils.ts +7 -3
- package/src/runner/test-harness.ts +20 -1
- package/src/runner/timeout.test.ts +10 -0
- package/src/runner/timeout.ts +11 -2
- package/src/runner/tool-integration.test.ts +1 -1
- package/src/runner/wait-step.test.ts +102 -0
- package/src/runner/workflow-runner.test.ts +208 -15
- package/src/runner/workflow-runner.ts +890 -818
- package/src/runner/workflow-scheduler.ts +75 -0
- package/src/runner/workflow-state.ts +269 -0
- package/src/runner/workflow-subflows.test.ts +13 -12
- package/src/scripts/generate-schemas.ts +16 -0
- package/src/templates/agents/explore.md +1 -0
- package/src/templates/agents/general.md +1 -0
- package/src/templates/agents/handoff-router.md +14 -0
- package/src/templates/agents/handoff-specialist.md +15 -0
- package/src/templates/agents/keystone-architect.md +13 -44
- package/src/templates/agents/my-agent.md +1 -0
- package/src/templates/agents/software-engineer.md +1 -0
- package/src/templates/agents/summarizer.md +1 -0
- package/src/templates/agents/test-agent.md +1 -0
- package/src/templates/agents/tester.md +1 -0
- package/src/templates/{basic-inputs.yaml → basics/basic-inputs.yaml} +2 -0
- package/src/templates/{basic-shell.yaml → basics/basic-shell.yaml} +2 -1
- package/src/templates/{full-feature-demo.yaml → basics/full-feature-demo.yaml} +2 -0
- package/src/templates/{stop-watch.yaml → basics/stop-watch.yaml} +1 -0
- package/src/templates/{child-rollback.yaml → control-flow/child-rollback.yaml} +1 -0
- package/src/templates/{cleanup-finally.yaml → control-flow/cleanup-finally.yaml} +1 -0
- package/src/templates/{fan-out-fan-in.yaml → control-flow/fan-out-fan-in.yaml} +3 -0
- package/src/templates/control-flow/idempotency-example.yaml +30 -0
- package/src/templates/{loop-parallel.yaml → control-flow/loop-parallel.yaml} +3 -0
- package/src/templates/{parent-rollback.yaml → control-flow/parent-rollback.yaml} +1 -0
- package/src/templates/{retry-policy.yaml → control-flow/retry-policy.yaml} +3 -0
- package/src/templates/features/artifact-example.yaml +39 -0
- package/src/templates/{engine-example.yaml → features/engine-example.yaml} +1 -0
- package/src/templates/{human-interaction.yaml → features/human-interaction.yaml} +1 -0
- package/src/templates/{llm-agent.yaml → features/llm-agent.yaml} +1 -0
- package/src/templates/{memory-service.yaml → features/memory-service.yaml} +2 -0
- package/src/templates/{robust-automation.yaml → features/robust-automation.yaml} +3 -0
- package/src/templates/features/script-example.yaml +27 -0
- package/src/templates/patterns/agent-handoff.yaml +53 -0
- package/src/templates/{approval-process.yaml → patterns/approval-process.yaml} +1 -0
- package/src/templates/{batch-processor.yaml → patterns/batch-processor.yaml} +2 -0
- package/src/templates/{composition-child.yaml → patterns/composition-child.yaml} +1 -0
- package/src/templates/{composition-parent.yaml → patterns/composition-parent.yaml} +1 -0
- package/src/templates/{data-pipeline.yaml → patterns/data-pipeline.yaml} +2 -0
- package/src/templates/{decompose-implement.yaml → scaffolding/decompose-implement.yaml} +1 -0
- package/src/templates/{decompose-problem.yaml → scaffolding/decompose-problem.yaml} +1 -0
- package/src/templates/{decompose-research.yaml → scaffolding/decompose-research.yaml} +1 -0
- package/src/templates/{decompose-review.yaml → scaffolding/decompose-review.yaml} +1 -0
- package/src/templates/{dev.yaml → scaffolding/dev.yaml} +1 -0
- package/src/templates/scaffolding/review-loop.yaml +97 -0
- package/src/templates/{scaffold-feature.yaml → scaffolding/scaffold-feature.yaml} +2 -0
- package/src/templates/{scaffold-generate.yaml → scaffolding/scaffold-generate.yaml} +1 -0
- package/src/templates/{scaffold-plan.yaml → scaffolding/scaffold-plan.yaml} +1 -0
- package/src/templates/testing/invalid.yaml +6 -0
- package/src/ui/dashboard.tsx +191 -33
- package/src/utils/auth-manager.test.ts +337 -0
- package/src/utils/auth-manager.ts +157 -61
- package/src/utils/blueprint-utils.ts +4 -6
- package/src/utils/config-loader.test.ts +2 -0
- package/src/utils/config-loader.ts +12 -3
- package/src/utils/constants.ts +76 -0
- package/src/utils/container.ts +63 -0
- package/src/utils/context-injector.test.ts +200 -0
- package/src/utils/context-injector.ts +244 -0
- package/src/utils/doc-generator.ts +85 -0
- package/src/utils/env-filter.ts +45 -0
- package/src/utils/json-parser.test.ts +12 -0
- package/src/utils/json-parser.ts +30 -5
- package/src/utils/logger.ts +12 -1
- package/src/utils/mermaid.ts +4 -0
- package/src/utils/paths.ts +52 -1
- package/src/utils/process-sandbox-worker.test.ts +46 -0
- package/src/utils/process-sandbox.ts +227 -14
- package/src/utils/redactor.test.ts +11 -6
- package/src/utils/redactor.ts +25 -9
- package/src/utils/sandbox.ts +3 -0
- package/src/utils/workflow-registry.test.ts +2 -2
- package/src/runner/llm-executor.ts +0 -638
- package/src/runner/shell-executor.ts +0 -366
- package/src/templates/invalid.yaml +0 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
@@ -6,19 +6,26 @@ import { MemoryDb } from '../db/memory-db.ts';
|
|
|
6
6
|
import { type RunStatus, WorkflowDb } from '../db/workflow-db.ts';
|
|
7
7
|
import type { ExpressionContext } from '../expression/evaluator.ts';
|
|
8
8
|
import { ExpressionEvaluator } from '../expression/evaluator.ts';
|
|
9
|
-
import type { Step, Workflow, WorkflowStep } from '../parser/schema.ts';
|
|
9
|
+
import type { LlmStep, PlanStep, Step, Workflow, WorkflowStep } from '../parser/schema.ts';
|
|
10
10
|
import { WorkflowParser } from '../parser/workflow-parser.ts';
|
|
11
11
|
import { StepStatus, type StepStatusType, WorkflowStatus } from '../types/status.ts';
|
|
12
12
|
import { ConfigLoader } from '../utils/config-loader.ts';
|
|
13
|
+
import { container } from '../utils/container.ts';
|
|
13
14
|
import { extractJson } from '../utils/json-parser.ts';
|
|
14
|
-
import {
|
|
15
|
+
import { ConsoleLogger, type Logger } from '../utils/logger.ts';
|
|
16
|
+
import type { Redactor } from '../utils/redactor.ts';
|
|
15
17
|
import { formatSchemaErrors, validateJsonSchema } from '../utils/schema-validator.ts';
|
|
16
18
|
import { WorkflowRegistry } from '../utils/workflow-registry.ts';
|
|
17
|
-
import {
|
|
19
|
+
import type { EventHandler, StepPhase, WorkflowEvent } from './events.ts';
|
|
20
|
+
import { ForeachExecutor } from './executors/foreach-executor.ts';
|
|
21
|
+
import { type RunnerFactory, executeSubWorkflow } from './executors/subworkflow-executor.ts';
|
|
18
22
|
import { type LLMMessage, getAdapter } from './llm-adapter.ts';
|
|
19
23
|
import { MCPManager } from './mcp-manager.ts';
|
|
20
24
|
import { ResourcePoolManager } from './resource-pool.ts';
|
|
21
25
|
import { withRetry } from './retry.ts';
|
|
26
|
+
import { ContextBuilder } from './services/context-builder.ts';
|
|
27
|
+
import { SecretManager } from './services/secret-manager.ts';
|
|
28
|
+
import { WorkflowValidator } from './services/workflow-validator.ts';
|
|
22
29
|
import {
|
|
23
30
|
type StepResult,
|
|
24
31
|
WorkflowSuspendedError,
|
|
@@ -26,8 +33,8 @@ import {
|
|
|
26
33
|
executeStep,
|
|
27
34
|
} from './step-executor.ts';
|
|
28
35
|
import { withTimeout } from './timeout.ts';
|
|
29
|
-
|
|
30
|
-
import {
|
|
36
|
+
import { WorkflowScheduler } from './workflow-scheduler.ts';
|
|
37
|
+
import { type ForeachStepContext, type StepContext, WorkflowState } from './workflow-state.ts';
|
|
31
38
|
|
|
32
39
|
/**
|
|
33
40
|
* A logger wrapper that redacts secrets from all log messages
|
|
@@ -74,6 +81,22 @@ function getWakeAt(output: unknown): string | undefined {
|
|
|
74
81
|
return typeof wakeAt === 'string' ? wakeAt : undefined;
|
|
75
82
|
}
|
|
76
83
|
|
|
84
|
+
const QUALITY_GATE_SCHEMA = {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
approved: { type: 'boolean' },
|
|
88
|
+
issues: { type: 'array', items: { type: 'string' } },
|
|
89
|
+
suggestions: { type: 'array', items: { type: 'string' } },
|
|
90
|
+
},
|
|
91
|
+
required: ['approved'],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type QualityGateReview = {
|
|
95
|
+
approved: boolean;
|
|
96
|
+
issues?: string[];
|
|
97
|
+
suggestions?: string[];
|
|
98
|
+
};
|
|
99
|
+
|
|
77
100
|
export interface RunOptions {
|
|
78
101
|
inputs?: Record<string, unknown>;
|
|
79
102
|
secrets?: Record<string, string>;
|
|
@@ -90,32 +113,20 @@ export interface RunOptions {
|
|
|
90
113
|
dedup?: boolean;
|
|
91
114
|
getAdapter?: typeof getAdapter;
|
|
92
115
|
executeStep?: typeof executeStep;
|
|
116
|
+
executeLlmStep?: typeof import('./executors/llm-executor.ts').executeLlmStep;
|
|
93
117
|
depth?: number;
|
|
94
118
|
allowSuccessResume?: boolean;
|
|
95
119
|
resourcePoolManager?: ResourcePoolManager;
|
|
96
120
|
allowInsecure?: boolean;
|
|
97
121
|
artifactRoot?: string;
|
|
122
|
+
db?: WorkflowDb;
|
|
123
|
+
memoryDb?: MemoryDb;
|
|
124
|
+
onEvent?: EventHandler;
|
|
125
|
+
memoize?: boolean;
|
|
126
|
+
signal?: AbortSignal;
|
|
98
127
|
}
|
|
99
128
|
|
|
100
|
-
|
|
101
|
-
output?: unknown;
|
|
102
|
-
outputs?: Record<string, unknown>;
|
|
103
|
-
status: StepStatusType;
|
|
104
|
-
error?: string;
|
|
105
|
-
usage?: {
|
|
106
|
-
prompt_tokens: number;
|
|
107
|
-
completion_tokens: number;
|
|
108
|
-
total_tokens: number;
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Type for foreach results - wraps array to ensure JSON serialization preserves all properties
|
|
113
|
-
export interface ForeachStepContext extends StepContext {
|
|
114
|
-
items: StepContext[]; // Individual iteration results
|
|
115
|
-
// output and outputs inherited from StepContext
|
|
116
|
-
// output: array of output values
|
|
117
|
-
// outputs: mapped outputs object
|
|
118
|
-
}
|
|
129
|
+
// Redacted StepContext and ForeachStepContext (moved to workflow-state.ts)
|
|
119
130
|
|
|
120
131
|
/**
|
|
121
132
|
* Main workflow execution engine
|
|
@@ -124,16 +135,18 @@ export class WorkflowRunner {
|
|
|
124
135
|
private workflow: Workflow;
|
|
125
136
|
private db: WorkflowDb;
|
|
126
137
|
private memoryDb: MemoryDb;
|
|
138
|
+
private contextMemory: Record<string, unknown> = {};
|
|
139
|
+
private envOverrides: Record<string, string> = {};
|
|
127
140
|
private _runId!: string;
|
|
128
|
-
private
|
|
141
|
+
private state!: WorkflowState;
|
|
142
|
+
private scheduler!: WorkflowScheduler;
|
|
129
143
|
private inputs!: Record<string, unknown>;
|
|
130
|
-
private
|
|
131
|
-
private
|
|
144
|
+
private secretManager: SecretManager;
|
|
145
|
+
private contextBuilder!: ContextBuilder;
|
|
146
|
+
private validator!: WorkflowValidator;
|
|
132
147
|
private rawLogger!: Logger;
|
|
133
|
-
private secretValues: string[] = [];
|
|
134
148
|
private redactAtRest = true;
|
|
135
149
|
private resumeRunId?: string;
|
|
136
|
-
private restored = false;
|
|
137
150
|
private logger!: Logger;
|
|
138
151
|
private mcpManager: MCPManager;
|
|
139
152
|
private options: RunOptions;
|
|
@@ -147,6 +160,7 @@ export class WorkflowRunner {
|
|
|
147
160
|
private lastFailedStep?: { id: string; error: string };
|
|
148
161
|
private abortController = new AbortController();
|
|
149
162
|
private resourcePool!: ResourcePoolManager;
|
|
163
|
+
private restored = false;
|
|
150
164
|
|
|
151
165
|
/**
|
|
152
166
|
* Get the abort signal for cancellation checks
|
|
@@ -162,6 +176,27 @@ export class WorkflowRunner {
|
|
|
162
176
|
return this.abortController.signal.aborted;
|
|
163
177
|
}
|
|
164
178
|
|
|
179
|
+
private createStepAbortController(): { controller: AbortController; cleanup: () => void } {
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
const parentSignal = this.abortSignal;
|
|
182
|
+
const onAbort = () => {
|
|
183
|
+
if (!controller.signal.aborted) {
|
|
184
|
+
controller.abort();
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (parentSignal.aborted) {
|
|
189
|
+
controller.abort();
|
|
190
|
+
return { controller, cleanup: () => {} };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
parentSignal.addEventListener('abort', onAbort, { once: true });
|
|
194
|
+
return {
|
|
195
|
+
controller,
|
|
196
|
+
cleanup: () => parentSignal.removeEventListener('abort', onAbort),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
165
200
|
constructor(workflow: Workflow, options: RunOptions = {}) {
|
|
166
201
|
this.workflow = workflow;
|
|
167
202
|
this.options = options;
|
|
@@ -173,23 +208,54 @@ export class WorkflowRunner {
|
|
|
173
208
|
);
|
|
174
209
|
}
|
|
175
210
|
|
|
176
|
-
|
|
177
|
-
this.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
211
|
+
// Use injected instances or resolve from container or create new from paths
|
|
212
|
+
this.db =
|
|
213
|
+
options.db ||
|
|
214
|
+
(options.dbPath
|
|
215
|
+
? new WorkflowDb(options.dbPath)
|
|
216
|
+
: container.resolveOptional<WorkflowDb>('db')) ||
|
|
217
|
+
new WorkflowDb(options.dbPath);
|
|
218
|
+
|
|
219
|
+
this.memoryDb =
|
|
220
|
+
options.memoryDb ||
|
|
221
|
+
(options.memoryDbPath
|
|
222
|
+
? new MemoryDb(options.memoryDbPath)
|
|
223
|
+
: container.resolveOptional<MemoryDb>('memoryDb')) ||
|
|
224
|
+
new MemoryDb(options.memoryDbPath);
|
|
225
|
+
|
|
226
|
+
this.secretManager = new SecretManager(options.secrets || {});
|
|
181
227
|
this.initLogger(options);
|
|
228
|
+
this.initRun(options);
|
|
229
|
+
|
|
230
|
+
this.validator = new WorkflowValidator(this.workflow, this.inputs);
|
|
231
|
+
this.contextBuilder = new ContextBuilder(
|
|
232
|
+
this.workflow,
|
|
233
|
+
this.inputs,
|
|
234
|
+
this.secretManager.getSecretValues(),
|
|
235
|
+
this.state,
|
|
236
|
+
this.logger
|
|
237
|
+
);
|
|
182
238
|
this.mcpManager = options.mcpManager || new MCPManager();
|
|
183
239
|
this.initResourcePool(options);
|
|
184
|
-
|
|
240
|
+
|
|
241
|
+
if (options.signal) {
|
|
242
|
+
if (options.signal.aborted) {
|
|
243
|
+
this.abortController.abort();
|
|
244
|
+
} else {
|
|
245
|
+
options.signal.addEventListener('abort', () => this.abortController.abort(), {
|
|
246
|
+
once: true,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
185
250
|
|
|
186
251
|
this.setupSignalHandlers();
|
|
187
252
|
}
|
|
188
253
|
|
|
189
254
|
private initLogger(options: RunOptions): void {
|
|
190
|
-
const rawLogger =
|
|
255
|
+
const rawLogger =
|
|
256
|
+
options.logger || container.resolveOptional<Logger>('logger') || new ConsoleLogger();
|
|
191
257
|
this.rawLogger = rawLogger;
|
|
192
|
-
this.logger = new RedactingLogger(rawLogger, this.
|
|
258
|
+
this.logger = new RedactingLogger(rawLogger, this.secretManager.getRedactor());
|
|
193
259
|
}
|
|
194
260
|
|
|
195
261
|
private initResourcePool(options: RunOptions): void {
|
|
@@ -201,7 +267,7 @@ export class WorkflowRunner {
|
|
|
201
267
|
const workflowPools: Record<string, number> = {};
|
|
202
268
|
|
|
203
269
|
if (this.workflow.pools) {
|
|
204
|
-
const baseContext = this.buildContext();
|
|
270
|
+
const baseContext = this.contextBuilder.buildContext(this.secretManager.getSecrets());
|
|
205
271
|
for (const [name, limit] of Object.entries(this.workflow.pools)) {
|
|
206
272
|
if (typeof limit === 'string') {
|
|
207
273
|
workflowPools[name] = Number(ExpressionEvaluator.evaluate(limit, baseContext));
|
|
@@ -227,6 +293,16 @@ export class WorkflowRunner {
|
|
|
227
293
|
this.inputs = options.inputs || {};
|
|
228
294
|
this._runId = randomUUID();
|
|
229
295
|
}
|
|
296
|
+
|
|
297
|
+
this.state = new WorkflowState(
|
|
298
|
+
this._runId,
|
|
299
|
+
this.workflow,
|
|
300
|
+
this.db,
|
|
301
|
+
this.inputs,
|
|
302
|
+
this.secretManager.getSecrets(),
|
|
303
|
+
this.logger
|
|
304
|
+
);
|
|
305
|
+
this.scheduler = new WorkflowScheduler(this.workflow, this.state.getCompletedStepIds());
|
|
230
306
|
}
|
|
231
307
|
|
|
232
308
|
/**
|
|
@@ -252,8 +328,6 @@ export class WorkflowRunner {
|
|
|
252
328
|
throw new Error(`Run ${this.runId} not found`);
|
|
253
329
|
}
|
|
254
330
|
|
|
255
|
-
// Only allow resuming failed, paused, canceled, or running (crash recovery) runs
|
|
256
|
-
// Unless specifically allowed (e.g. for rollback/compensation)
|
|
257
331
|
if (
|
|
258
332
|
run.status !== WorkflowStatus.FAILED &&
|
|
259
333
|
run.status !== WorkflowStatus.PAUSED &&
|
|
@@ -268,7 +342,7 @@ export class WorkflowRunner {
|
|
|
268
342
|
|
|
269
343
|
if (run.status === WorkflowStatus.RUNNING) {
|
|
270
344
|
this.logger.warn(
|
|
271
|
-
`⚠️ Resuming a run marked as 'running'. This usually means the previous process crashed or was killed forcefully
|
|
345
|
+
`⚠️ Resuming a run marked as 'running'. This usually means the previous process crashed or was killed forcefully.`
|
|
272
346
|
);
|
|
273
347
|
}
|
|
274
348
|
|
|
@@ -276,209 +350,20 @@ export class WorkflowRunner {
|
|
|
276
350
|
this.logger.log('📋 Resuming a previously canceled run. Completed steps will be skipped.');
|
|
277
351
|
}
|
|
278
352
|
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
if (!run.inputs || run.inputs === 'null' || run.inputs === '') {
|
|
283
|
-
this.logger.warn(`Run ${this.runId} has no persisted inputs`);
|
|
284
|
-
// Keep existing inputs
|
|
285
|
-
} else {
|
|
286
|
-
const storedInputs = JSON.parse(run.inputs);
|
|
287
|
-
this.inputs = { ...storedInputs, ...this.inputs };
|
|
288
|
-
}
|
|
289
|
-
} catch (error) {
|
|
290
|
-
this.logger.error(
|
|
291
|
-
`CRITICAL: Failed to parse inputs from run ${this.runId}. Data may be corrupted. Using default/resume inputs. Error: ${error instanceof Error ? error.message : String(error)}`
|
|
292
|
-
);
|
|
293
|
-
// Fallback: preserve existing inputs from resume options
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Load all step executions for this run
|
|
297
|
-
const steps = await this.db.getStepsByRun(this.runId);
|
|
298
|
-
|
|
299
|
-
// Group steps by step_id to handle foreach loops (multiple executions per step_id)
|
|
300
|
-
const stepExecutionsByStepId = new Map<string, typeof steps>();
|
|
301
|
-
for (const step of steps) {
|
|
302
|
-
if (!stepExecutionsByStepId.has(step.step_id)) {
|
|
303
|
-
stepExecutionsByStepId.set(step.step_id, []);
|
|
304
|
-
}
|
|
305
|
-
stepExecutionsByStepId.get(step.step_id)?.push(step);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Get topological order to ensure dependencies are restored before dependents
|
|
309
|
-
const executionOrder = WorkflowParser.topologicalSort(this.workflow);
|
|
310
|
-
const completedStepIds = new Set<string>();
|
|
311
|
-
|
|
312
|
-
// Reconstruct step contexts in topological order
|
|
313
|
-
for (const stepId of executionOrder) {
|
|
314
|
-
const stepExecutions = stepExecutionsByStepId.get(stepId);
|
|
315
|
-
if (!stepExecutions || stepExecutions.length === 0) continue;
|
|
316
|
-
|
|
317
|
-
const stepDef = this.workflow.steps.find((s) => s.id === stepId);
|
|
318
|
-
if (!stepDef) continue;
|
|
319
|
-
|
|
320
|
-
const isForeach = !!stepDef.foreach;
|
|
321
|
-
|
|
322
|
-
if (isForeach) {
|
|
323
|
-
// Reconstruct foreach aggregated context
|
|
324
|
-
const items: StepContext[] = [];
|
|
325
|
-
const outputs: unknown[] = [];
|
|
326
|
-
let allSuccess = true;
|
|
327
|
-
|
|
328
|
-
// Sort by iteration_index to ensure correct order
|
|
329
|
-
const sortedExecs = [...stepExecutions].sort(
|
|
330
|
-
(a, b) => (a.iteration_index ?? 0) - (b.iteration_index ?? 0)
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
for (const exec of sortedExecs) {
|
|
334
|
-
if (exec.iteration_index === null) continue; // Skip parent step record
|
|
335
|
-
|
|
336
|
-
if (exec.status === StepStatus.SUCCESS || exec.status === StepStatus.SKIPPED) {
|
|
337
|
-
let output: unknown = null;
|
|
338
|
-
try {
|
|
339
|
-
output = exec.output ? JSON.parse(exec.output) : null;
|
|
340
|
-
} catch (error) {
|
|
341
|
-
this.logger.warn(
|
|
342
|
-
`Failed to parse output for step ${stepId} iteration ${exec.iteration_index}: ${error}`
|
|
343
|
-
);
|
|
344
|
-
output = { error: 'Failed to parse output' };
|
|
345
|
-
}
|
|
346
|
-
items[exec.iteration_index] = {
|
|
347
|
-
output,
|
|
348
|
-
outputs:
|
|
349
|
-
typeof output === 'object' && output !== null && !Array.isArray(output)
|
|
350
|
-
? (output as Record<string, unknown>)
|
|
351
|
-
: {},
|
|
352
|
-
status: exec.status as typeof StepStatus.SUCCESS | typeof StepStatus.SKIPPED,
|
|
353
|
-
error: exec.error || undefined,
|
|
354
|
-
};
|
|
355
|
-
outputs[exec.iteration_index] = output;
|
|
356
|
-
} else {
|
|
357
|
-
allSuccess = false;
|
|
358
|
-
// Still populate with placeholder if failed
|
|
359
|
-
items[exec.iteration_index] = {
|
|
360
|
-
output: null,
|
|
361
|
-
outputs: {},
|
|
362
|
-
status: exec.status as StepStatusType,
|
|
363
|
-
error: exec.error || undefined,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Use persisted foreach items from parent step for deterministic resume
|
|
369
|
-
// This ensures the resume uses the same array as the initial run
|
|
370
|
-
let expectedCount = -1;
|
|
371
|
-
const parentExec = stepExecutions.find((e) => e.iteration_index === null);
|
|
372
|
-
if (parentExec?.output) {
|
|
373
|
-
try {
|
|
374
|
-
const parsed = JSON.parse(parentExec.output);
|
|
375
|
-
if (parsed.__foreachItems && Array.isArray(parsed.__foreachItems)) {
|
|
376
|
-
expectedCount = parsed.__foreachItems.length;
|
|
377
|
-
}
|
|
378
|
-
} catch (_e) {
|
|
379
|
-
// ignore parse errors
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Fallback to expression evaluation if persisted items not found
|
|
384
|
-
if (expectedCount === -1) {
|
|
385
|
-
try {
|
|
386
|
-
const baseContext = this.buildContext();
|
|
387
|
-
const foreachExpr = stepDef.foreach;
|
|
388
|
-
if (foreachExpr) {
|
|
389
|
-
const foreachItems = ExpressionEvaluator.evaluate(foreachExpr, baseContext);
|
|
390
|
-
if (Array.isArray(foreachItems)) {
|
|
391
|
-
expectedCount = foreachItems.length;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
} catch (e) {
|
|
395
|
-
// If we can't evaluate yet (dependencies not met?), we can't be sure it's complete
|
|
396
|
-
allSuccess = false;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Check if we have all items (no gaps)
|
|
401
|
-
const hasAllItems =
|
|
402
|
-
expectedCount !== -1 &&
|
|
403
|
-
items.length === expectedCount &&
|
|
404
|
-
!Array.from({ length: expectedCount }).some((_, i) => !items[i]);
|
|
405
|
-
|
|
406
|
-
// Determine overall status based on iterations
|
|
407
|
-
let status: StepContext['status'] = StepStatus.SUCCESS;
|
|
408
|
-
if (allSuccess && hasAllItems) {
|
|
409
|
-
status = StepStatus.SUCCESS;
|
|
410
|
-
} else if (items.some((item) => item?.status === StepStatus.SUSPENDED)) {
|
|
411
|
-
status = StepStatus.SUSPENDED;
|
|
412
|
-
} else {
|
|
413
|
-
status = StepStatus.FAILED;
|
|
414
|
-
}
|
|
353
|
+
// Hydrate state from DB
|
|
354
|
+
await this.state.restore();
|
|
415
355
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
status,
|
|
422
|
-
items,
|
|
423
|
-
} as ForeachStepContext);
|
|
424
|
-
|
|
425
|
-
// Only mark as fully completed if all iterations completed successfully AND we have all items
|
|
426
|
-
if (status === StepStatus.SUCCESS) {
|
|
427
|
-
completedStepIds.add(stepId);
|
|
428
|
-
}
|
|
429
|
-
} else {
|
|
430
|
-
// Single execution step
|
|
431
|
-
const exec = stepExecutions[0];
|
|
432
|
-
if (
|
|
433
|
-
exec.status === StepStatus.SUCCESS ||
|
|
434
|
-
exec.status === StepStatus.SKIPPED ||
|
|
435
|
-
exec.status === StepStatus.SUSPENDED ||
|
|
436
|
-
exec.status === StepStatus.WAITING
|
|
437
|
-
) {
|
|
438
|
-
let output: unknown = null;
|
|
439
|
-
try {
|
|
440
|
-
output = exec.output ? JSON.parse(exec.output) : null;
|
|
441
|
-
} catch (error) {
|
|
442
|
-
this.logger.warn(`Failed to parse output for step ${stepId}: ${error}`);
|
|
443
|
-
output = { error: 'Failed to parse output' };
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// If step is WAITING, check if timer has elapsed
|
|
447
|
-
let effectiveStatus = exec.status as StepContext['status'];
|
|
448
|
-
if (exec.status === StepStatus.WAITING) {
|
|
449
|
-
const timer = await this.db.getTimerByStep(this.runId, stepId);
|
|
450
|
-
const timerId = timer?.id;
|
|
451
|
-
const wakeAt = timer?.wake_at;
|
|
452
|
-
if (timerId && wakeAt && new Date(wakeAt) <= new Date()) {
|
|
453
|
-
// Timer elapsed!
|
|
454
|
-
await this.db.completeTimer(timerId);
|
|
455
|
-
await this.db.completeStep(exec.id, StepStatus.SUCCESS, output);
|
|
456
|
-
effectiveStatus = StepStatus.SUCCESS;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
let effectiveError = exec.error || undefined;
|
|
460
|
-
if (exec.status === StepStatus.WAITING && effectiveStatus === StepStatus.SUCCESS) {
|
|
461
|
-
effectiveError = undefined;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
this.stepContexts.set(stepId, {
|
|
465
|
-
output,
|
|
466
|
-
outputs:
|
|
467
|
-
typeof output === 'object' && output !== null && !Array.isArray(output)
|
|
468
|
-
? (output as Record<string, unknown>)
|
|
469
|
-
: {},
|
|
470
|
-
status: effectiveStatus,
|
|
471
|
-
error: effectiveError,
|
|
472
|
-
});
|
|
473
|
-
if (effectiveStatus !== StepStatus.SUSPENDED && effectiveStatus !== StepStatus.WAITING) {
|
|
474
|
-
completedStepIds.add(stepId);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
356
|
+
// Re-initialize scheduler with completed steps from restored state
|
|
357
|
+
const completedSteps = new Set<string>();
|
|
358
|
+
for (const [stepId, ctx] of this.state.entries()) {
|
|
359
|
+
if (ctx.status === StepStatus.SUCCESS || ctx.status === StepStatus.SKIPPED) {
|
|
360
|
+
completedSteps.add(stepId);
|
|
477
361
|
}
|
|
478
362
|
}
|
|
363
|
+
this.scheduler = new WorkflowScheduler(this.workflow, completedSteps);
|
|
479
364
|
|
|
480
365
|
this.restored = true;
|
|
481
|
-
this.logger.log(`✓ Restored state: ${
|
|
366
|
+
this.logger.log(`✓ Restored state: ${completedSteps.size} step(s) hydrated`);
|
|
482
367
|
}
|
|
483
368
|
|
|
484
369
|
/**
|
|
@@ -526,33 +411,40 @@ export class WorkflowRunner {
|
|
|
526
411
|
const stepDef = JSON.parse(compRecord.definition) as Step;
|
|
527
412
|
this.logger.log(` Running compensation: ${stepDef.id} (undoing ${compRecord.step_id})`);
|
|
528
413
|
|
|
529
|
-
await this.db.updateCompensationStatus(compRecord.id,
|
|
414
|
+
await this.db.updateCompensationStatus(compRecord.id, StepStatus.RUNNING);
|
|
530
415
|
|
|
531
416
|
// Build context for compensation
|
|
532
417
|
// It has access to the original step's output via steps.<step_id>.output
|
|
533
|
-
const context = this.buildContext();
|
|
418
|
+
const context = this.contextBuilder.buildContext(this.secretManager.getSecrets());
|
|
534
419
|
|
|
535
420
|
try {
|
|
536
421
|
// Execute the compensation step
|
|
537
422
|
const result = await executeStep(stepDef, context, this.logger, {
|
|
538
423
|
executeWorkflowFn: this.executeSubWorkflow.bind(this),
|
|
539
424
|
mcpManager: this.mcpManager,
|
|
425
|
+
db: this.db,
|
|
540
426
|
memoryDb: this.memoryDb,
|
|
541
427
|
workflowDir: this.options.workflowDir,
|
|
542
428
|
dryRun: this.options.dryRun,
|
|
543
429
|
runId: this.runId,
|
|
544
430
|
artifactRoot: this.options.artifactRoot,
|
|
545
431
|
redactForStorage: this.redactForStorage.bind(this),
|
|
432
|
+
emitEvent: this.emitEvent.bind(this),
|
|
433
|
+
workflowName: this.workflow.name,
|
|
546
434
|
});
|
|
547
435
|
|
|
548
|
-
if (result.status ===
|
|
436
|
+
if (result.status === StepStatus.SUCCESS) {
|
|
549
437
|
this.logger.log(` ✓ Compensation ${stepDef.id} succeeded`);
|
|
550
|
-
await this.db.updateCompensationStatus(
|
|
438
|
+
await this.db.updateCompensationStatus(
|
|
439
|
+
compRecord.id,
|
|
440
|
+
StepStatus.SUCCESS,
|
|
441
|
+
result.output
|
|
442
|
+
);
|
|
551
443
|
} else {
|
|
552
444
|
this.logger.error(` ✗ Compensation ${stepDef.id} failed: ${result.error}`);
|
|
553
445
|
await this.db.updateCompensationStatus(
|
|
554
446
|
compRecord.id,
|
|
555
|
-
|
|
447
|
+
StepStatus.FAILED,
|
|
556
448
|
result.output,
|
|
557
449
|
result.error
|
|
558
450
|
);
|
|
@@ -560,7 +452,7 @@ export class WorkflowRunner {
|
|
|
560
452
|
} catch (err) {
|
|
561
453
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
562
454
|
this.logger.error(` ✗ Compensation ${stepDef.id} crashed: ${errMsg}`);
|
|
563
|
-
await this.db.updateCompensationStatus(compRecord.id,
|
|
455
|
+
await this.db.updateCompensationStatus(compRecord.id, StepStatus.FAILED, null, errMsg);
|
|
564
456
|
}
|
|
565
457
|
|
|
566
458
|
// 2. Recursive rollback for sub-workflows
|
|
@@ -574,8 +466,10 @@ export class WorkflowRunner {
|
|
|
574
466
|
if (subRunId) {
|
|
575
467
|
await this.cascadeRollback(subRunId, errorReason);
|
|
576
468
|
}
|
|
577
|
-
} catch (
|
|
578
|
-
|
|
469
|
+
} catch (e) {
|
|
470
|
+
this.logger.warn(
|
|
471
|
+
` ⚠️ Failed to parse sub-workflow output for rollback: ${e instanceof Error ? e.message : String(e)}`
|
|
472
|
+
);
|
|
579
473
|
}
|
|
580
474
|
}
|
|
581
475
|
}
|
|
@@ -631,69 +525,36 @@ export class WorkflowRunner {
|
|
|
631
525
|
}
|
|
632
526
|
}
|
|
633
527
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
private loadSecrets(): Record<string, string> {
|
|
638
|
-
const secrets: Record<string, string> = { ...(this.options.secrets || {}) };
|
|
639
|
-
|
|
640
|
-
// Common non-secret environment variables to exclude from redaction
|
|
641
|
-
const blocklist = new Set([
|
|
642
|
-
'USER',
|
|
643
|
-
'PATH',
|
|
644
|
-
'SHELL',
|
|
645
|
-
'HOME',
|
|
646
|
-
'PWD',
|
|
647
|
-
'LOGNAME',
|
|
648
|
-
'LANG',
|
|
649
|
-
'TERM',
|
|
650
|
-
'EDITOR',
|
|
651
|
-
'VISUAL',
|
|
652
|
-
'_',
|
|
653
|
-
'SHLVL',
|
|
654
|
-
'LC_ALL',
|
|
655
|
-
'DISPLAY',
|
|
656
|
-
'SSH_AUTH_SOCK',
|
|
657
|
-
'XPC_FLAGS',
|
|
658
|
-
'XPC_SERVICE_NAME',
|
|
659
|
-
'ITERM_SESSION_ID',
|
|
660
|
-
'ITERM_PROFILE',
|
|
661
|
-
'TERM_PROGRAM',
|
|
662
|
-
'TERM_PROGRAM_VERSION',
|
|
663
|
-
'COLORTERM',
|
|
664
|
-
'LC_TERMINAL',
|
|
665
|
-
'LC_TERMINAL_VERSION',
|
|
666
|
-
'PWD',
|
|
667
|
-
'OLDPWD',
|
|
668
|
-
'HOME',
|
|
669
|
-
'USER',
|
|
670
|
-
'SHELL',
|
|
671
|
-
'PATH',
|
|
672
|
-
'LOGNAME',
|
|
673
|
-
'TMPDIR',
|
|
674
|
-
'XDG_CONFIG_HOME',
|
|
675
|
-
'XDG_DATA_HOME',
|
|
676
|
-
'XDG_CACHE_HOME',
|
|
677
|
-
'XDG_RUNTIME_DIR',
|
|
678
|
-
]);
|
|
679
|
-
|
|
680
|
-
// Bun automatically loads .env file
|
|
681
|
-
for (const [key, value] of Object.entries(Bun.env)) {
|
|
682
|
-
if (value && !blocklist.has(key)) {
|
|
683
|
-
secrets[key] = value;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
return secrets;
|
|
528
|
+
private redactForStorage<T>(value: T): T {
|
|
529
|
+
if (!this.redactAtRest) return value;
|
|
530
|
+
return this.secretManager.getRedactor().redactValue(value) as T;
|
|
687
531
|
}
|
|
688
532
|
|
|
689
|
-
private
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
533
|
+
private async calculateStepCacheKey(
|
|
534
|
+
step: Step,
|
|
535
|
+
inputs: Record<string, unknown>
|
|
536
|
+
): Promise<string | null> {
|
|
537
|
+
const memoizeEnabled = step.memoize ?? this.options.memoize ?? false;
|
|
538
|
+
if (!memoizeEnabled) return null;
|
|
693
539
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
540
|
+
// Only memoize deterministic steps by default unless explicitly requested
|
|
541
|
+
const cacheableTypes = ['shell', 'file', 'script', 'request', 'engine', 'blueprint'];
|
|
542
|
+
if (!cacheableTypes.includes(step.type) && step.memoize !== true) return null;
|
|
543
|
+
|
|
544
|
+
const data = {
|
|
545
|
+
type: step.type,
|
|
546
|
+
inputs,
|
|
547
|
+
env: step.env,
|
|
548
|
+
version: 2, // Cache versioning
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// Use runtime-agnostic hashing
|
|
552
|
+
// @ts-ignore - Check for Bun environment
|
|
553
|
+
const hash =
|
|
554
|
+
typeof Bun !== 'undefined'
|
|
555
|
+
? Bun.hash(JSON.stringify(data)).toString(16)
|
|
556
|
+
: createHash('sha256').update(JSON.stringify(data)).digest('hex');
|
|
557
|
+
return hash;
|
|
697
558
|
}
|
|
698
559
|
|
|
699
560
|
private validateSchema(
|
|
@@ -768,6 +629,24 @@ export class WorkflowRunner {
|
|
|
768
629
|
op: step.op,
|
|
769
630
|
allowOutsideCwd: step.allowOutsideCwd,
|
|
770
631
|
});
|
|
632
|
+
case 'artifact':
|
|
633
|
+
return stripUndefined({
|
|
634
|
+
op: step.op,
|
|
635
|
+
name: ExpressionEvaluator.evaluateString(
|
|
636
|
+
(step as import('../parser/schema.ts').ArtifactStep).name,
|
|
637
|
+
context
|
|
638
|
+
),
|
|
639
|
+
paths: (step as import('../parser/schema.ts').ArtifactStep).paths?.map((p) =>
|
|
640
|
+
ExpressionEvaluator.evaluateString(p, context)
|
|
641
|
+
),
|
|
642
|
+
path: (step as import('../parser/schema.ts').ArtifactStep).path
|
|
643
|
+
? ExpressionEvaluator.evaluateString(
|
|
644
|
+
(step as import('../parser/schema.ts').ArtifactStep).path as string,
|
|
645
|
+
context
|
|
646
|
+
)
|
|
647
|
+
: undefined,
|
|
648
|
+
allowOutsideCwd: step.allowOutsideCwd,
|
|
649
|
+
});
|
|
771
650
|
case 'request': {
|
|
772
651
|
let headers: Record<string, string> | undefined;
|
|
773
652
|
if (step.headers) {
|
|
@@ -804,9 +683,11 @@ export class WorkflowRunner {
|
|
|
804
683
|
}
|
|
805
684
|
case 'llm':
|
|
806
685
|
return stripUndefined({
|
|
807
|
-
agent: step.agent,
|
|
808
|
-
provider: step.provider
|
|
809
|
-
|
|
686
|
+
agent: ExpressionEvaluator.evaluateString(step.agent, context),
|
|
687
|
+
provider: step.provider
|
|
688
|
+
? ExpressionEvaluator.evaluateString(step.provider, context)
|
|
689
|
+
: undefined,
|
|
690
|
+
model: step.model ? ExpressionEvaluator.evaluateString(step.model, context) : undefined,
|
|
810
691
|
prompt: ExpressionEvaluator.evaluateString(step.prompt, context),
|
|
811
692
|
tools: step.tools,
|
|
812
693
|
maxIterations: step.maxIterations,
|
|
@@ -867,6 +748,14 @@ export class WorkflowRunner {
|
|
|
867
748
|
: undefined,
|
|
868
749
|
limit: step.limit,
|
|
869
750
|
});
|
|
751
|
+
case 'wait':
|
|
752
|
+
return stripUndefined({
|
|
753
|
+
event: ExpressionEvaluator.evaluateString(
|
|
754
|
+
(step as import('../parser/schema.ts').WaitStep).event,
|
|
755
|
+
context
|
|
756
|
+
),
|
|
757
|
+
oneShot: (step as import('../parser/schema.ts').WaitStep).oneShot,
|
|
758
|
+
});
|
|
870
759
|
default:
|
|
871
760
|
return {};
|
|
872
761
|
}
|
|
@@ -875,196 +764,6 @@ export class WorkflowRunner {
|
|
|
875
764
|
/**
|
|
876
765
|
* Collect primitive secret values from structured inputs.
|
|
877
766
|
*/
|
|
878
|
-
private static collectSecretValues(
|
|
879
|
-
value: unknown,
|
|
880
|
-
sink: Set<string>,
|
|
881
|
-
seen: WeakSet<object>
|
|
882
|
-
): void {
|
|
883
|
-
if (value === null || value === undefined) return;
|
|
884
|
-
|
|
885
|
-
if (typeof value === 'string') {
|
|
886
|
-
sink.add(value);
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
891
|
-
sink.add(String(value));
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
if (typeof value !== 'object') return;
|
|
896
|
-
|
|
897
|
-
if (seen.has(value)) return;
|
|
898
|
-
seen.add(value);
|
|
899
|
-
|
|
900
|
-
if (Array.isArray(value)) {
|
|
901
|
-
for (const item of value) {
|
|
902
|
-
WorkflowRunner.collectSecretValues(item, sink, seen);
|
|
903
|
-
}
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
for (const item of Object.values(value as Record<string, unknown>)) {
|
|
908
|
-
WorkflowRunner.collectSecretValues(item, sink, seen);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/**
|
|
913
|
-
* Apply workflow defaults to inputs and validate types
|
|
914
|
-
*/
|
|
915
|
-
private applyDefaultsAndValidate(): void {
|
|
916
|
-
if (!this.workflow.inputs) return;
|
|
917
|
-
|
|
918
|
-
const secretValues = new Set<string>();
|
|
919
|
-
|
|
920
|
-
for (const [key, config] of Object.entries(this.workflow.inputs)) {
|
|
921
|
-
// Apply default if missing
|
|
922
|
-
if (this.inputs[key] === undefined && config.default !== undefined) {
|
|
923
|
-
this.inputs[key] = config.default;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
if (config.secret) {
|
|
927
|
-
if (this.inputs[key] === WorkflowRunner.REDACTED_PLACEHOLDER) {
|
|
928
|
-
throw new Error(
|
|
929
|
-
`Secret input "${key}" was redacted at rest. Please provide it again to resume this run.`
|
|
930
|
-
);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
// Validate required inputs
|
|
935
|
-
if (this.inputs[key] === undefined) {
|
|
936
|
-
throw new Error(`Missing required input: ${key}`);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// Basic type validation
|
|
940
|
-
const value = this.inputs[key];
|
|
941
|
-
const type = config.type.toLowerCase();
|
|
942
|
-
|
|
943
|
-
if (type === 'string' && typeof value !== 'string') {
|
|
944
|
-
throw new Error(`Input "${key}" must be a string, got ${typeof value}`);
|
|
945
|
-
}
|
|
946
|
-
if (type === 'number' && typeof value !== 'number') {
|
|
947
|
-
throw new Error(`Input "${key}" must be a number, got ${typeof value}`);
|
|
948
|
-
}
|
|
949
|
-
if (type === 'boolean' && typeof value !== 'boolean') {
|
|
950
|
-
throw new Error(`Input "${key}" must be a boolean, got ${typeof value}`);
|
|
951
|
-
}
|
|
952
|
-
if (type === 'array' && !Array.isArray(value)) {
|
|
953
|
-
throw new Error(`Input "${key}" must be an array, got ${typeof value}`);
|
|
954
|
-
}
|
|
955
|
-
if (
|
|
956
|
-
type === 'object' &&
|
|
957
|
-
(typeof value !== 'object' || value === null || Array.isArray(value))
|
|
958
|
-
) {
|
|
959
|
-
throw new Error(`Input "${key}" must be an object, got ${typeof value}`);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
if (config.values) {
|
|
963
|
-
if (type !== 'string' && type !== 'number' && type !== 'boolean') {
|
|
964
|
-
throw new Error(`Input "${key}" cannot use enum values with type "${type}"`);
|
|
965
|
-
}
|
|
966
|
-
for (const allowed of config.values) {
|
|
967
|
-
const matchesType =
|
|
968
|
-
(type === 'string' && typeof allowed === 'string') ||
|
|
969
|
-
(type === 'number' && typeof allowed === 'number') ||
|
|
970
|
-
(type === 'boolean' && typeof allowed === 'boolean');
|
|
971
|
-
if (!matchesType) {
|
|
972
|
-
throw new Error(
|
|
973
|
-
`Input "${key}" enum value ${JSON.stringify(allowed)} must be a ${type}`
|
|
974
|
-
);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
if (!config.values.includes(value as string | number | boolean)) {
|
|
978
|
-
throw new Error(
|
|
979
|
-
`Input "${key}" must be one of: ${config.values.map((v) => JSON.stringify(v)).join(', ')}`
|
|
980
|
-
);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
if (config.secret && value !== undefined && value !== WorkflowRunner.REDACTED_PLACEHOLDER) {
|
|
985
|
-
WorkflowRunner.collectSecretValues(value, secretValues, new WeakSet());
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
this.secretValues = Array.from(secretValues);
|
|
990
|
-
this.refreshRedactor();
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
/**
|
|
994
|
-
* Build expression context for evaluation
|
|
995
|
-
*/
|
|
996
|
-
private buildContext(item?: unknown, index?: number): ExpressionContext {
|
|
997
|
-
const stepsContext: Record<
|
|
998
|
-
string,
|
|
999
|
-
{
|
|
1000
|
-
output?: unknown;
|
|
1001
|
-
outputs?: Record<string, unknown>;
|
|
1002
|
-
status?: string;
|
|
1003
|
-
error?: string;
|
|
1004
|
-
items?: StepContext[];
|
|
1005
|
-
}
|
|
1006
|
-
> = {};
|
|
1007
|
-
|
|
1008
|
-
for (const [stepId, ctx] of this.stepContexts.entries()) {
|
|
1009
|
-
// For foreach results, include items array for iteration access
|
|
1010
|
-
if ('items' in ctx && ctx.items) {
|
|
1011
|
-
stepsContext[stepId] = {
|
|
1012
|
-
output: ctx.output,
|
|
1013
|
-
outputs: ctx.outputs,
|
|
1014
|
-
status: ctx.status,
|
|
1015
|
-
error: ctx.error,
|
|
1016
|
-
items: ctx.items,
|
|
1017
|
-
};
|
|
1018
|
-
} else {
|
|
1019
|
-
stepsContext[stepId] = {
|
|
1020
|
-
output: ctx.output,
|
|
1021
|
-
outputs: ctx.outputs,
|
|
1022
|
-
status: ctx.status,
|
|
1023
|
-
error: ctx.error,
|
|
1024
|
-
};
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
const baseContext: ExpressionContext = {
|
|
1029
|
-
inputs: this.inputs,
|
|
1030
|
-
secrets: this.loadSecrets(), // Access secrets from options
|
|
1031
|
-
secretValues: this.secretValues,
|
|
1032
|
-
steps: stepsContext,
|
|
1033
|
-
item,
|
|
1034
|
-
index,
|
|
1035
|
-
env: {},
|
|
1036
|
-
output: item
|
|
1037
|
-
? undefined
|
|
1038
|
-
: this.stepContexts.get(this.workflow.steps.find((s) => !s.foreach)?.id || '')?.output,
|
|
1039
|
-
last_failed_step: this.lastFailedStep,
|
|
1040
|
-
};
|
|
1041
|
-
|
|
1042
|
-
const resolvedEnv: Record<string, string> = {};
|
|
1043
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
1044
|
-
if (value !== undefined) {
|
|
1045
|
-
resolvedEnv[key] = value;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
if (this.workflow.env) {
|
|
1050
|
-
for (const [key, value] of Object.entries(this.workflow.env)) {
|
|
1051
|
-
try {
|
|
1052
|
-
resolvedEnv[key] = ExpressionEvaluator.evaluateString(value, {
|
|
1053
|
-
...baseContext,
|
|
1054
|
-
env: resolvedEnv,
|
|
1055
|
-
});
|
|
1056
|
-
} catch (error) {
|
|
1057
|
-
this.logger.warn(
|
|
1058
|
-
`Warning: Failed to evaluate workflow env "${key}": ${error instanceof Error ? error.message : String(error)}`
|
|
1059
|
-
);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
baseContext.env = resolvedEnv;
|
|
1065
|
-
return baseContext;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
767
|
/**
|
|
1069
768
|
* Evaluate a conditional expression
|
|
1070
769
|
*/
|
|
@@ -1082,10 +781,9 @@ export class WorkflowRunner {
|
|
|
1082
781
|
try {
|
|
1083
782
|
return !this.evaluateCondition(step.if, context);
|
|
1084
783
|
} catch (error) {
|
|
1085
|
-
|
|
1086
|
-
`
|
|
784
|
+
throw new Error(
|
|
785
|
+
`Failed to evaluate condition for step "${step.id}": ${error instanceof Error ? error.message : String(error)}`
|
|
1087
786
|
);
|
|
1088
|
-
return true; // Skip on error
|
|
1089
787
|
}
|
|
1090
788
|
}
|
|
1091
789
|
|
|
@@ -1134,58 +832,30 @@ export class WorkflowRunner {
|
|
|
1134
832
|
try {
|
|
1135
833
|
await this.db.clearExpiredIdempotencyRecord(scopedKey);
|
|
1136
834
|
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
835
|
+
const result = await this.db.atomicClaimIdempotencyKey(
|
|
836
|
+
scopedKey,
|
|
837
|
+
this.runId,
|
|
838
|
+
stepId,
|
|
839
|
+
ttlSeconds
|
|
840
|
+
);
|
|
841
|
+
|
|
842
|
+
switch (result.status) {
|
|
843
|
+
case 'claimed':
|
|
844
|
+
return { status: 'claimed' };
|
|
845
|
+
case 'already-running':
|
|
846
|
+
return { status: 'in-flight' };
|
|
847
|
+
case 'completed': {
|
|
1140
848
|
let output: unknown = null;
|
|
1141
849
|
try {
|
|
1142
|
-
output =
|
|
850
|
+
output = result.record.output ? JSON.parse(result.record.output) : null;
|
|
1143
851
|
} catch (parseError) {
|
|
1144
852
|
this.logger.warn(
|
|
1145
853
|
` ⚠️ Failed to parse idempotency output for ${stepId}: ${parseError instanceof Error ? parseError.message : String(parseError)}`
|
|
1146
854
|
);
|
|
1147
855
|
}
|
|
1148
|
-
return { status: 'hit', output, error:
|
|
1149
|
-
}
|
|
1150
|
-
if (existing.status === StepStatus.RUNNING) {
|
|
1151
|
-
return { status: 'in-flight' };
|
|
856
|
+
return { status: 'hit', output, error: result.record.error || undefined };
|
|
1152
857
|
}
|
|
1153
|
-
|
|
1154
|
-
const claimed = await this.db.markIdempotencyRecordRunning(
|
|
1155
|
-
scopedKey,
|
|
1156
|
-
this.runId,
|
|
1157
|
-
stepId,
|
|
1158
|
-
ttlSeconds
|
|
1159
|
-
);
|
|
1160
|
-
if (claimed) {
|
|
1161
|
-
return { status: 'claimed' };
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
const inserted = await this.db.insertIdempotencyRecordIfAbsent(
|
|
1166
|
-
scopedKey,
|
|
1167
|
-
this.runId,
|
|
1168
|
-
stepId,
|
|
1169
|
-
StepStatus.RUNNING,
|
|
1170
|
-
ttlSeconds
|
|
1171
|
-
);
|
|
1172
|
-
if (inserted) {
|
|
1173
|
-
return { status: 'claimed' };
|
|
1174
858
|
}
|
|
1175
|
-
|
|
1176
|
-
const current = await this.db.getIdempotencyRecord(scopedKey);
|
|
1177
|
-
if (current?.status === StepStatus.SUCCESS) {
|
|
1178
|
-
let output: unknown = null;
|
|
1179
|
-
try {
|
|
1180
|
-
output = current.output ? JSON.parse(current.output) : null;
|
|
1181
|
-
} catch (parseError) {
|
|
1182
|
-
this.logger.warn(
|
|
1183
|
-
` ⚠️ Failed to parse idempotency output for ${stepId}: ${parseError instanceof Error ? parseError.message : String(parseError)}`
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
return { status: 'hit', output, error: current.error || undefined };
|
|
1187
|
-
}
|
|
1188
|
-
return { status: 'in-flight' };
|
|
1189
859
|
} catch (error) {
|
|
1190
860
|
this.logger.warn(
|
|
1191
861
|
` ⚠️ Failed to claim idempotency key for ${stepId}: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -1279,7 +949,7 @@ export class WorkflowRunner {
|
|
|
1279
949
|
stepExecId,
|
|
1280
950
|
StepStatus.FAILED,
|
|
1281
951
|
null,
|
|
1282
|
-
this.redactAtRest ? this.
|
|
952
|
+
this.secretManager.redactAtRest ? this.secretManager.redact(errorMsg) : errorMsg
|
|
1283
953
|
);
|
|
1284
954
|
return {
|
|
1285
955
|
output: null,
|
|
@@ -1292,6 +962,26 @@ export class WorkflowRunner {
|
|
|
1292
962
|
}
|
|
1293
963
|
}
|
|
1294
964
|
|
|
965
|
+
// Global step caching (memoization)
|
|
966
|
+
const inputs = this.contextBuilder.buildStepInputs(step, context);
|
|
967
|
+
const cacheKey = await this.calculateStepCacheKey(step, inputs);
|
|
968
|
+
if (cacheKey) {
|
|
969
|
+
const cached = await this.db.getStepCache(cacheKey);
|
|
970
|
+
if (cached) {
|
|
971
|
+
this.logger.log(` ⚡ Step ${step.id} skipped (global cache hit)`);
|
|
972
|
+
const output = JSON.parse(cached.output);
|
|
973
|
+
await this.db.completeStep(stepExecId, StepStatus.SUCCESS, output);
|
|
974
|
+
return {
|
|
975
|
+
output,
|
|
976
|
+
outputs:
|
|
977
|
+
typeof output === 'object' && output !== null && !Array.isArray(output)
|
|
978
|
+
? (output as Record<string, unknown>)
|
|
979
|
+
: {},
|
|
980
|
+
status: StepStatus.SUCCESS,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
1295
985
|
const idempotencyContextForRetry =
|
|
1296
986
|
idempotencyClaimed && scopedIdempotencyKey
|
|
1297
987
|
? {
|
|
@@ -1325,21 +1015,85 @@ export class WorkflowRunner {
|
|
|
1325
1015
|
await this.db.startStep(stepExecId);
|
|
1326
1016
|
}
|
|
1327
1017
|
|
|
1328
|
-
|
|
1018
|
+
if (stepToExecute.breakpoint && this.options.debug && !isRecursion) {
|
|
1019
|
+
if (!process.stdin.isTTY) {
|
|
1020
|
+
const message = `Breakpoint hit before step ${stepToExecute.id}`;
|
|
1021
|
+
if (dedupEnabled && idempotencyClaimed) {
|
|
1022
|
+
await this.recordIdempotencyResult(
|
|
1023
|
+
scopedIdempotencyKey,
|
|
1024
|
+
stepToExecute.id,
|
|
1025
|
+
StepStatus.SUSPENDED,
|
|
1026
|
+
null,
|
|
1027
|
+
message,
|
|
1028
|
+
idempotencyTtlSeconds
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
await this.db.completeStep(
|
|
1032
|
+
stepExecId,
|
|
1033
|
+
StepStatus.SUSPENDED,
|
|
1034
|
+
null,
|
|
1035
|
+
this.secretManager.redactAtRest ? this.secretManager.redact(message) : message
|
|
1036
|
+
);
|
|
1037
|
+
return {
|
|
1038
|
+
output: null,
|
|
1039
|
+
outputs: {},
|
|
1040
|
+
status: StepStatus.SUSPENDED,
|
|
1041
|
+
error: message,
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
try {
|
|
1046
|
+
const { DebugRepl } = await import('./debug-repl.ts');
|
|
1047
|
+
const repl = new DebugRepl(
|
|
1048
|
+
context,
|
|
1049
|
+
stepToExecute,
|
|
1050
|
+
undefined,
|
|
1051
|
+
this.logger,
|
|
1052
|
+
process.stdin,
|
|
1053
|
+
process.stdout,
|
|
1054
|
+
{
|
|
1055
|
+
mode: 'breakpoint',
|
|
1056
|
+
}
|
|
1057
|
+
);
|
|
1058
|
+
const action = await repl.start();
|
|
1059
|
+
|
|
1060
|
+
if (action.type === 'skip') {
|
|
1061
|
+
this.logger.log(` ⏭️ Skipping step ${stepToExecute.id} at breakpoint`);
|
|
1062
|
+
await this.db.completeStep(stepExecId, StepStatus.SKIPPED, null, undefined, undefined);
|
|
1063
|
+
return {
|
|
1064
|
+
output: null,
|
|
1065
|
+
outputs: {},
|
|
1066
|
+
status: StepStatus.SKIPPED,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (action.type === 'continue' || action.type === 'retry') {
|
|
1071
|
+
stepToExecute = action.modifiedStep || stepToExecute;
|
|
1072
|
+
}
|
|
1073
|
+
} catch (replError) {
|
|
1074
|
+
this.logger.error(` ✗ Debug REPL error: ${replError}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const operation = async (attemptContext: ExpressionContext, abortSignal?: AbortSignal) => {
|
|
1329
1079
|
const exec = this.options.executeStep || executeStep;
|
|
1330
|
-
|
|
1080
|
+
let result = await exec(stepToExecute, attemptContext, this.logger, {
|
|
1331
1081
|
executeWorkflowFn: this.executeSubWorkflow.bind(this),
|
|
1332
1082
|
mcpManager: this.mcpManager,
|
|
1083
|
+
db: this.db,
|
|
1333
1084
|
memoryDb: this.memoryDb,
|
|
1334
1085
|
workflowDir: this.options.workflowDir,
|
|
1335
1086
|
dryRun: this.options.dryRun,
|
|
1336
|
-
abortSignal
|
|
1087
|
+
abortSignal,
|
|
1337
1088
|
runId: this.runId,
|
|
1338
1089
|
stepExecutionId: stepExecId,
|
|
1339
1090
|
artifactRoot: this.options.artifactRoot,
|
|
1340
|
-
redactForStorage: this.redactForStorage.bind(this),
|
|
1091
|
+
redactForStorage: this.secretManager.redactForStorage.bind(this.secretManager),
|
|
1341
1092
|
getAdapter: this.options.getAdapter,
|
|
1342
1093
|
executeStep: this.options.executeStep || executeStep,
|
|
1094
|
+
executeLlmStep: this.options.executeLlmStep,
|
|
1095
|
+
emitEvent: this.emitEvent.bind(this),
|
|
1096
|
+
workflowName: this.workflow.name,
|
|
1343
1097
|
});
|
|
1344
1098
|
if (result.status === 'failed') {
|
|
1345
1099
|
throw new StepExecutionError(result);
|
|
@@ -1353,7 +1107,7 @@ export class WorkflowRunner {
|
|
|
1353
1107
|
'summary' in result.output
|
|
1354
1108
|
? (result.output as { summary?: unknown }).summary
|
|
1355
1109
|
: result.output;
|
|
1356
|
-
this.validateSchema(
|
|
1110
|
+
this.validator.validateSchema(
|
|
1357
1111
|
'output',
|
|
1358
1112
|
stepToExecute.outputSchema,
|
|
1359
1113
|
outputForValidation,
|
|
@@ -1363,6 +1117,7 @@ export class WorkflowRunner {
|
|
|
1363
1117
|
const message = error instanceof Error ? error.message : String(error);
|
|
1364
1118
|
const outputRetries = stepToExecute.outputRetries || 0;
|
|
1365
1119
|
const currentAttempt = (attemptContext.outputRepairAttempts as number) || 0;
|
|
1120
|
+
let handled = false;
|
|
1366
1121
|
|
|
1367
1122
|
// Only attempt repair for LLM steps with outputRetries configured
|
|
1368
1123
|
if (stepToExecute.type === 'llm' && outputRetries > 0 && currentAttempt < outputRetries) {
|
|
@@ -1396,15 +1151,19 @@ export class WorkflowRunner {
|
|
|
1396
1151
|
const repairResult = await exec(repairStep, repairContext, this.logger, {
|
|
1397
1152
|
executeWorkflowFn: this.executeSubWorkflow.bind(this),
|
|
1398
1153
|
mcpManager: this.mcpManager,
|
|
1154
|
+
db: this.db,
|
|
1399
1155
|
memoryDb: this.memoryDb,
|
|
1400
1156
|
workflowDir: this.options.workflowDir,
|
|
1401
1157
|
dryRun: this.options.dryRun,
|
|
1402
|
-
abortSignal
|
|
1158
|
+
abortSignal,
|
|
1403
1159
|
runId: this.runId,
|
|
1404
1160
|
stepExecutionId: stepExecId,
|
|
1405
1161
|
artifactRoot: this.options.artifactRoot,
|
|
1406
|
-
redactForStorage: this.redactForStorage.bind(this),
|
|
1162
|
+
redactForStorage: this.secretManager.redactForStorage.bind(this.secretManager),
|
|
1407
1163
|
executeStep: this.options.executeStep || executeStep,
|
|
1164
|
+
executeLlmStep: this.options.executeLlmStep,
|
|
1165
|
+
emitEvent: this.emitEvent.bind(this),
|
|
1166
|
+
workflowName: this.workflow.name,
|
|
1408
1167
|
});
|
|
1409
1168
|
|
|
1410
1169
|
if (repairResult.status === 'failed') {
|
|
@@ -1413,7 +1172,7 @@ export class WorkflowRunner {
|
|
|
1413
1172
|
|
|
1414
1173
|
// Validate the repaired output
|
|
1415
1174
|
try {
|
|
1416
|
-
this.validateSchema(
|
|
1175
|
+
this.validator.validateSchema(
|
|
1417
1176
|
'output',
|
|
1418
1177
|
stepToExecute.outputSchema,
|
|
1419
1178
|
repairResult.output,
|
|
@@ -1422,15 +1181,19 @@ export class WorkflowRunner {
|
|
|
1422
1181
|
this.logger.log(
|
|
1423
1182
|
` ✓ Output repair successful after ${currentAttempt + 1} attempt(s)`
|
|
1424
1183
|
);
|
|
1425
|
-
|
|
1184
|
+
result = repairResult;
|
|
1185
|
+
handled = true;
|
|
1426
1186
|
} catch (repairError) {
|
|
1427
1187
|
// If still failing, either retry again or give up
|
|
1428
1188
|
if (currentAttempt + 1 < outputRetries) {
|
|
1429
1189
|
// Try again with updated context
|
|
1430
|
-
return operation(
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1190
|
+
return operation(
|
|
1191
|
+
{
|
|
1192
|
+
...attemptContext,
|
|
1193
|
+
outputRepairAttempts: currentAttempt + 1,
|
|
1194
|
+
},
|
|
1195
|
+
abortSignal
|
|
1196
|
+
);
|
|
1434
1197
|
}
|
|
1435
1198
|
const repairMessage =
|
|
1436
1199
|
repairError instanceof Error ? repairError.message : String(repairError);
|
|
@@ -1442,20 +1205,29 @@ export class WorkflowRunner {
|
|
|
1442
1205
|
}
|
|
1443
1206
|
}
|
|
1444
1207
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1208
|
+
if (!handled) {
|
|
1209
|
+
throw new StepExecutionError({
|
|
1210
|
+
...result,
|
|
1211
|
+
status: 'failed',
|
|
1212
|
+
error: message,
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1450
1215
|
}
|
|
1451
1216
|
}
|
|
1217
|
+
if (
|
|
1218
|
+
result.status === 'success' &&
|
|
1219
|
+
(stepToExecute.type === 'llm' || stepToExecute.type === 'plan') &&
|
|
1220
|
+
stepToExecute.qualityGate
|
|
1221
|
+
) {
|
|
1222
|
+
result = await this.runQualityGate(stepToExecute, result, attemptContext, abortSignal);
|
|
1223
|
+
}
|
|
1452
1224
|
return result;
|
|
1453
1225
|
};
|
|
1454
1226
|
|
|
1455
1227
|
try {
|
|
1456
1228
|
if (stepToExecute.inputSchema) {
|
|
1457
|
-
const inputsForValidation = this.buildStepInputs(stepToExecute, context);
|
|
1458
|
-
this.validateSchema(
|
|
1229
|
+
const inputsForValidation = this.contextBuilder.buildStepInputs(stepToExecute, context);
|
|
1230
|
+
this.validator.validateSchema(
|
|
1459
1231
|
'input',
|
|
1460
1232
|
stepToExecute.inputSchema,
|
|
1461
1233
|
inputsForValidation,
|
|
@@ -1464,10 +1236,18 @@ export class WorkflowRunner {
|
|
|
1464
1236
|
}
|
|
1465
1237
|
|
|
1466
1238
|
const operationWithTimeout = async () => {
|
|
1467
|
-
|
|
1468
|
-
|
|
1239
|
+
const { controller, cleanup } = this.createStepAbortController();
|
|
1240
|
+
try {
|
|
1241
|
+
const attempt = operation(context, controller.signal);
|
|
1242
|
+
if (step.timeout) {
|
|
1243
|
+
return await withTimeout(attempt, step.timeout, `Step ${step.id}`, {
|
|
1244
|
+
abortController: controller,
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
return await attempt;
|
|
1248
|
+
} finally {
|
|
1249
|
+
cleanup();
|
|
1469
1250
|
}
|
|
1470
|
-
return await operation(context);
|
|
1471
1251
|
};
|
|
1472
1252
|
|
|
1473
1253
|
const result = await withRetry(operationWithTimeout, step.retry, async (attempt, error) => {
|
|
@@ -1475,10 +1255,10 @@ export class WorkflowRunner {
|
|
|
1475
1255
|
await this.db.incrementRetry(stepExecId);
|
|
1476
1256
|
});
|
|
1477
1257
|
|
|
1478
|
-
const persistedOutput = this.redactForStorage(result.output);
|
|
1258
|
+
const persistedOutput = this.secretManager.redactForStorage(result.output);
|
|
1479
1259
|
const persistedError = result.error
|
|
1480
|
-
? this.redactAtRest
|
|
1481
|
-
? this.
|
|
1260
|
+
? this.secretManager.redactAtRest
|
|
1261
|
+
? this.secretManager.redact(result.error)
|
|
1482
1262
|
: result.error
|
|
1483
1263
|
: result.error;
|
|
1484
1264
|
|
|
@@ -1504,8 +1284,8 @@ export class WorkflowRunner {
|
|
|
1504
1284
|
stepExecId,
|
|
1505
1285
|
StepStatus.SUSPENDED,
|
|
1506
1286
|
persistedOutput,
|
|
1507
|
-
this.redactAtRest
|
|
1508
|
-
? this.
|
|
1287
|
+
this.secretManager.redactAtRest
|
|
1288
|
+
? this.secretManager.redact('Waiting for interaction')
|
|
1509
1289
|
: 'Waiting for interaction',
|
|
1510
1290
|
result.usage
|
|
1511
1291
|
);
|
|
@@ -1535,7 +1315,7 @@ export class WorkflowRunner {
|
|
|
1535
1315
|
stepExecId,
|
|
1536
1316
|
StepStatus.WAITING,
|
|
1537
1317
|
persistedOutput,
|
|
1538
|
-
this.redactAtRest ? this.
|
|
1318
|
+
this.secretManager.redactAtRest ? this.secretManager.redact(waitError) : waitError,
|
|
1539
1319
|
result.usage
|
|
1540
1320
|
);
|
|
1541
1321
|
result.error = waitError;
|
|
@@ -1549,7 +1329,7 @@ export class WorkflowRunner {
|
|
|
1549
1329
|
persistedError,
|
|
1550
1330
|
result.usage
|
|
1551
1331
|
);
|
|
1552
|
-
if (
|
|
1332
|
+
if (result.status === StepStatus.SUCCESS) {
|
|
1553
1333
|
const existingTimer = await this.db.getTimerByStep(this.runId, step.id);
|
|
1554
1334
|
if (existingTimer) {
|
|
1555
1335
|
await this.db.completeTimer(existingTimer.id);
|
|
@@ -1618,14 +1398,25 @@ export class WorkflowRunner {
|
|
|
1618
1398
|
);
|
|
1619
1399
|
}
|
|
1620
1400
|
|
|
1621
|
-
|
|
1401
|
+
const finalResult = {
|
|
1622
1402
|
output: result.output,
|
|
1623
1403
|
outputs,
|
|
1624
1404
|
status: result.status,
|
|
1625
1405
|
error: result.error,
|
|
1626
1406
|
usage: result.usage,
|
|
1627
1407
|
};
|
|
1408
|
+
|
|
1409
|
+
// Store in global cache if enabled
|
|
1410
|
+
if (cacheKey && result.status === StepStatus.SUCCESS) {
|
|
1411
|
+
const ttl = step.memoizeTtlSeconds;
|
|
1412
|
+
await this.db.storeStepCache(cacheKey, this.workflow.name, step.id, persistedOutput, ttl);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
return finalResult;
|
|
1628
1416
|
} catch (error) {
|
|
1417
|
+
if (error instanceof WorkflowSuspendedError || error instanceof WorkflowWaitingError) {
|
|
1418
|
+
throw error;
|
|
1419
|
+
}
|
|
1629
1420
|
// Reflexion (Self-Correction) logic
|
|
1630
1421
|
if (step.reflexion) {
|
|
1631
1422
|
const { limit = 3, hint } = step.reflexion;
|
|
@@ -1742,7 +1533,7 @@ export class WorkflowRunner {
|
|
|
1742
1533
|
const failureResult = error instanceof StepExecutionError ? error.result : null;
|
|
1743
1534
|
const errorMsg =
|
|
1744
1535
|
failureResult?.error || (error instanceof Error ? error.message : String(error));
|
|
1745
|
-
const redactedErrorMsg = this.
|
|
1536
|
+
const redactedErrorMsg = this.secretManager.redact(errorMsg);
|
|
1746
1537
|
const failureOutput = failureResult?.output ?? null;
|
|
1747
1538
|
const failureOutputs =
|
|
1748
1539
|
typeof failureOutput === 'object' && failureOutput !== null && !Array.isArray(failureOutput)
|
|
@@ -1756,8 +1547,8 @@ export class WorkflowRunner {
|
|
|
1756
1547
|
await this.db.completeStep(
|
|
1757
1548
|
stepExecId,
|
|
1758
1549
|
StepStatus.SUCCESS,
|
|
1759
|
-
this.redactForStorage(failureOutput),
|
|
1760
|
-
this.redactAtRest ? redactedErrorMsg : errorMsg
|
|
1550
|
+
this.secretManager.redactForStorage(failureOutput),
|
|
1551
|
+
this.secretManager.redactAtRest ? redactedErrorMsg : errorMsg
|
|
1761
1552
|
);
|
|
1762
1553
|
if (dedupEnabled && idempotencyClaimed) {
|
|
1763
1554
|
await this.recordIdempotencyResult(
|
|
@@ -1781,8 +1572,8 @@ export class WorkflowRunner {
|
|
|
1781
1572
|
await this.db.completeStep(
|
|
1782
1573
|
stepExecId,
|
|
1783
1574
|
StepStatus.FAILED,
|
|
1784
|
-
this.redactForStorage(failureOutput),
|
|
1785
|
-
this.redactAtRest ? redactedErrorMsg : errorMsg
|
|
1575
|
+
this.secretManager.redactForStorage(failureOutput),
|
|
1576
|
+
this.secretManager.redactAtRest ? redactedErrorMsg : errorMsg
|
|
1786
1577
|
);
|
|
1787
1578
|
if (dedupEnabled && idempotencyClaimed) {
|
|
1788
1579
|
await this.recordIdempotencyResult(
|
|
@@ -1861,9 +1652,11 @@ Do not change the 'id' or 'type' or 'auto_heal' fields.
|
|
|
1861
1652
|
debug: this.options.debug,
|
|
1862
1653
|
runId: this.runId,
|
|
1863
1654
|
artifactRoot: this.options.artifactRoot,
|
|
1864
|
-
redactForStorage: this.redactForStorage.bind(this),
|
|
1655
|
+
redactForStorage: this.secretManager.redactForStorage.bind(this.secretManager),
|
|
1865
1656
|
allowInsecure: this.options.allowInsecure,
|
|
1866
1657
|
executeStep: this.options.executeStep || executeStep,
|
|
1658
|
+
emitEvent: this.emitEvent.bind(this),
|
|
1659
|
+
workflowName: this.workflow.name,
|
|
1867
1660
|
});
|
|
1868
1661
|
|
|
1869
1662
|
if (result.status !== 'success' || !result.output) {
|
|
@@ -1892,11 +1685,9 @@ Do not change the 'id' or 'type' or 'auto_heal' fields.
|
|
|
1892
1685
|
let textToEmbed = `Step ID: ${step.id} (${step.type})\n`;
|
|
1893
1686
|
|
|
1894
1687
|
if (step.type === 'llm') {
|
|
1895
|
-
|
|
1896
|
-
textToEmbed += `Task Context/Prompt:\n${(step as any).prompt}\n\n`;
|
|
1688
|
+
textToEmbed += `Task Context/Prompt:\n${(step as LlmStep).prompt}\n\n`;
|
|
1897
1689
|
} else if (step.type === 'shell') {
|
|
1898
|
-
|
|
1899
|
-
textToEmbed += `Command:\n${(step as any).run}\n\n`;
|
|
1690
|
+
textToEmbed += `Command:\n${(step as unknown as { run: string }).run}\n\n`;
|
|
1900
1691
|
}
|
|
1901
1692
|
|
|
1902
1693
|
textToEmbed += `Successful Outcome:\n${JSON.stringify(result.output, null, 2)}`;
|
|
@@ -1933,8 +1724,7 @@ Rules:
|
|
|
1933
1724
|
- Creating missing directories
|
|
1934
1725
|
- Adjusting flags or arguments`;
|
|
1935
1726
|
|
|
1936
|
-
|
|
1937
|
-
const runCommand = (step as any).run;
|
|
1727
|
+
const runCommand = (step as unknown as { run: string }).run;
|
|
1938
1728
|
const userContent = `The following step failed:
|
|
1939
1729
|
\`\`\`json
|
|
1940
1730
|
${JSON.stringify({ type: step.type, run: runCommand }, null, 2)}
|
|
@@ -1952,31 +1742,14 @@ Please provide the fixed step configuration as JSON.`;
|
|
|
1952
1742
|
{ role: 'user', content: userContent },
|
|
1953
1743
|
];
|
|
1954
1744
|
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
const { adapter, resolvedModel } = getAdapterFn('gpt-4o');
|
|
1960
|
-
this.logger.log(` 🤖 Mechanic is analyzing the failure using ${resolvedModel}...`);
|
|
1961
|
-
|
|
1962
|
-
const response = await adapter.chat(messages, {
|
|
1963
|
-
model: resolvedModel,
|
|
1964
|
-
});
|
|
1745
|
+
// Use the default model (gpt-4o) or configured default for the Mechanic
|
|
1746
|
+
// We'll use gpt-4o as a strong default for this reasoning task
|
|
1747
|
+
const getAdapterFn = this.options.getAdapter || getAdapter;
|
|
1748
|
+
const { adapter } = getAdapterFn('gpt-4o');
|
|
1965
1749
|
|
|
1966
|
-
|
|
1967
|
-
if (!content) {
|
|
1968
|
-
throw new Error('Mechanic returned empty response');
|
|
1969
|
-
}
|
|
1750
|
+
const response = await adapter.chat(messages);
|
|
1970
1751
|
|
|
1971
|
-
|
|
1972
|
-
const fixedConfig = extractJson(content) as Partial<Step>;
|
|
1973
|
-
return fixedConfig;
|
|
1974
|
-
} catch (e) {
|
|
1975
|
-
throw new Error(`Failed to parse Mechanic's response as JSON: ${content}`);
|
|
1976
|
-
}
|
|
1977
|
-
} catch (err) {
|
|
1978
|
-
throw new Error(`Mechanic unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
1979
|
-
}
|
|
1752
|
+
return extractJson(response.message.content || '{}') as Partial<Step>;
|
|
1980
1753
|
}
|
|
1981
1754
|
|
|
1982
1755
|
/**
|
|
@@ -2026,18 +1799,233 @@ ${strategyInstructions[strategy]}
|
|
|
2026
1799
|
Please provide a corrected response that exactly matches the required schema.`;
|
|
2027
1800
|
}
|
|
2028
1801
|
|
|
1802
|
+
private buildPlanPromptFromStep(step: PlanStep, context: ExpressionContext): string {
|
|
1803
|
+
const goal = ExpressionEvaluator.evaluateString(step.goal, context);
|
|
1804
|
+
const contextText = step.context
|
|
1805
|
+
? ExpressionEvaluator.evaluateString(step.context, context)
|
|
1806
|
+
: '';
|
|
1807
|
+
const constraintsText = step.constraints
|
|
1808
|
+
? ExpressionEvaluator.evaluateString(step.constraints, context)
|
|
1809
|
+
: '';
|
|
1810
|
+
|
|
1811
|
+
return `You are a planner. Break the goal into a concise, ordered list of steps.
|
|
1812
|
+
|
|
1813
|
+
Goal:
|
|
1814
|
+
${goal}
|
|
1815
|
+
|
|
1816
|
+
Context:
|
|
1817
|
+
${contextText || 'None'}
|
|
1818
|
+
|
|
1819
|
+
Constraints:
|
|
1820
|
+
${constraintsText || 'None'}
|
|
1821
|
+
|
|
1822
|
+
Each step should be small, specific, and independently executable.
|
|
1823
|
+
Include any dependencies under "needs" and optional "workflow" or "inputs" when appropriate.
|
|
1824
|
+
|
|
1825
|
+
Return only the structured JSON required by the schema.`;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
private buildQualityGateReviewPrompt(
|
|
1829
|
+
step: LlmStep | PlanStep,
|
|
1830
|
+
output: unknown,
|
|
1831
|
+
gatePrompt: string | undefined,
|
|
1832
|
+
context: ExpressionContext
|
|
1833
|
+
): string {
|
|
1834
|
+
const reviewContext = {
|
|
1835
|
+
...context,
|
|
1836
|
+
output,
|
|
1837
|
+
};
|
|
1838
|
+
|
|
1839
|
+
if (gatePrompt) {
|
|
1840
|
+
return ExpressionEvaluator.evaluateString(gatePrompt, reviewContext);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
const taskDescription =
|
|
1844
|
+
step.type === 'plan' ? this.buildPlanPromptFromStep(step, context) : step.prompt;
|
|
1845
|
+
|
|
1846
|
+
return `Review the output for correctness, completeness, and clarity.
|
|
1847
|
+
|
|
1848
|
+
Task:
|
|
1849
|
+
${taskDescription}
|
|
1850
|
+
|
|
1851
|
+
Output:
|
|
1852
|
+
${typeof output === 'string' ? output : JSON.stringify(output, null, 2)}
|
|
1853
|
+
|
|
1854
|
+
Identify issues, risks, and missing details. Be specific.
|
|
1855
|
+
Return only the structured JSON required by the schema.`;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
private buildQualityGateRefinePrompt(
|
|
1859
|
+
step: LlmStep | PlanStep,
|
|
1860
|
+
output: unknown,
|
|
1861
|
+
review: QualityGateReview,
|
|
1862
|
+
context: ExpressionContext
|
|
1863
|
+
): string {
|
|
1864
|
+
const basePrompt =
|
|
1865
|
+
step.type === 'plan' ? this.buildPlanPromptFromStep(step, context) : step.prompt;
|
|
1866
|
+
const reviewText = JSON.stringify(review, null, 2);
|
|
1867
|
+
const outputText = typeof output === 'string' ? output : JSON.stringify(output, null, 2);
|
|
1868
|
+
|
|
1869
|
+
return `${basePrompt}
|
|
1870
|
+
|
|
1871
|
+
---
|
|
1872
|
+
|
|
1873
|
+
QUALITY REVIEW FAILED
|
|
1874
|
+
|
|
1875
|
+
Reviewer feedback:
|
|
1876
|
+
${reviewText}
|
|
1877
|
+
|
|
1878
|
+
Previous output:
|
|
1879
|
+
${outputText}
|
|
1880
|
+
|
|
1881
|
+
Revise the output to address the feedback. Return only the corrected output.`;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
private async runQualityGate(
|
|
1885
|
+
step: LlmStep | PlanStep,
|
|
1886
|
+
result: StepResult,
|
|
1887
|
+
context: ExpressionContext,
|
|
1888
|
+
abortSignal?: AbortSignal
|
|
1889
|
+
): Promise<StepResult> {
|
|
1890
|
+
const gate = step.qualityGate;
|
|
1891
|
+
if (!gate) return result;
|
|
1892
|
+
|
|
1893
|
+
let attempts = (context.qualityGateAttempts as number) || 0;
|
|
1894
|
+
const maxAttempts = gate.maxAttempts ?? 1;
|
|
1895
|
+
let currentResult = result;
|
|
1896
|
+
|
|
1897
|
+
while (true) {
|
|
1898
|
+
if (abortSignal?.aborted) {
|
|
1899
|
+
throw new Error('Step canceled');
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
const reviewContext = {
|
|
1903
|
+
...context,
|
|
1904
|
+
output: currentResult.output,
|
|
1905
|
+
qualityGateAttempts: attempts,
|
|
1906
|
+
};
|
|
1907
|
+
const reviewPrompt = this.buildQualityGateReviewPrompt(
|
|
1908
|
+
step,
|
|
1909
|
+
currentResult.output,
|
|
1910
|
+
gate.prompt,
|
|
1911
|
+
reviewContext
|
|
1912
|
+
);
|
|
1913
|
+
const reviewStep: Step = {
|
|
1914
|
+
id: `${step.id}-quality-review`,
|
|
1915
|
+
type: 'llm',
|
|
1916
|
+
agent: gate.agent,
|
|
1917
|
+
provider: gate.provider,
|
|
1918
|
+
model: gate.model,
|
|
1919
|
+
prompt: reviewPrompt,
|
|
1920
|
+
outputSchema: QUALITY_GATE_SCHEMA,
|
|
1921
|
+
} as LlmStep;
|
|
1922
|
+
|
|
1923
|
+
const exec = this.options.executeStep || executeStep;
|
|
1924
|
+
const reviewResult = await exec(reviewStep, reviewContext, this.logger, {
|
|
1925
|
+
executeWorkflowFn: this.executeSubWorkflow.bind(this),
|
|
1926
|
+
mcpManager: this.mcpManager,
|
|
1927
|
+
db: this.db,
|
|
1928
|
+
memoryDb: this.memoryDb,
|
|
1929
|
+
workflowDir: this.options.workflowDir,
|
|
1930
|
+
dryRun: this.options.dryRun,
|
|
1931
|
+
abortSignal,
|
|
1932
|
+
runId: this.runId,
|
|
1933
|
+
artifactRoot: this.options.artifactRoot,
|
|
1934
|
+
redactForStorage: this.secretManager.redactForStorage.bind(this.secretManager),
|
|
1935
|
+
getAdapter: this.options.getAdapter,
|
|
1936
|
+
executeStep: this.options.executeStep || executeStep,
|
|
1937
|
+
emitEvent: this.emitEvent.bind(this),
|
|
1938
|
+
workflowName: this.workflow.name,
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1941
|
+
if (reviewResult.status !== 'success' || !reviewResult.output) {
|
|
1942
|
+
throw new StepExecutionError({
|
|
1943
|
+
...reviewResult,
|
|
1944
|
+
status: 'failed',
|
|
1945
|
+
error: reviewResult.error || 'Quality gate review failed',
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
this.validator.validateSchema(
|
|
1950
|
+
'output',
|
|
1951
|
+
QUALITY_GATE_SCHEMA,
|
|
1952
|
+
reviewResult.output,
|
|
1953
|
+
reviewStep.id
|
|
1954
|
+
);
|
|
1955
|
+
|
|
1956
|
+
const review = reviewResult.output as QualityGateReview;
|
|
1957
|
+
if (review.approved) {
|
|
1958
|
+
return currentResult;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
if (attempts >= maxAttempts) {
|
|
1962
|
+
const issues = review.issues?.join('; ') || 'Quality gate rejected output';
|
|
1963
|
+
throw new StepExecutionError({
|
|
1964
|
+
...currentResult,
|
|
1965
|
+
status: 'failed',
|
|
1966
|
+
error: `Quality gate rejected: ${issues}`,
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
attempts += 1;
|
|
1971
|
+
this.logger.log(` 🔍 Quality gate rejected output; refining (${attempts}/${maxAttempts})`);
|
|
1972
|
+
|
|
1973
|
+
const refinePrompt = this.buildQualityGateRefinePrompt(
|
|
1974
|
+
step,
|
|
1975
|
+
currentResult.output,
|
|
1976
|
+
review,
|
|
1977
|
+
context
|
|
1978
|
+
);
|
|
1979
|
+
const refinedStep: Step = {
|
|
1980
|
+
...step,
|
|
1981
|
+
prompt: refinePrompt,
|
|
1982
|
+
};
|
|
1983
|
+
const refinedContext = {
|
|
1984
|
+
...context,
|
|
1985
|
+
qualityGateAttempts: attempts,
|
|
1986
|
+
};
|
|
1987
|
+
|
|
1988
|
+
const refinedResult = await exec(refinedStep, refinedContext, this.logger, {
|
|
1989
|
+
executeWorkflowFn: this.executeSubWorkflow.bind(this),
|
|
1990
|
+
mcpManager: this.mcpManager,
|
|
1991
|
+
db: this.db,
|
|
1992
|
+
memoryDb: this.memoryDb,
|
|
1993
|
+
workflowDir: this.options.workflowDir,
|
|
1994
|
+
dryRun: this.options.dryRun,
|
|
1995
|
+
abortSignal,
|
|
1996
|
+
runId: this.runId,
|
|
1997
|
+
artifactRoot: this.options.artifactRoot,
|
|
1998
|
+
redactForStorage: this.secretManager.redactForStorage.bind(this.secretManager),
|
|
1999
|
+
getAdapter: this.options.getAdapter,
|
|
2000
|
+
executeStep: this.options.executeStep || executeStep,
|
|
2001
|
+
emitEvent: this.emitEvent.bind(this),
|
|
2002
|
+
workflowName: this.workflow.name,
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
if (refinedResult.status === 'failed') {
|
|
2006
|
+
throw new StepExecutionError(refinedResult);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
if (step.outputSchema) {
|
|
2010
|
+
this.validator.validateSchema('output', step.outputSchema, refinedResult.output, step.id);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
currentResult = refinedResult;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2029
2017
|
/**
|
|
2030
2018
|
* Execute a step (handles foreach if present)
|
|
2031
2019
|
*/
|
|
2032
2020
|
private async executeStepWithForeach(step: Step): Promise<void> {
|
|
2033
|
-
const baseContext = this.buildContext();
|
|
2021
|
+
const baseContext = this.contextBuilder.buildContext(this.secretManager.getSecrets());
|
|
2034
2022
|
|
|
2035
2023
|
if (this.shouldSkipStep(step, baseContext)) {
|
|
2036
2024
|
this.logger.log(` ⊘ Skipping step ${step.id} (condition not met)`);
|
|
2037
2025
|
const stepExecId = randomUUID();
|
|
2038
2026
|
await this.db.createStep(stepExecId, this.runId, step.id);
|
|
2039
2027
|
await this.db.completeStep(stepExecId, 'skipped', null);
|
|
2040
|
-
this.
|
|
2028
|
+
this.state.set(step.id, { status: 'skipped' });
|
|
2041
2029
|
return;
|
|
2042
2030
|
}
|
|
2043
2031
|
|
|
@@ -2046,12 +2034,11 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2046
2034
|
const stepExecId = randomUUID();
|
|
2047
2035
|
await this.db.createStep(stepExecId, this.runId, step.id);
|
|
2048
2036
|
await this.db.completeStep(stepExecId, StepStatus.SKIPPED, null);
|
|
2049
|
-
this.
|
|
2037
|
+
this.state.set(step.id, { status: StepStatus.SKIPPED });
|
|
2050
2038
|
return;
|
|
2051
2039
|
}
|
|
2052
2040
|
|
|
2053
2041
|
if (step.foreach) {
|
|
2054
|
-
const { ForeachExecutor } = await import('./foreach-executor.ts');
|
|
2055
2042
|
const executor = new ForeachExecutor(
|
|
2056
2043
|
this.db,
|
|
2057
2044
|
this.logger,
|
|
@@ -2060,10 +2047,10 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2060
2047
|
this.resourcePool
|
|
2061
2048
|
);
|
|
2062
2049
|
|
|
2063
|
-
const existingContext = this.
|
|
2050
|
+
const existingContext = this.state.get(step.id) as ForeachStepContext;
|
|
2064
2051
|
const result = await executor.execute(step, baseContext, this.runId, existingContext);
|
|
2065
2052
|
|
|
2066
|
-
this.
|
|
2053
|
+
this.state.set(step.id, result);
|
|
2067
2054
|
} else {
|
|
2068
2055
|
// Single execution
|
|
2069
2056
|
const stepExecId = randomUUID();
|
|
@@ -2072,7 +2059,7 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2072
2059
|
const result = await this.executeStepInternal(step, baseContext, stepExecId);
|
|
2073
2060
|
|
|
2074
2061
|
// Update global state
|
|
2075
|
-
this.
|
|
2062
|
+
this.state.set(step.id, result);
|
|
2076
2063
|
|
|
2077
2064
|
if (result.status === 'suspended') {
|
|
2078
2065
|
const inputType = step.type === 'human' ? step.inputType : 'text';
|
|
@@ -2096,99 +2083,127 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2096
2083
|
*/
|
|
2097
2084
|
private async executeSubWorkflow(
|
|
2098
2085
|
step: WorkflowStep,
|
|
2099
|
-
context: ExpressionContext
|
|
2086
|
+
context: ExpressionContext,
|
|
2087
|
+
abortSignal?: AbortSignal
|
|
2100
2088
|
): Promise<StepResult> {
|
|
2101
|
-
const
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
// Evaluate inputs for the sub-workflow
|
|
2106
|
-
const inputs: Record<string, unknown> = {};
|
|
2107
|
-
if (step.inputs) {
|
|
2108
|
-
for (const [key, value] of Object.entries(step.inputs)) {
|
|
2109
|
-
inputs[key] = ExpressionEvaluator.evaluate(value, context);
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2089
|
+
const factory: RunnerFactory = {
|
|
2090
|
+
create: (workflow, options) => new WorkflowRunner(workflow, options),
|
|
2091
|
+
};
|
|
2112
2092
|
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
dedup: this.options.dedup,
|
|
2123
|
-
artifactRoot: this.options.artifactRoot,
|
|
2093
|
+
return executeSubWorkflow(step, context, {
|
|
2094
|
+
runnerFactory: factory,
|
|
2095
|
+
parentWorkflowDir: this.options.workflowDir,
|
|
2096
|
+
parentDbPath: this.db.dbPath,
|
|
2097
|
+
parentLogger: this.logger,
|
|
2098
|
+
parentMcpManager: this.mcpManager,
|
|
2099
|
+
parentDepth: this.depth,
|
|
2100
|
+
parentOptions: this.options,
|
|
2101
|
+
abortSignal,
|
|
2124
2102
|
});
|
|
2103
|
+
}
|
|
2125
2104
|
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
// Handle explicit output mapping
|
|
2134
|
-
if (step.outputMapping) {
|
|
2135
|
-
for (const [alias, mapping] of Object.entries(step.outputMapping)) {
|
|
2136
|
-
let originalKey: string;
|
|
2137
|
-
let defaultValue: unknown;
|
|
2138
|
-
|
|
2139
|
-
if (typeof mapping === 'string') {
|
|
2140
|
-
originalKey = mapping;
|
|
2141
|
-
} else {
|
|
2142
|
-
originalKey = mapping.from;
|
|
2143
|
-
defaultValue = mapping.default;
|
|
2144
|
-
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Redact secrets from a value
|
|
2107
|
+
*/
|
|
2108
|
+
public redact<T>(value: T): T {
|
|
2109
|
+
return this.secretManager.redactValue(value) as T;
|
|
2110
|
+
}
|
|
2145
2111
|
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2112
|
+
private emitEvent(event: WorkflowEvent): void {
|
|
2113
|
+
try {
|
|
2114
|
+
const redactor = this.secretManager.getRedactor();
|
|
2115
|
+
const redacted = redactor.redactValue(event) as WorkflowEvent;
|
|
2116
|
+
if (redacted.type === 'llm.thought') {
|
|
2117
|
+
void this.db
|
|
2118
|
+
.storeThoughtEvent(
|
|
2119
|
+
redacted.runId,
|
|
2120
|
+
redacted.workflow,
|
|
2121
|
+
redacted.stepId,
|
|
2122
|
+
redacted.content,
|
|
2123
|
+
redacted.source
|
|
2124
|
+
)
|
|
2125
|
+
.catch((error) => {
|
|
2126
|
+
this.logger.warn(
|
|
2127
|
+
` ⚠️ Failed to store thought event: ${error instanceof Error ? error.message : String(error)}`
|
|
2153
2128
|
);
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
if (this.options.onEvent) {
|
|
2132
|
+
this.options.onEvent(redacted);
|
|
2156
2133
|
}
|
|
2157
|
-
|
|
2158
|
-
return {
|
|
2159
|
-
output: {
|
|
2160
|
-
...mappedOutputs,
|
|
2161
|
-
outputs: rawOutputs, // Namespaced raw outputs
|
|
2162
|
-
__subRunId: subRunner.runId, // Track sub-workflow run ID for rollback
|
|
2163
|
-
},
|
|
2164
|
-
status: 'success',
|
|
2165
|
-
};
|
|
2166
2134
|
} catch (error) {
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2171
|
-
};
|
|
2135
|
+
this.logger.warn(
|
|
2136
|
+
` ⚠️ Failed to emit event: ${error instanceof Error ? error.message : String(error)}`
|
|
2137
|
+
);
|
|
2172
2138
|
}
|
|
2173
2139
|
}
|
|
2174
2140
|
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2141
|
+
private emitStepStart(
|
|
2142
|
+
step: Step,
|
|
2143
|
+
phase: StepPhase,
|
|
2144
|
+
stepIndex?: number,
|
|
2145
|
+
totalSteps?: number
|
|
2146
|
+
): number {
|
|
2147
|
+
const startedAt = Date.now();
|
|
2148
|
+
this.emitEvent({
|
|
2149
|
+
type: 'step.start',
|
|
2150
|
+
timestamp: new Date(startedAt).toISOString(),
|
|
2151
|
+
runId: this.runId,
|
|
2152
|
+
workflow: this.workflow.name,
|
|
2153
|
+
stepId: step.id,
|
|
2154
|
+
stepType: step.type,
|
|
2155
|
+
phase,
|
|
2156
|
+
stepIndex,
|
|
2157
|
+
totalSteps,
|
|
2158
|
+
});
|
|
2159
|
+
return startedAt;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
private emitStepEnd(
|
|
2163
|
+
step: Step,
|
|
2164
|
+
phase: StepPhase,
|
|
2165
|
+
startedAt: number,
|
|
2166
|
+
error?: unknown,
|
|
2167
|
+
stepIndex?: number,
|
|
2168
|
+
totalSteps?: number
|
|
2169
|
+
): void {
|
|
2170
|
+
const endedAt = Date.now();
|
|
2171
|
+
const context = this.state.get(step.id);
|
|
2172
|
+
const status = context?.status || StepStatus.FAILED;
|
|
2173
|
+
const errorMsg =
|
|
2174
|
+
context?.error ||
|
|
2175
|
+
(error instanceof Error ? error.message : error ? String(error) : undefined);
|
|
2176
|
+
|
|
2177
|
+
this.emitEvent({
|
|
2178
|
+
type: 'step.end',
|
|
2179
|
+
timestamp: new Date(endedAt).toISOString(),
|
|
2180
|
+
runId: this.runId,
|
|
2181
|
+
workflow: this.workflow.name,
|
|
2182
|
+
stepId: step.id,
|
|
2183
|
+
stepType: step.type,
|
|
2184
|
+
phase,
|
|
2185
|
+
status,
|
|
2186
|
+
durationMs: endedAt - startedAt,
|
|
2187
|
+
error: status === StepStatus.SUCCESS || status === StepStatus.SKIPPED ? undefined : errorMsg,
|
|
2188
|
+
stepIndex,
|
|
2189
|
+
totalSteps,
|
|
2190
|
+
});
|
|
2180
2191
|
}
|
|
2181
2192
|
|
|
2182
2193
|
/**
|
|
2183
2194
|
* Execute the workflow
|
|
2184
2195
|
*/
|
|
2185
2196
|
async run(): Promise<Record<string, unknown>> {
|
|
2197
|
+
const expressionStrict = ConfigLoader.load().expression?.strict ?? false;
|
|
2198
|
+
ExpressionEvaluator.setStrictMode(expressionStrict);
|
|
2199
|
+
let completionEvent: WorkflowEvent | null = null;
|
|
2200
|
+
|
|
2186
2201
|
// Handle resume state restoration
|
|
2187
2202
|
if (this.resumeRunId && !this.restored) {
|
|
2188
2203
|
await this.restoreState();
|
|
2189
2204
|
}
|
|
2190
2205
|
|
|
2191
|
-
const isResume = !!this.resumeRunId || this.
|
|
2206
|
+
const isResume = !!this.resumeRunId || this.state.size > 0;
|
|
2192
2207
|
|
|
2193
2208
|
this.logger.log(`\n🏛️ ${isResume ? 'Resuming' : 'Running'} workflow: ${this.workflow.name}`);
|
|
2194
2209
|
this.logger.log(`Run ID: ${this.runId}`);
|
|
@@ -2197,56 +2212,80 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2197
2212
|
' Workflows can execute arbitrary shell commands and access your environment.\n'
|
|
2198
2213
|
);
|
|
2199
2214
|
|
|
2200
|
-
this.redactAtRest = ConfigLoader.load().storage?.redact_secrets_at_rest ?? true;
|
|
2215
|
+
this.secretManager.redactAtRest = ConfigLoader.load().storage?.redact_secrets_at_rest ?? true;
|
|
2201
2216
|
|
|
2202
2217
|
// Apply defaults and validate inputs
|
|
2203
|
-
this.applyDefaultsAndValidate();
|
|
2218
|
+
const validated = this.validator.applyDefaultsAndValidate();
|
|
2219
|
+
if (validated.secretValues.length > 0) {
|
|
2220
|
+
this.secretManager.setSecretValues(validated.secretValues);
|
|
2221
|
+
this.logger = new RedactingLogger(this.rawLogger, this.secretManager.getRedactor());
|
|
2222
|
+
this.contextBuilder = new ContextBuilder(
|
|
2223
|
+
this.workflow,
|
|
2224
|
+
this.inputs,
|
|
2225
|
+
this.secretManager.getSecretValues(),
|
|
2226
|
+
this.state,
|
|
2227
|
+
this.logger
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2204
2230
|
|
|
2205
2231
|
// Create run record (only for new runs, not for resume)
|
|
2206
2232
|
if (!isResume) {
|
|
2207
|
-
await this.db.createRun(
|
|
2233
|
+
await this.db.createRun(
|
|
2234
|
+
this.runId,
|
|
2235
|
+
this.workflow.name,
|
|
2236
|
+
this.secretManager.redactForStorage(this.inputs)
|
|
2237
|
+
);
|
|
2208
2238
|
}
|
|
2209
2239
|
await this.db.updateRunStatus(this.runId, 'running');
|
|
2240
|
+
this.emitEvent({
|
|
2241
|
+
type: 'workflow.start',
|
|
2242
|
+
timestamp: new Date().toISOString(),
|
|
2243
|
+
runId: this.runId,
|
|
2244
|
+
workflow: this.workflow.name,
|
|
2245
|
+
inputs: this.secretManager.redactValue(this.inputs),
|
|
2246
|
+
});
|
|
2210
2247
|
|
|
2211
2248
|
try {
|
|
2212
|
-
//
|
|
2213
|
-
const executionOrder =
|
|
2214
|
-
const stepMap = new Map(this.workflow.steps.map((s) => [s.id, s]));
|
|
2215
|
-
|
|
2216
|
-
// Initialize completedSteps with already completed steps (for resume)
|
|
2217
|
-
// Only include steps that were successful or skipped, so failed steps are retried
|
|
2218
|
-
const completedSteps = new Set<string>();
|
|
2219
|
-
for (const [id, ctx] of this.stepContexts.entries()) {
|
|
2220
|
-
if (ctx.status === 'success' || ctx.status === 'skipped') {
|
|
2221
|
-
completedSteps.add(id);
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
// Filter out already completed steps from execution order
|
|
2226
|
-
const remainingSteps = executionOrder.filter((stepId) => !completedSteps.has(stepId));
|
|
2249
|
+
// Use scheduler's execution order
|
|
2250
|
+
const executionOrder = this.scheduler.getExecutionOrder();
|
|
2227
2251
|
|
|
2228
|
-
if (isResume &&
|
|
2252
|
+
if (isResume && this.scheduler.isComplete()) {
|
|
2229
2253
|
this.logger.log('All steps already completed. Nothing to resume.\n');
|
|
2230
2254
|
// Evaluate outputs from completed state
|
|
2231
2255
|
const outputs = this.evaluateOutputs();
|
|
2232
|
-
await this.db.updateRunStatus(
|
|
2256
|
+
await this.db.updateRunStatus(
|
|
2257
|
+
this.runId,
|
|
2258
|
+
'success',
|
|
2259
|
+
this.secretManager.redactForStorage(outputs)
|
|
2260
|
+
);
|
|
2233
2261
|
this.logger.log('✨ Workflow already completed!\n');
|
|
2262
|
+
completionEvent = {
|
|
2263
|
+
type: 'workflow.complete',
|
|
2264
|
+
timestamp: new Date().toISOString(),
|
|
2265
|
+
runId: this.runId,
|
|
2266
|
+
workflow: this.workflow.name,
|
|
2267
|
+
status: WorkflowStatus.SUCCESS,
|
|
2268
|
+
outputs: this.secretManager.redactValue(outputs),
|
|
2269
|
+
};
|
|
2234
2270
|
return outputs;
|
|
2235
2271
|
}
|
|
2236
2272
|
|
|
2237
|
-
|
|
2238
|
-
|
|
2273
|
+
const pendingCount = this.scheduler.getPendingCount();
|
|
2274
|
+
const totalSteps = executionOrder.length;
|
|
2275
|
+
const completedCount = totalSteps - pendingCount;
|
|
2276
|
+
|
|
2277
|
+
if (isResume && completedCount > 0) {
|
|
2278
|
+
this.logger.log(`Skipping ${completedCount} already completed step(s)\n`);
|
|
2239
2279
|
}
|
|
2240
2280
|
|
|
2241
2281
|
this.logger.log(`Execution order: ${executionOrder.join(' → ')}\n`);
|
|
2242
2282
|
|
|
2243
|
-
const totalSteps = executionOrder.length;
|
|
2244
2283
|
const stepIndices = new Map(executionOrder.map((id, index) => [id, index + 1]));
|
|
2245
2284
|
|
|
2246
2285
|
// Evaluate global concurrency limit
|
|
2247
|
-
let globalConcurrencyLimit =
|
|
2286
|
+
let globalConcurrencyLimit = pendingCount || 10;
|
|
2248
2287
|
if (this.workflow.concurrency !== undefined) {
|
|
2249
|
-
const baseContext = this.buildContext();
|
|
2288
|
+
const baseContext = this.contextBuilder.buildContext(this.secretManager.getSecrets());
|
|
2250
2289
|
if (typeof this.workflow.concurrency === 'string') {
|
|
2251
2290
|
globalConcurrencyLimit = Number(
|
|
2252
2291
|
ExpressionEvaluator.evaluate(this.workflow.concurrency, baseContext)
|
|
@@ -2267,11 +2306,10 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2267
2306
|
}
|
|
2268
2307
|
|
|
2269
2308
|
// Execute steps in parallel where possible (respecting dependencies and global concurrency)
|
|
2270
|
-
const pendingSteps = new Set(remainingSteps);
|
|
2271
2309
|
const runningPromises = new Map<string, Promise<void>>();
|
|
2272
2310
|
|
|
2273
2311
|
try {
|
|
2274
|
-
while (
|
|
2312
|
+
while (!this.scheduler.isComplete() || runningPromises.size > 0) {
|
|
2275
2313
|
// Check for cancellation - drain in-flight steps but don't start new ones
|
|
2276
2314
|
if (this.isCanceled) {
|
|
2277
2315
|
if (runningPromises.size > 0) {
|
|
@@ -2283,73 +2321,71 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2283
2321
|
throw new Error('Workflow canceled by user');
|
|
2284
2322
|
}
|
|
2285
2323
|
|
|
2286
|
-
// 1. Find runnable steps
|
|
2287
|
-
|
|
2324
|
+
// 1. Find runnable steps from scheduler
|
|
2325
|
+
const runnableSteps = this.scheduler
|
|
2326
|
+
.getRunnableSteps(runningPromises.size, globalConcurrencyLimit)
|
|
2327
|
+
.filter((step) => this.resourcePool.hasCapacity(step.pool || step.type));
|
|
2328
|
+
|
|
2329
|
+
for (const step of runnableSteps) {
|
|
2288
2330
|
// Don't schedule new steps if canceled
|
|
2289
2331
|
if (this.isCanceled) break;
|
|
2290
2332
|
|
|
2291
|
-
const
|
|
2292
|
-
|
|
2293
|
-
throw new Error(`Step ${stepId} not found in workflow`);
|
|
2294
|
-
}
|
|
2333
|
+
const stepId = step.id;
|
|
2334
|
+
this.scheduler.startStep(stepId);
|
|
2295
2335
|
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
dependenciesMet = this.isJoinConditionMet(
|
|
2299
|
-
step as import('../parser/schema.ts').JoinStep,
|
|
2300
|
-
completedSteps
|
|
2301
|
-
);
|
|
2302
|
-
} else {
|
|
2303
|
-
dependenciesMet = step.needs.every((dep: string) => completedSteps.has(dep));
|
|
2304
|
-
}
|
|
2336
|
+
// Determine pool for this step
|
|
2337
|
+
const poolName = step.pool || step.type;
|
|
2305
2338
|
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
release();
|
|
2333
|
-
}
|
|
2334
|
-
runningPromises.delete(stepId);
|
|
2339
|
+
// Start execution
|
|
2340
|
+
const stepIndex = stepIndices.get(stepId);
|
|
2341
|
+
|
|
2342
|
+
const promise = (async () => {
|
|
2343
|
+
let release: (() => void) | undefined;
|
|
2344
|
+
const startedAt = this.emitStepStart(step, 'main', stepIndex, totalSteps);
|
|
2345
|
+
try {
|
|
2346
|
+
this.logger.debug?.(
|
|
2347
|
+
`[${stepIndex}/${totalSteps}] ⏳ Waiting for pool: ${poolName}`
|
|
2348
|
+
);
|
|
2349
|
+
release = await this.resourcePool.acquire(poolName, { signal: this.abortSignal });
|
|
2350
|
+
|
|
2351
|
+
this.logger.log(
|
|
2352
|
+
`[${stepIndex}/${totalSteps}] ▶ Executing step: ${step.id} (${step.type})`
|
|
2353
|
+
);
|
|
2354
|
+
|
|
2355
|
+
await this.executeStepWithForeach(step);
|
|
2356
|
+
this.emitStepEnd(step, 'main', startedAt, undefined, stepIndex, totalSteps);
|
|
2357
|
+
this.scheduler.markStepComplete(stepId);
|
|
2358
|
+
this.logger.log(`[${stepIndex}/${totalSteps}] ✓ Step ${step.id} completed\n`);
|
|
2359
|
+
} catch (error) {
|
|
2360
|
+
this.emitStepEnd(step, 'main', startedAt, error, stepIndex, totalSteps);
|
|
2361
|
+
throw error;
|
|
2362
|
+
} finally {
|
|
2363
|
+
if (typeof release === 'function') {
|
|
2364
|
+
release();
|
|
2335
2365
|
}
|
|
2336
|
-
|
|
2366
|
+
runningPromises.delete(stepId);
|
|
2367
|
+
}
|
|
2368
|
+
})();
|
|
2337
2369
|
|
|
2338
|
-
|
|
2339
|
-
}
|
|
2370
|
+
runningPromises.set(stepId, promise);
|
|
2340
2371
|
}
|
|
2341
2372
|
|
|
2342
2373
|
// 2. Detect deadlock (only if not canceled)
|
|
2343
|
-
if (!this.isCanceled && runningPromises.size === 0 &&
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2374
|
+
if (!this.isCanceled && runningPromises.size === 0 && !this.scheduler.isComplete()) {
|
|
2375
|
+
// Check if there are ANY steps whose dependencies are met, even if they're blocked by capacity/concurrency
|
|
2376
|
+
const readySteps = this.scheduler.getRunnableSteps(0, Number.MAX_SAFE_INTEGER);
|
|
2377
|
+
if (readySteps.length === 0) {
|
|
2378
|
+
throw new Error(
|
|
2379
|
+
'Deadlock detected in workflow execution. Steps remaining but none runnable (dependency cycles or missing inputs).'
|
|
2380
|
+
);
|
|
2381
|
+
}
|
|
2348
2382
|
}
|
|
2349
2383
|
|
|
2350
2384
|
// 3. Wait for at least one step to finish before checking again
|
|
2351
2385
|
if (runningPromises.size > 0) {
|
|
2352
2386
|
await Promise.race(runningPromises.values());
|
|
2387
|
+
// Yield to event loop to prevent tight loop if multiple steps finish in same tick
|
|
2388
|
+
await Bun.sleep(0);
|
|
2353
2389
|
}
|
|
2354
2390
|
}
|
|
2355
2391
|
} catch (error) {
|
|
@@ -2369,37 +2405,61 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2369
2405
|
throw error;
|
|
2370
2406
|
}
|
|
2371
2407
|
|
|
2372
|
-
// Determine final status
|
|
2373
|
-
const failedSteps = remainingSteps.filter(
|
|
2374
|
-
(id) => this.stepContexts.get(id)?.status === StepStatus.FAILED
|
|
2375
|
-
);
|
|
2376
|
-
|
|
2377
2408
|
// Evaluate outputs
|
|
2378
2409
|
const outputs = this.evaluateOutputs();
|
|
2379
2410
|
|
|
2380
2411
|
// Mark run as complete
|
|
2381
|
-
await this.db.updateRunStatus(
|
|
2412
|
+
await this.db.updateRunStatus(
|
|
2413
|
+
this.runId,
|
|
2414
|
+
'success',
|
|
2415
|
+
this.secretManager.redactForStorage(outputs)
|
|
2416
|
+
);
|
|
2382
2417
|
|
|
2383
2418
|
this.logger.log('✨ Workflow completed successfully!\n');
|
|
2384
2419
|
|
|
2420
|
+
completionEvent = {
|
|
2421
|
+
type: 'workflow.complete',
|
|
2422
|
+
timestamp: new Date().toISOString(),
|
|
2423
|
+
runId: this.runId,
|
|
2424
|
+
workflow: this.workflow.name,
|
|
2425
|
+
status: WorkflowStatus.SUCCESS,
|
|
2426
|
+
outputs: this.secretManager.redactValue(outputs),
|
|
2427
|
+
};
|
|
2428
|
+
|
|
2385
2429
|
return outputs;
|
|
2386
2430
|
} catch (error) {
|
|
2387
2431
|
if (error instanceof WorkflowSuspendedError) {
|
|
2388
2432
|
await this.db.updateRunStatus(this.runId, 'paused');
|
|
2389
2433
|
this.logger.log(`\n⏸ Workflow paused: ${error.message}`);
|
|
2434
|
+
completionEvent = {
|
|
2435
|
+
type: 'workflow.complete',
|
|
2436
|
+
timestamp: new Date().toISOString(),
|
|
2437
|
+
runId: this.runId,
|
|
2438
|
+
workflow: this.workflow.name,
|
|
2439
|
+
status: WorkflowStatus.PAUSED,
|
|
2440
|
+
error: error.message,
|
|
2441
|
+
};
|
|
2390
2442
|
throw error;
|
|
2391
2443
|
}
|
|
2392
2444
|
|
|
2393
2445
|
if (error instanceof WorkflowWaitingError) {
|
|
2394
2446
|
await this.db.updateRunStatus(this.runId, 'paused');
|
|
2395
2447
|
this.logger.log(`\n⏳ Workflow waiting: ${error.message}`);
|
|
2448
|
+
completionEvent = {
|
|
2449
|
+
type: 'workflow.complete',
|
|
2450
|
+
timestamp: new Date().toISOString(),
|
|
2451
|
+
runId: this.runId,
|
|
2452
|
+
workflow: this.workflow.name,
|
|
2453
|
+
status: WorkflowStatus.PAUSED,
|
|
2454
|
+
error: error.message,
|
|
2455
|
+
};
|
|
2396
2456
|
throw error;
|
|
2397
2457
|
}
|
|
2398
2458
|
|
|
2399
2459
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2400
2460
|
|
|
2401
2461
|
// Find the failed step from stepContexts
|
|
2402
|
-
for (const [stepId, ctx] of this.
|
|
2462
|
+
for (const [stepId, ctx] of this.state.entries()) {
|
|
2403
2463
|
if (ctx.status === 'failed') {
|
|
2404
2464
|
this.lastFailedStep = { id: stepId, error: ctx.error || errorMsg };
|
|
2405
2465
|
break;
|
|
@@ -2414,12 +2474,23 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2414
2474
|
this.runId,
|
|
2415
2475
|
'failed',
|
|
2416
2476
|
undefined,
|
|
2417
|
-
this.redactAtRest ? this.
|
|
2477
|
+
this.secretManager.redactAtRest ? this.secretManager.redact(errorMsg) : errorMsg
|
|
2418
2478
|
);
|
|
2479
|
+
completionEvent = {
|
|
2480
|
+
type: 'workflow.complete',
|
|
2481
|
+
timestamp: new Date().toISOString(),
|
|
2482
|
+
runId: this.runId,
|
|
2483
|
+
workflow: this.workflow.name,
|
|
2484
|
+
status: WorkflowStatus.FAILED,
|
|
2485
|
+
error: errorMsg,
|
|
2486
|
+
};
|
|
2419
2487
|
throw error;
|
|
2420
2488
|
} finally {
|
|
2421
2489
|
this.removeSignalHandlers();
|
|
2422
2490
|
await this.runFinally();
|
|
2491
|
+
if (completionEvent) {
|
|
2492
|
+
this.emitEvent(completionEvent);
|
|
2493
|
+
}
|
|
2423
2494
|
if (!this.options.mcpManager) {
|
|
2424
2495
|
await this.mcpManager.stopAll();
|
|
2425
2496
|
}
|
|
@@ -2450,9 +2521,9 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2450
2521
|
const step = stepMap.get(stepId);
|
|
2451
2522
|
if (!step) continue;
|
|
2452
2523
|
|
|
2453
|
-
// Dependencies can be from main steps (already in this.
|
|
2524
|
+
// Dependencies can be from main steps (already in this.state) or previous finally steps
|
|
2454
2525
|
const dependenciesMet = step.needs.every(
|
|
2455
|
-
(dep: string) => this.
|
|
2526
|
+
(dep: string) => this.state.has(dep) || completedFinallySteps.has(dep)
|
|
2456
2527
|
);
|
|
2457
2528
|
|
|
2458
2529
|
if (dependenciesMet) {
|
|
@@ -2462,8 +2533,22 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2462
2533
|
this.logger.log(
|
|
2463
2534
|
`[${finallyStepIndex}/${totalFinallySteps}] ▶ Executing finally step: ${step.id} (${step.type})`
|
|
2464
2535
|
);
|
|
2536
|
+
const startedAt = this.emitStepStart(
|
|
2537
|
+
step,
|
|
2538
|
+
'finally',
|
|
2539
|
+
finallyStepIndex,
|
|
2540
|
+
totalFinallySteps
|
|
2541
|
+
);
|
|
2465
2542
|
const promise = this.executeStepWithForeach(step)
|
|
2466
2543
|
.then(() => {
|
|
2544
|
+
this.emitStepEnd(
|
|
2545
|
+
step,
|
|
2546
|
+
'finally',
|
|
2547
|
+
startedAt,
|
|
2548
|
+
undefined,
|
|
2549
|
+
finallyStepIndex,
|
|
2550
|
+
totalFinallySteps
|
|
2551
|
+
);
|
|
2467
2552
|
completedFinallySteps.add(stepId);
|
|
2468
2553
|
runningPromises.delete(stepId);
|
|
2469
2554
|
this.logger.log(
|
|
@@ -2471,6 +2556,14 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2471
2556
|
);
|
|
2472
2557
|
})
|
|
2473
2558
|
.catch((err) => {
|
|
2559
|
+
this.emitStepEnd(
|
|
2560
|
+
step,
|
|
2561
|
+
'finally',
|
|
2562
|
+
startedAt,
|
|
2563
|
+
err,
|
|
2564
|
+
finallyStepIndex,
|
|
2565
|
+
totalFinallySteps
|
|
2566
|
+
);
|
|
2474
2567
|
runningPromises.delete(stepId);
|
|
2475
2568
|
this.logger.error(
|
|
2476
2569
|
` ✗ Finally step ${step.id} failed: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2490,6 +2583,7 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2490
2583
|
|
|
2491
2584
|
if (runningPromises.size > 0) {
|
|
2492
2585
|
await Promise.race(runningPromises.values());
|
|
2586
|
+
await Bun.sleep(0);
|
|
2493
2587
|
}
|
|
2494
2588
|
}
|
|
2495
2589
|
} catch (error) {
|
|
@@ -2531,9 +2625,9 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2531
2625
|
const step = stepMap.get(stepId);
|
|
2532
2626
|
if (!step) continue;
|
|
2533
2627
|
|
|
2534
|
-
// Dependencies can be from main steps (already in this.
|
|
2628
|
+
// Dependencies can be from main steps (already in this.state) or previous errors steps
|
|
2535
2629
|
const dependenciesMet = step.needs.every(
|
|
2536
|
-
(dep: string) => this.
|
|
2630
|
+
(dep: string) => this.state.has(dep) || completedErrorsSteps.has(dep)
|
|
2537
2631
|
);
|
|
2538
2632
|
|
|
2539
2633
|
if (dependenciesMet) {
|
|
@@ -2543,8 +2637,17 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2543
2637
|
this.logger.log(
|
|
2544
2638
|
`[${errorsStepIndex}/${totalErrorsSteps}] ▶ Executing errors step: ${step.id} (${step.type})`
|
|
2545
2639
|
);
|
|
2640
|
+
const startedAt = this.emitStepStart(step, 'errors', errorsStepIndex, totalErrorsSteps);
|
|
2546
2641
|
const promise = this.executeStepWithForeach(step)
|
|
2547
2642
|
.then(() => {
|
|
2643
|
+
this.emitStepEnd(
|
|
2644
|
+
step,
|
|
2645
|
+
'errors',
|
|
2646
|
+
startedAt,
|
|
2647
|
+
undefined,
|
|
2648
|
+
errorsStepIndex,
|
|
2649
|
+
totalErrorsSteps
|
|
2650
|
+
);
|
|
2548
2651
|
completedErrorsSteps.add(stepId);
|
|
2549
2652
|
runningPromises.delete(stepId);
|
|
2550
2653
|
this.logger.log(
|
|
@@ -2552,6 +2655,7 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2552
2655
|
);
|
|
2553
2656
|
})
|
|
2554
2657
|
.catch((err) => {
|
|
2658
|
+
this.emitStepEnd(step, 'errors', startedAt, err, errorsStepIndex, totalErrorsSteps);
|
|
2555
2659
|
runningPromises.delete(stepId);
|
|
2556
2660
|
this.logger.error(
|
|
2557
2661
|
` ✗ Errors step ${step.id} failed: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2571,6 +2675,7 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2571
2675
|
|
|
2572
2676
|
if (runningPromises.size > 0) {
|
|
2573
2677
|
await Promise.race(runningPromises.values());
|
|
2678
|
+
await Bun.sleep(0);
|
|
2574
2679
|
}
|
|
2575
2680
|
}
|
|
2576
2681
|
} catch (error) {
|
|
@@ -2588,7 +2693,7 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2588
2693
|
* Evaluate workflow outputs
|
|
2589
2694
|
*/
|
|
2590
2695
|
private evaluateOutputs(): Record<string, unknown> {
|
|
2591
|
-
const context = this.buildContext();
|
|
2696
|
+
const context = this.contextBuilder.buildContext(this.secretManager.getSecrets());
|
|
2592
2697
|
const outputs: Record<string, unknown> = {};
|
|
2593
2698
|
|
|
2594
2699
|
if (this.workflow.outputs) {
|
|
@@ -2607,7 +2712,7 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2607
2712
|
// Validate outputs against schema if provided
|
|
2608
2713
|
if (this.workflow.outputSchema) {
|
|
2609
2714
|
try {
|
|
2610
|
-
this.validateSchema('output', this.workflow.outputSchema, outputs, 'workflow');
|
|
2715
|
+
this.validator.validateSchema('output', this.workflow.outputSchema, outputs, 'workflow');
|
|
2611
2716
|
} catch (error) {
|
|
2612
2717
|
throw new Error(
|
|
2613
2718
|
`Workflow output validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -2618,39 +2723,6 @@ Please provide a corrected response that exactly matches the required schema.`;
|
|
|
2618
2723
|
return outputs;
|
|
2619
2724
|
}
|
|
2620
2725
|
|
|
2621
|
-
/**
|
|
2622
|
-
* Check if a join condition is met based on completed dependencies
|
|
2623
|
-
*/
|
|
2624
|
-
private isJoinConditionMet(
|
|
2625
|
-
step: import('../parser/schema.ts').JoinStep,
|
|
2626
|
-
completedSteps: Set<string>
|
|
2627
|
-
): boolean {
|
|
2628
|
-
const total = step.needs.length;
|
|
2629
|
-
if (total === 0) return true;
|
|
2630
|
-
|
|
2631
|
-
// Count successful/skipped dependencies
|
|
2632
|
-
const successCount = step.needs.filter((dep) => completedSteps.has(dep)).length;
|
|
2633
|
-
|
|
2634
|
-
// Find failed/suspended dependencies (that we've already tried)
|
|
2635
|
-
// If some dependencies failed (and didn't allowFailure), the whole workflow would usually fail.
|
|
2636
|
-
// If allowFailure was true, they are in completedSteps.
|
|
2637
|
-
// So completedSteps effectively represents "done successfully".
|
|
2638
|
-
|
|
2639
|
-
if (step.condition === 'all') {
|
|
2640
|
-
return successCount === total;
|
|
2641
|
-
}
|
|
2642
|
-
if (step.condition === 'any') {
|
|
2643
|
-
// Met if at least one succeeded, OR if all finished and none succeeded?
|
|
2644
|
-
// Actually strictly "any" means at least one success.
|
|
2645
|
-
return successCount > 0;
|
|
2646
|
-
}
|
|
2647
|
-
if (typeof step.condition === 'number') {
|
|
2648
|
-
return successCount >= step.condition;
|
|
2649
|
-
}
|
|
2650
|
-
|
|
2651
|
-
return successCount === total;
|
|
2652
|
-
}
|
|
2653
|
-
|
|
2654
2726
|
/**
|
|
2655
2727
|
* Register top-level compensation for the workflow
|
|
2656
2728
|
*/
|