keystone-cli 2.0.0 → 2.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 +43 -4
- package/package.json +4 -1
- package/src/cli.ts +1 -0
- package/src/commands/event.ts +9 -0
- package/src/commands/run.ts +17 -0
- package/src/db/dynamic-state-manager.ts +12 -9
- package/src/db/memory-db.test.ts +19 -1
- package/src/db/memory-db.ts +101 -22
- package/src/db/workflow-db.ts +181 -9
- package/src/expression/evaluator.ts +4 -1
- package/src/parser/config-schema.ts +6 -0
- package/src/parser/schema.ts +1 -0
- package/src/runner/__test__/llm-test-setup.ts +43 -11
- package/src/runner/durable-timers.test.ts +1 -1
- package/src/runner/executors/dynamic-executor.ts +125 -88
- package/src/runner/executors/engine-executor.ts +10 -39
- package/src/runner/executors/file-executor.ts +67 -0
- package/src/runner/executors/foreach-executor.ts +170 -17
- package/src/runner/executors/human-executor.ts +18 -0
- package/src/runner/executors/llm/stream-handler.ts +103 -0
- package/src/runner/executors/llm/tool-manager.ts +360 -0
- package/src/runner/executors/llm-executor.ts +288 -555
- package/src/runner/executors/memory-executor.ts +41 -34
- package/src/runner/executors/shell-executor.ts +96 -52
- package/src/runner/executors/subworkflow-executor.ts +16 -0
- package/src/runner/executors/types.ts +3 -1
- package/src/runner/executors/verification_fixes.test.ts +46 -0
- package/src/runner/join-scheduling.test.ts +2 -1
- package/src/runner/llm-adapter.integration.test.ts +10 -5
- package/src/runner/llm-adapter.ts +57 -18
- package/src/runner/llm-clarification.test.ts +4 -1
- package/src/runner/llm-executor.test.ts +21 -7
- package/src/runner/mcp-client.ts +36 -2
- package/src/runner/mcp-server.ts +65 -36
- package/src/runner/recovery-security.test.ts +5 -2
- package/src/runner/reflexion.test.ts +6 -3
- package/src/runner/services/context-builder.ts +13 -4
- package/src/runner/services/workflow-validator.ts +2 -1
- package/src/runner/standard-tools-ast.test.ts +4 -2
- package/src/runner/standard-tools-execution.test.ts +14 -1
- package/src/runner/standard-tools-integration.test.ts +6 -0
- package/src/runner/standard-tools.ts +13 -10
- package/src/runner/step-executor.ts +2 -2
- package/src/runner/tool-integration.test.ts +4 -1
- package/src/runner/workflow-runner.test.ts +23 -12
- package/src/runner/workflow-runner.ts +172 -79
- package/src/runner/workflow-state.ts +181 -111
- package/src/ui/dashboard.tsx +17 -3
- package/src/utils/config-loader.ts +4 -0
- package/src/utils/constants.ts +4 -0
- package/src/utils/context-injector.test.ts +27 -27
- package/src/utils/context-injector.ts +68 -26
- package/src/utils/process-sandbox.ts +138 -148
- package/src/utils/redactor.ts +39 -9
- package/src/utils/resource-loader.ts +24 -19
- package/src/utils/sandbox.ts +6 -0
- package/src/utils/stream-utils.ts +58 -0
|
@@ -275,7 +275,7 @@ describe('WorkflowRunner', () => {
|
|
|
275
275
|
{
|
|
276
276
|
id: 's1',
|
|
277
277
|
type: 'shell',
|
|
278
|
-
run: 'echo
|
|
278
|
+
run: 'echo hello',
|
|
279
279
|
needs: [],
|
|
280
280
|
outputSchema: {
|
|
281
281
|
type: 'object',
|
|
@@ -577,7 +577,7 @@ describe('WorkflowRunner', () => {
|
|
|
577
577
|
const cached = await db.getStepCache(cacheKey);
|
|
578
578
|
expect(cached).not.toBeNull();
|
|
579
579
|
expect(cached?.output).not.toContain(secret);
|
|
580
|
-
expect(JSON.parse(cached?.output).stdout).toContain('***REDACTED***');
|
|
580
|
+
expect(JSON.parse(cached?.output || '{}').stdout).toContain('***REDACTED***');
|
|
581
581
|
db.close();
|
|
582
582
|
|
|
583
583
|
if (existsSync(memoizeDbPath)) rmSync(memoizeDbPath);
|
|
@@ -657,7 +657,7 @@ describe('WorkflowRunner', () => {
|
|
|
657
657
|
const workflow: Workflow = {
|
|
658
658
|
name: 'resume-wf',
|
|
659
659
|
steps: [
|
|
660
|
-
{ id: 's1', type: 'shell', run: 'echo
|
|
660
|
+
{ id: 's1', type: 'shell', run: 'echo one', needs: [] },
|
|
661
661
|
{ id: 's2', type: 'shell', run: 'exit 1', needs: ['s1'] },
|
|
662
662
|
],
|
|
663
663
|
} as unknown as Workflow;
|
|
@@ -677,8 +677,8 @@ describe('WorkflowRunner', () => {
|
|
|
677
677
|
const fixedWorkflow: Workflow = {
|
|
678
678
|
name: 'resume-wf',
|
|
679
679
|
steps: [
|
|
680
|
-
{ id: 's1', type: 'shell', run: 'echo
|
|
681
|
-
{ id: 's2', type: 'shell', run: 'echo
|
|
680
|
+
{ id: 's1', type: 'shell', run: 'echo one', needs: [] },
|
|
681
|
+
{ id: 's2', type: 'shell', run: 'echo two', needs: ['s1'] },
|
|
682
682
|
],
|
|
683
683
|
outputs: {
|
|
684
684
|
out: '${{ steps.s1.output.stdout.trim() }}-${{ steps.s2.output.stdout.trim() }}',
|
|
@@ -756,7 +756,15 @@ describe('WorkflowRunner', () => {
|
|
|
756
756
|
it('should redact secrets from outputs', async () => {
|
|
757
757
|
const workflow: Workflow = {
|
|
758
758
|
name: 'redaction-wf',
|
|
759
|
-
steps: [
|
|
759
|
+
steps: [
|
|
760
|
+
{
|
|
761
|
+
id: 's1',
|
|
762
|
+
type: 'shell',
|
|
763
|
+
run: 'echo "Secret is my-super-secret"',
|
|
764
|
+
allowInsecure: true,
|
|
765
|
+
needs: [],
|
|
766
|
+
},
|
|
767
|
+
],
|
|
760
768
|
outputs: {
|
|
761
769
|
out: '${{ steps.s1.output.stdout.trim() }}',
|
|
762
770
|
},
|
|
@@ -779,7 +787,7 @@ describe('WorkflowRunner', () => {
|
|
|
779
787
|
inputs: {
|
|
780
788
|
token: { type: 'string', secret: true },
|
|
781
789
|
},
|
|
782
|
-
steps: [{ id: 's1', type: 'shell', run: 'echo
|
|
790
|
+
steps: [{ id: 's1', type: 'shell', run: 'echo ok', needs: [] }],
|
|
783
791
|
} as unknown as Workflow;
|
|
784
792
|
|
|
785
793
|
ConfigLoader.setConfig({
|
|
@@ -793,6 +801,8 @@ describe('WorkflowRunner', () => {
|
|
|
793
801
|
engines: { allowlist: {}, denylist: [] },
|
|
794
802
|
concurrency: { default: 10, pools: { llm: 2, shell: 5, http: 10, engine: 2 } },
|
|
795
803
|
expression: { strict: false },
|
|
804
|
+
embedding_dimension: 1536,
|
|
805
|
+
logging: { suppress_security_warning: false, suppress_ai_sdk_warnings: false },
|
|
796
806
|
});
|
|
797
807
|
|
|
798
808
|
const runner = new WorkflowRunner(workflow, {
|
|
@@ -881,7 +891,8 @@ describe('WorkflowRunner', () => {
|
|
|
881
891
|
});
|
|
882
892
|
|
|
883
893
|
it('should handle foreach suspension and resume correctly', async () => {
|
|
884
|
-
const resumeDbPath =
|
|
894
|
+
const resumeDbPath = `test-foreach-resume-${randomUUID()}.db`;
|
|
895
|
+
if (existsSync(resumeDbPath)) rmSync(resumeDbPath);
|
|
885
896
|
if (existsSync(resumeDbPath)) rmSync(resumeDbPath);
|
|
886
897
|
|
|
887
898
|
const workflow: Workflow = {
|
|
@@ -965,7 +976,7 @@ describe('WorkflowRunner', () => {
|
|
|
965
976
|
});
|
|
966
977
|
|
|
967
978
|
it('should reuse persisted foreach items on resume even if inputs change', async () => {
|
|
968
|
-
const resumeDbPath =
|
|
979
|
+
const resumeDbPath = `test-foreach-resume-items-${randomUUID()}.db`;
|
|
969
980
|
if (existsSync(resumeDbPath)) rmSync(resumeDbPath);
|
|
970
981
|
|
|
971
982
|
const workflow: Workflow = {
|
|
@@ -1023,7 +1034,7 @@ describe('WorkflowRunner', () => {
|
|
|
1023
1034
|
});
|
|
1024
1035
|
|
|
1025
1036
|
it('should resume a workflow marked as running (crashed process)', async () => {
|
|
1026
|
-
const resumeDbPath =
|
|
1037
|
+
const resumeDbPath = `test-running-resume-${randomUUID()}.db`;
|
|
1027
1038
|
if (existsSync(resumeDbPath)) rmSync(resumeDbPath);
|
|
1028
1039
|
|
|
1029
1040
|
const workflow: Workflow = {
|
|
@@ -1105,7 +1116,7 @@ describe('WorkflowRunner', () => {
|
|
|
1105
1116
|
});
|
|
1106
1117
|
|
|
1107
1118
|
it('should allow cancellation via abortSignal', async () => {
|
|
1108
|
-
const cancelDbPath =
|
|
1119
|
+
const cancelDbPath = `test-cancel-${randomUUID()}.db`;
|
|
1109
1120
|
if (existsSync(cancelDbPath)) rmSync(cancelDbPath);
|
|
1110
1121
|
|
|
1111
1122
|
const workflow: Workflow = {
|
|
@@ -1130,7 +1141,7 @@ describe('WorkflowRunner', () => {
|
|
|
1130
1141
|
});
|
|
1131
1142
|
|
|
1132
1143
|
it('should resume from canceled state', async () => {
|
|
1133
|
-
const resumeDbPath =
|
|
1144
|
+
const resumeDbPath = `test-cancel-resume-${randomUUID()}.db`;
|
|
1134
1145
|
if (existsSync(resumeDbPath)) rmSync(resumeDbPath);
|
|
1135
1146
|
|
|
1136
1147
|
const workflow: Workflow = {
|
|
@@ -14,7 +14,7 @@ import { ConfigLoader } from '../utils/config-loader.ts';
|
|
|
14
14
|
import { container } from '../utils/container.ts';
|
|
15
15
|
import { extractJson } from '../utils/json-parser.ts';
|
|
16
16
|
import { ConsoleLogger, type Logger } from '../utils/logger.ts';
|
|
17
|
-
import
|
|
17
|
+
import { Redactor } from '../utils/redactor.ts';
|
|
18
18
|
import { formatSchemaErrors, validateJsonSchema } from '../utils/schema-validator.ts';
|
|
19
19
|
import { WorkflowRegistry } from '../utils/workflow-registry.ts';
|
|
20
20
|
import type { EventHandler, StepPhase, WorkflowEvent } from './events.ts';
|
|
@@ -71,7 +71,7 @@ class RedactingLogger implements Logger {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
class StepExecutionError extends Error {
|
|
74
|
+
export class StepExecutionError extends Error {
|
|
75
75
|
constructor(public readonly result: StepResult) {
|
|
76
76
|
super(result.error || 'Step failed');
|
|
77
77
|
this.name = 'StepExecutionError';
|
|
@@ -160,9 +160,10 @@ export class WorkflowRunner {
|
|
|
160
160
|
private hasWarnedMemory = false;
|
|
161
161
|
private static readonly MEMORY_WARNING_THRESHOLD = 1000;
|
|
162
162
|
private static readonly MAX_RECURSION_DEPTH = 10;
|
|
163
|
-
private static readonly REDACTED_PLACEHOLDER =
|
|
163
|
+
private static readonly REDACTED_PLACEHOLDER = Redactor.REDACTED_PLACEHOLDER;
|
|
164
164
|
private depth = 0;
|
|
165
165
|
private lastFailedStep?: { id: string; error: string };
|
|
166
|
+
private ownsDb = false;
|
|
166
167
|
private abortController = new AbortController();
|
|
167
168
|
private resourcePool!: ResourcePoolManager;
|
|
168
169
|
private restored = false;
|
|
@@ -217,22 +218,38 @@ export class WorkflowRunner {
|
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
// Use injected instances or resolve from container or create new from paths
|
|
220
|
-
|
|
221
|
-
options.db
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
221
|
+
if (options.db) {
|
|
222
|
+
this.db = options.db;
|
|
223
|
+
this.ownsDb = false;
|
|
224
|
+
} else {
|
|
225
|
+
const fromContainer = container.resolveOptional<WorkflowDb>('db');
|
|
226
|
+
if (fromContainer && !options.dbPath) {
|
|
227
|
+
this.db = fromContainer;
|
|
228
|
+
this.ownsDb = false;
|
|
229
|
+
} else {
|
|
230
|
+
this.db = new WorkflowDb(options.dbPath);
|
|
231
|
+
this.ownsDb = true;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
233
234
|
|
|
234
235
|
this.secretManager = new SecretManager(options.secrets || {});
|
|
235
236
|
this.initLogger(options);
|
|
237
|
+
|
|
238
|
+
// Initialize MemoryDB with reference counting logic
|
|
239
|
+
if (options.memoryDb) {
|
|
240
|
+
this.memoryDb = options.memoryDb;
|
|
241
|
+
this.memoryDb.retain();
|
|
242
|
+
} else if (options.memoryDbPath) {
|
|
243
|
+
this.memoryDb = MemoryDb.acquire(options.memoryDbPath);
|
|
244
|
+
} else {
|
|
245
|
+
const fromContainer = container.resolveOptional<MemoryDb>('memoryDb');
|
|
246
|
+
if (fromContainer) {
|
|
247
|
+
this.memoryDb = fromContainer;
|
|
248
|
+
this.memoryDb.retain();
|
|
249
|
+
} else {
|
|
250
|
+
this.memoryDb = MemoryDb.acquire(options.memoryDbPath);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
236
253
|
this.initRun(options);
|
|
237
254
|
|
|
238
255
|
this.validator = new WorkflowValidator(this.workflow, this.inputs);
|
|
@@ -439,6 +456,7 @@ export class WorkflowRunner {
|
|
|
439
456
|
redactForStorage: this.redactForStorage.bind(this),
|
|
440
457
|
emitEvent: this.emitEvent.bind(this),
|
|
441
458
|
workflowName: this.workflow.name,
|
|
459
|
+
depth: this.depth,
|
|
442
460
|
});
|
|
443
461
|
|
|
444
462
|
if (result.status === StepStatus.SUCCESS) {
|
|
@@ -466,13 +484,27 @@ export class WorkflowRunner {
|
|
|
466
484
|
// 2. Recursive rollback for sub-workflows
|
|
467
485
|
// Try to find if this step was a workflow step with a subRunId
|
|
468
486
|
const stepExec = await this.db.getMainStep(this.runId, compRecord.step_id);
|
|
469
|
-
|
|
470
|
-
|
|
487
|
+
|
|
488
|
+
let subRunId: string | undefined;
|
|
489
|
+
|
|
490
|
+
// Check metadata first (reliable storage even if step crashed)
|
|
491
|
+
if (stepExec?.metadata) {
|
|
471
492
|
try {
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
493
|
+
const metadata = JSON.parse(stepExec.metadata);
|
|
494
|
+
if (metadata.__subRunId) {
|
|
495
|
+
subRunId = metadata.__subRunId;
|
|
496
|
+
}
|
|
497
|
+
} catch (e) {
|
|
498
|
+
// Ignore metadata parsing check, fall back to output
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Fallback to output (legacy/completed steps)
|
|
503
|
+
if (!subRunId && stepExec?.output) {
|
|
504
|
+
try {
|
|
505
|
+
const output = JSON.parse(stepExec.output);
|
|
506
|
+
if (output?.__subRunId) {
|
|
507
|
+
subRunId = output.__subRunId;
|
|
476
508
|
}
|
|
477
509
|
} catch (e) {
|
|
478
510
|
this.logger.warn(
|
|
@@ -480,6 +512,10 @@ export class WorkflowRunner {
|
|
|
480
512
|
);
|
|
481
513
|
}
|
|
482
514
|
}
|
|
515
|
+
|
|
516
|
+
if (subRunId) {
|
|
517
|
+
await this.cascadeRollback(subRunId, errorReason);
|
|
518
|
+
}
|
|
483
519
|
}
|
|
484
520
|
|
|
485
521
|
this.logger.log(' Rollback completed.\n');
|
|
@@ -734,8 +770,10 @@ export class WorkflowRunner {
|
|
|
734
770
|
scopedKey: string;
|
|
735
771
|
ttlSeconds?: number;
|
|
736
772
|
claimed: boolean;
|
|
737
|
-
}
|
|
773
|
+
},
|
|
774
|
+
options?: { skipStatusUpdates?: boolean }
|
|
738
775
|
): Promise<StepContext> {
|
|
776
|
+
const skipStatusUpdates = options?.skipStatusUpdates === true;
|
|
739
777
|
// Check idempotency key for dedup (scoped per run by default)
|
|
740
778
|
const dedupEnabled = this.options.dedup !== false;
|
|
741
779
|
let idempotencyKey: string | undefined = idempotencyContext?.rawKey;
|
|
@@ -763,7 +801,9 @@ export class WorkflowRunner {
|
|
|
763
801
|
if (claim.status === 'hit') {
|
|
764
802
|
this.logger.log(` ⟳ Step ${step.id} skipped (idempotency hit: ${idempotencyKey})`);
|
|
765
803
|
const output = claim.output;
|
|
766
|
-
|
|
804
|
+
if (!skipStatusUpdates) {
|
|
805
|
+
await this.db.completeStep(stepExecId, 'success', output, claim.error || undefined);
|
|
806
|
+
}
|
|
767
807
|
return {
|
|
768
808
|
output,
|
|
769
809
|
outputs:
|
|
@@ -776,12 +816,14 @@ export class WorkflowRunner {
|
|
|
776
816
|
}
|
|
777
817
|
if (claim.status === 'in-flight') {
|
|
778
818
|
const errorMsg = `Idempotency key already in-flight: ${idempotencyKey}`;
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
819
|
+
if (!skipStatusUpdates) {
|
|
820
|
+
await this.db.completeStep(
|
|
821
|
+
stepExecId,
|
|
822
|
+
StepStatus.FAILED,
|
|
823
|
+
null,
|
|
824
|
+
this.secretManager.redactAtRest ? this.secretManager.redact(errorMsg) : errorMsg
|
|
825
|
+
);
|
|
826
|
+
}
|
|
785
827
|
return {
|
|
786
828
|
output: null,
|
|
787
829
|
outputs: {},
|
|
@@ -800,8 +842,24 @@ export class WorkflowRunner {
|
|
|
800
842
|
const cached = await this.db.getStepCache(cacheKey);
|
|
801
843
|
if (cached) {
|
|
802
844
|
this.logger.log(` ⚡ Step ${step.id} skipped (global cache hit)`);
|
|
845
|
+
|
|
846
|
+
// IMPORTANT: If we claimed an idempotency key, we MUST mark it as success
|
|
847
|
+
// otherwise it stays "in-flight" until TTL expires.
|
|
848
|
+
if (idempotencyClaimed && scopedIdempotencyKey && !skipStatusUpdates) {
|
|
849
|
+
await this.recordIdempotencyResult(
|
|
850
|
+
scopedIdempotencyKey,
|
|
851
|
+
step.id,
|
|
852
|
+
StepStatus.SUCCESS,
|
|
853
|
+
cached,
|
|
854
|
+
undefined,
|
|
855
|
+
idempotencyTtlSeconds
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
803
859
|
const output = JSON.parse(cached.output);
|
|
804
|
-
|
|
860
|
+
if (!skipStatusUpdates) {
|
|
861
|
+
await this.db.completeStep(stepExecId, StepStatus.SUCCESS, output);
|
|
862
|
+
}
|
|
805
863
|
return {
|
|
806
864
|
output,
|
|
807
865
|
outputs:
|
|
@@ -842,7 +900,7 @@ export class WorkflowRunner {
|
|
|
842
900
|
const isRecursion =
|
|
843
901
|
(context.reflexionAttempts as number) > 0 || (context.autoHealAttempts as number) > 0;
|
|
844
902
|
|
|
845
|
-
if (!isRecursion) {
|
|
903
|
+
if (!isRecursion && !skipStatusUpdates) {
|
|
846
904
|
await this.db.startStep(stepExecId);
|
|
847
905
|
}
|
|
848
906
|
|
|
@@ -859,12 +917,14 @@ export class WorkflowRunner {
|
|
|
859
917
|
idempotencyTtlSeconds
|
|
860
918
|
);
|
|
861
919
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
920
|
+
if (!skipStatusUpdates) {
|
|
921
|
+
await this.db.completeStep(
|
|
922
|
+
stepExecId,
|
|
923
|
+
StepStatus.SUSPENDED,
|
|
924
|
+
null,
|
|
925
|
+
this.secretManager.redactAtRest ? this.secretManager.redact(message) : message
|
|
926
|
+
);
|
|
927
|
+
}
|
|
868
928
|
return {
|
|
869
929
|
output: null,
|
|
870
930
|
outputs: {},
|
|
@@ -890,7 +950,9 @@ export class WorkflowRunner {
|
|
|
890
950
|
|
|
891
951
|
if (action.type === 'skip') {
|
|
892
952
|
this.logger.log(` ⏭️ Skipping step ${stepToExecute.id} at breakpoint`);
|
|
893
|
-
|
|
953
|
+
if (!skipStatusUpdates) {
|
|
954
|
+
await this.db.completeStep(stepExecId, StepStatus.SKIPPED, null, undefined, undefined);
|
|
955
|
+
}
|
|
894
956
|
return {
|
|
895
957
|
output: null,
|
|
896
958
|
outputs: {},
|
|
@@ -1082,7 +1144,9 @@ export class WorkflowRunner {
|
|
|
1082
1144
|
|
|
1083
1145
|
const result = await withRetry(operationWithTimeout, step.retry, async (attempt, error) => {
|
|
1084
1146
|
this.logger.log(` ↻ Retry ${attempt}/${step.retry?.count} for step ${step.id}`);
|
|
1085
|
-
|
|
1147
|
+
if (!skipStatusUpdates) {
|
|
1148
|
+
await this.db.incrementRetry(stepExecId);
|
|
1149
|
+
}
|
|
1086
1150
|
});
|
|
1087
1151
|
|
|
1088
1152
|
const persistedOutput = this.secretManager.redactForStorage(result.output);
|
|
@@ -1100,7 +1164,7 @@ export class WorkflowRunner {
|
|
|
1100
1164
|
await this.db.createTimer(timerId, this.runId, step.id, 'human');
|
|
1101
1165
|
}
|
|
1102
1166
|
}
|
|
1103
|
-
if (dedupEnabled && idempotencyClaimed) {
|
|
1167
|
+
if (dedupEnabled && idempotencyClaimed && !skipStatusUpdates) {
|
|
1104
1168
|
await this.recordIdempotencyResult(
|
|
1105
1169
|
scopedIdempotencyKey,
|
|
1106
1170
|
step.id,
|
|
@@ -1110,15 +1174,17 @@ export class WorkflowRunner {
|
|
|
1110
1174
|
idempotencyTtlSeconds
|
|
1111
1175
|
);
|
|
1112
1176
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1177
|
+
if (!skipStatusUpdates) {
|
|
1178
|
+
await this.db.completeStep(
|
|
1179
|
+
stepExecId,
|
|
1180
|
+
StepStatus.SUSPENDED,
|
|
1181
|
+
persistedOutput,
|
|
1182
|
+
this.secretManager.redactAtRest
|
|
1183
|
+
? this.secretManager.redact('Waiting for interaction')
|
|
1184
|
+
: 'Waiting for interaction',
|
|
1185
|
+
result.usage
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1122
1188
|
return result;
|
|
1123
1189
|
}
|
|
1124
1190
|
|
|
@@ -1131,7 +1197,7 @@ export class WorkflowRunner {
|
|
|
1131
1197
|
const timerId = randomUUID();
|
|
1132
1198
|
await this.db.createTimer(timerId, this.runId, step.id, 'sleep', wakeAt);
|
|
1133
1199
|
}
|
|
1134
|
-
if (dedupEnabled && idempotencyClaimed) {
|
|
1200
|
+
if (dedupEnabled && idempotencyClaimed && !skipStatusUpdates) {
|
|
1135
1201
|
await this.recordIdempotencyResult(
|
|
1136
1202
|
scopedIdempotencyKey,
|
|
1137
1203
|
step.id,
|
|
@@ -1141,24 +1207,28 @@ export class WorkflowRunner {
|
|
|
1141
1207
|
idempotencyTtlSeconds
|
|
1142
1208
|
);
|
|
1143
1209
|
}
|
|
1210
|
+
if (!skipStatusUpdates) {
|
|
1211
|
+
await this.db.completeStep(
|
|
1212
|
+
stepExecId,
|
|
1213
|
+
StepStatus.WAITING,
|
|
1214
|
+
persistedOutput,
|
|
1215
|
+
this.secretManager.redactAtRest ? this.secretManager.redact(waitError) : waitError,
|
|
1216
|
+
result.usage
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
result.error = waitError;
|
|
1220
|
+
return result;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (!skipStatusUpdates) {
|
|
1144
1224
|
await this.db.completeStep(
|
|
1145
1225
|
stepExecId,
|
|
1146
|
-
|
|
1226
|
+
result.status,
|
|
1147
1227
|
persistedOutput,
|
|
1148
|
-
|
|
1228
|
+
persistedError,
|
|
1149
1229
|
result.usage
|
|
1150
1230
|
);
|
|
1151
|
-
result.error = waitError;
|
|
1152
|
-
return result;
|
|
1153
1231
|
}
|
|
1154
|
-
|
|
1155
|
-
await this.db.completeStep(
|
|
1156
|
-
stepExecId,
|
|
1157
|
-
result.status,
|
|
1158
|
-
persistedOutput,
|
|
1159
|
-
persistedError,
|
|
1160
|
-
result.usage
|
|
1161
|
-
);
|
|
1162
1232
|
if (result.status === StepStatus.SUCCESS) {
|
|
1163
1233
|
const existingTimer = await this.db.getTimerByStep(this.runId, step.id);
|
|
1164
1234
|
if (existingTimer) {
|
|
@@ -1217,7 +1287,7 @@ export class WorkflowRunner {
|
|
|
1217
1287
|
outputs = {};
|
|
1218
1288
|
}
|
|
1219
1289
|
|
|
1220
|
-
if (dedupEnabled && idempotencyClaimed) {
|
|
1290
|
+
if (dedupEnabled && idempotencyClaimed && !skipStatusUpdates) {
|
|
1221
1291
|
await this.recordIdempotencyResult(
|
|
1222
1292
|
scopedIdempotencyKey,
|
|
1223
1293
|
step.id,
|
|
@@ -1237,7 +1307,7 @@ export class WorkflowRunner {
|
|
|
1237
1307
|
};
|
|
1238
1308
|
|
|
1239
1309
|
// Store in global cache if enabled
|
|
1240
|
-
if (cacheKey && result.status === StepStatus.SUCCESS) {
|
|
1310
|
+
if (cacheKey && result.status === StepStatus.SUCCESS && !skipStatusUpdates) {
|
|
1241
1311
|
const ttl = step.memoizeTtlSeconds;
|
|
1242
1312
|
await this.db.storeStepCache(cacheKey, this.workflow.name, step.id, persistedOutput, ttl);
|
|
1243
1313
|
}
|
|
@@ -1283,7 +1353,8 @@ export class WorkflowRunner {
|
|
|
1283
1353
|
newStep as Step,
|
|
1284
1354
|
nextContext,
|
|
1285
1355
|
stepExecId,
|
|
1286
|
-
idempotencyContextForRetry
|
|
1356
|
+
idempotencyContextForRetry,
|
|
1357
|
+
options // Pass options recursively (incl. skipStatusUpdates)
|
|
1287
1358
|
);
|
|
1288
1359
|
} catch (healError) {
|
|
1289
1360
|
this.logger.error(
|
|
@@ -1332,7 +1403,8 @@ export class WorkflowRunner {
|
|
|
1332
1403
|
newStep as Step,
|
|
1333
1404
|
nextContext,
|
|
1334
1405
|
stepExecId,
|
|
1335
|
-
idempotencyContextForRetry
|
|
1406
|
+
idempotencyContextForRetry,
|
|
1407
|
+
options
|
|
1336
1408
|
);
|
|
1337
1409
|
} catch (healError) {
|
|
1338
1410
|
this.logger.error(
|
|
@@ -1358,7 +1430,8 @@ export class WorkflowRunner {
|
|
|
1358
1430
|
stepToRun,
|
|
1359
1431
|
context,
|
|
1360
1432
|
stepExecId,
|
|
1361
|
-
idempotencyContextForRetry
|
|
1433
|
+
idempotencyContextForRetry,
|
|
1434
|
+
options
|
|
1362
1435
|
);
|
|
1363
1436
|
}
|
|
1364
1437
|
if (action.type === 'skip') {
|
|
@@ -1947,7 +2020,8 @@ Revise the output to address the feedback. Return only the corrected output.`;
|
|
|
1947
2020
|
private async executeSubWorkflow(
|
|
1948
2021
|
step: WorkflowStep,
|
|
1949
2022
|
context: ExpressionContext,
|
|
1950
|
-
abortSignal?: AbortSignal
|
|
2023
|
+
abortSignal?: AbortSignal,
|
|
2024
|
+
stepExecutionId?: string
|
|
1951
2025
|
): Promise<StepResult> {
|
|
1952
2026
|
const factory: RunnerFactory = {
|
|
1953
2027
|
create: (workflow, options) => new WorkflowRunner(workflow, options),
|
|
@@ -1960,8 +2034,10 @@ Revise the output to address the feedback. Return only the corrected output.`;
|
|
|
1960
2034
|
parentLogger: this.logger,
|
|
1961
2035
|
parentMcpManager: this.mcpManager,
|
|
1962
2036
|
parentDepth: this.depth,
|
|
1963
|
-
parentOptions: this.options,
|
|
2037
|
+
parentOptions: { ...this.options, executeStep: undefined }, // Prevent recursion of injected executeStep
|
|
1964
2038
|
abortSignal,
|
|
2039
|
+
stepExecutionId,
|
|
2040
|
+
parentDb: this.db,
|
|
1965
2041
|
});
|
|
1966
2042
|
}
|
|
1967
2043
|
|
|
@@ -1980,6 +2056,9 @@ Revise the output to address the feedback. Return only the corrected output.`;
|
|
|
1980
2056
|
// Track step.end events for summary generation
|
|
1981
2057
|
if (redacted.type === 'step.end') {
|
|
1982
2058
|
this.stepEvents.push(redacted);
|
|
2059
|
+
if (this.stepEvents.length > 2000) {
|
|
2060
|
+
this.stepEvents.shift();
|
|
2061
|
+
}
|
|
1983
2062
|
}
|
|
1984
2063
|
|
|
1985
2064
|
if (redacted.type === 'llm.thought') {
|
|
@@ -2076,12 +2155,16 @@ Revise the output to address the feedback. Return only the corrected output.`;
|
|
|
2076
2155
|
|
|
2077
2156
|
this.logger.log(`\n🏛️ ${isResume ? 'Resuming' : 'Running'} workflow: ${this.workflow.name}`);
|
|
2078
2157
|
this.logger.log(`Run ID: ${this.runId}`);
|
|
2079
|
-
this.logger.log(
|
|
2080
|
-
'\n⚠️ Security Warning: Only run workflows from trusted sources.\n' +
|
|
2081
|
-
' Workflows can execute arbitrary shell commands and access your environment.\n'
|
|
2082
|
-
);
|
|
2083
2158
|
|
|
2084
|
-
|
|
2159
|
+
const config = ConfigLoader.load();
|
|
2160
|
+
if (!config.logging?.suppress_security_warning) {
|
|
2161
|
+
this.logger.log(
|
|
2162
|
+
'\n⚠️ Security Warning: Only run workflows from trusted sources.\n' +
|
|
2163
|
+
' Workflows can execute arbitrary shell commands and access your environment.\n'
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
this.secretManager.redactAtRest = config.storage?.redact_secrets_at_rest ?? true;
|
|
2085
2168
|
|
|
2086
2169
|
// Apply defaults and validate inputs
|
|
2087
2170
|
const validated = this.validator.applyDefaultsAndValidate();
|
|
@@ -2193,7 +2276,9 @@ Revise the output to address the feedback. Return only the corrected output.`;
|
|
|
2193
2276
|
// 1. Find runnable steps from scheduler
|
|
2194
2277
|
const runnableSteps = this.scheduler
|
|
2195
2278
|
.getRunnableSteps(runningPromises.size, globalConcurrencyLimit)
|
|
2196
|
-
.filter(
|
|
2279
|
+
.filter(
|
|
2280
|
+
(step) => step.foreach || this.resourcePool.hasCapacity(step.pool || step.type)
|
|
2281
|
+
);
|
|
2197
2282
|
|
|
2198
2283
|
for (const step of runnableSteps) {
|
|
2199
2284
|
// Don't schedule new steps if canceled
|
|
@@ -2215,7 +2300,13 @@ Revise the output to address the feedback. Return only the corrected output.`;
|
|
|
2215
2300
|
this.logger.debug?.(
|
|
2216
2301
|
`[${stepIndex}/${totalSteps}] ⏳ Waiting for pool: ${poolName}`
|
|
2217
2302
|
);
|
|
2218
|
-
|
|
2303
|
+
// For foreach steps, we don't acquire the resource in the parent step
|
|
2304
|
+
// The ForeachExecutor will handle acquiring it for each iteration
|
|
2305
|
+
if (!step.foreach) {
|
|
2306
|
+
release = await this.resourcePool.acquire(poolName, {
|
|
2307
|
+
signal: this.abortSignal,
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2219
2310
|
|
|
2220
2311
|
this.logger.log(
|
|
2221
2312
|
`[${stepIndex}/${totalSteps}] ▶ Executing step: ${step.id} (${step.type})`
|
|
@@ -2389,7 +2480,9 @@ Revise the output to address the feedback. Return only the corrected output.`;
|
|
|
2389
2480
|
if (!this.options.mcpManager) {
|
|
2390
2481
|
await this.mcpManager.stopAll();
|
|
2391
2482
|
}
|
|
2392
|
-
this.
|
|
2483
|
+
if (this.ownsDb) {
|
|
2484
|
+
this.db.close();
|
|
2485
|
+
}
|
|
2393
2486
|
}
|
|
2394
2487
|
}
|
|
2395
2488
|
|
|
@@ -2597,7 +2690,7 @@ Revise the output to address the feedback. Return only the corrected output.`;
|
|
|
2597
2690
|
outputs[key] = ExpressionEvaluator.evaluate(expression, context);
|
|
2598
2691
|
} catch (error) {
|
|
2599
2692
|
this.logger.warn(
|
|
2600
|
-
|
|
2693
|
+
`⚠️ Failed to evaluate output '${key}': ${error instanceof Error ? error.message : String(error)}. Setting to null.`
|
|
2601
2694
|
);
|
|
2602
2695
|
outputs[key] = null;
|
|
2603
2696
|
}
|