@pikku/core 0.12.70 → 0.12.72
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/CHANGELOG.md +143 -0
- package/LICENSE +21 -0
- package/dist/function/function-runner.js +7 -2
- package/dist/handle-error.d.ts +2 -0
- package/dist/handle-error.js +8 -2
- package/dist/schema.d.ts +24 -0
- package/dist/schema.js +46 -0
- package/dist/services/in-memory-queue-service.d.ts +6 -0
- package/dist/services/in-memory-queue-service.js +8 -1
- package/dist/services/in-memory-workflow-service.d.ts +3 -5
- package/dist/services/in-memory-workflow-service.js +10 -19
- package/dist/services/workflow-service.d.ts +7 -5
- package/dist/types/core.types.d.ts +7 -0
- package/dist/wirings/ai-agent/ai-agent-agui.js +0 -8
- package/dist/wirings/ai-agent/ai-agent-prepare.js +1 -2
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +0 -6
- package/dist/wirings/http/http-runner.d.ts +1 -1
- package/dist/wirings/http/http-runner.js +4 -2
- package/dist/wirings/http/http.types.d.ts +2 -0
- package/dist/wirings/http/index.d.ts +2 -1
- package/dist/wirings/http/index.js +1 -1
- package/dist/wirings/http/pikku-fetch-http-request.d.ts +11 -1
- package/dist/wirings/http/pikku-fetch-http-request.js +67 -3
- package/dist/wirings/mcp/mcp-runner.d.ts +5 -0
- package/dist/wirings/mcp/mcp-runner.js +5 -2
- package/dist/wirings/workflow/graph/graph-runner.js +3 -2
- package/dist/wirings/workflow/graph/graph-validation.d.ts +0 -2
- package/dist/wirings/workflow/graph/graph-validation.js +0 -142
- package/dist/wirings/workflow/graph/index.d.ts +1 -1
- package/dist/wirings/workflow/graph/index.js +1 -1
- package/dist/wirings/workflow/index.d.ts +0 -1
- package/dist/wirings/workflow/index.js +0 -2
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +69 -15
- package/dist/wirings/workflow/pikku-workflow-service.js +260 -164
- package/dist/wirings/workflow/workflow.types.d.ts +1 -6
- package/package.json +1 -2
- package/src/function/function-runner.test.ts +75 -0
- package/src/function/function-runner.ts +15 -2
- package/src/gopass-secrets-removed.test.ts +51 -0
- package/src/handle-error.test.ts +108 -1
- package/src/handle-error.ts +10 -2
- package/src/schema.test.ts +103 -0
- package/src/schema.ts +51 -0
- package/src/services/in-memory-queue-service.test.ts +66 -1
- package/src/services/in-memory-queue-service.ts +13 -2
- package/src/services/in-memory-workflow-service.ts +12 -25
- package/src/services/workflow-service.ts +7 -4
- package/src/types/core.types.ts +7 -0
- package/src/wirings/ai-agent/ai-agent-agui.test.ts +0 -16
- package/src/wirings/ai-agent/ai-agent-agui.ts +0 -9
- package/src/wirings/ai-agent/ai-agent-prepare.ts +1 -2
- package/src/wirings/ai-agent/ai-agent.types.ts +0 -7
- package/src/wirings/http/http-runner.ts +4 -1
- package/src/wirings/http/http.types.ts +2 -0
- package/src/wirings/http/index.ts +5 -1
- package/src/wirings/http/pikku-fetch-http-request.test.ts +89 -0
- package/src/wirings/http/pikku-fetch-http-request.ts +85 -3
- package/src/wirings/mcp/mcp-runner.test.ts +40 -0
- package/src/wirings/mcp/mcp-runner.ts +11 -2
- package/src/wirings/workflow/graph/graph-runner.ts +3 -2
- package/src/wirings/workflow/graph/graph-validation.test.ts +1 -144
- package/src/wirings/workflow/graph/graph-validation.ts +0 -196
- package/src/wirings/workflow/graph/index.ts +1 -5
- package/src/wirings/workflow/index.ts +0 -6
- package/src/wirings/workflow/pikku-workflow-service.ts +377 -212
- package/src/wirings/workflow/scenario-expectations.test.ts +153 -0
- package/src/wirings/workflow/scenario-step.test.ts +1 -1
- package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
- package/src/wirings/workflow/workflow-dispatch-payload.test.ts +59 -0
- package/src/wirings/workflow/workflow-mirror.test.ts +178 -0
- package/src/wirings/workflow/workflow-replay-snapshot.test.ts +139 -0
- package/src/wirings/workflow/workflow-run-context.test.ts +177 -0
- package/src/wirings/workflow/workflow-run-polling.test.ts +132 -0
- package/src/wirings/workflow/workflow-step-ordinal.test.ts +4 -4
- package/src/wirings/workflow/workflow.types.ts +1 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/src/services/gopass-secrets.ts +0 -78
|
@@ -337,15 +337,80 @@ export interface WorkflowRunExtension {
|
|
|
337
337
|
): Promise<void>
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
/**
|
|
341
|
+
* States a run never leaves. `suspended` is deliberately absent: a suspended
|
|
342
|
+
* run stops a poll loop but can still be resumed, so anything the process holds
|
|
343
|
+
* for it has to survive.
|
|
344
|
+
*/
|
|
345
|
+
const WORKFLOW_TERMINAL_STATES: ReadonlySet<string> = new Set([
|
|
346
|
+
'completed',
|
|
347
|
+
'failed',
|
|
348
|
+
'cancelled',
|
|
349
|
+
])
|
|
350
|
+
|
|
351
|
+
/** First wait when polling a run, before the backoff starts widening it. */
|
|
352
|
+
const WORKFLOW_POLL_MIN_MS = 10
|
|
353
|
+
|
|
354
|
+
/** How much each successive wait grows, up to the caller's ceiling. */
|
|
355
|
+
const WORKFLOW_POLL_FACTOR = 1.6
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Ceiling for the wait on an inline sub-workflow. Lower than a top-level run's
|
|
359
|
+
* default, because the parent step is blocked on it and every wait here is
|
|
360
|
+
* added latency in the middle of a workflow rather than at its edge.
|
|
361
|
+
*/
|
|
362
|
+
const WORKFLOW_CHILD_POLL_MAX_MS = 500
|
|
363
|
+
|
|
340
364
|
/**
|
|
341
365
|
* Abstract workflow state service
|
|
342
366
|
* Implementations provide pluggable storage backends (SQLite, PostgreSQL, etc.)
|
|
343
367
|
* Combines orchestration and step execution
|
|
344
368
|
*/
|
|
369
|
+
/**
|
|
370
|
+
* Everything the engine holds in memory for a run that is executing in this
|
|
371
|
+
* process. One entry, one lifetime: created when the run starts executing here
|
|
372
|
+
* and dropped when nothing is holding it open any more.
|
|
373
|
+
*
|
|
374
|
+
* The `replay` half is rebuilt from scratch on every orchestrator tick; the
|
|
375
|
+
* rest outlives individual ticks and belongs to whoever started the run.
|
|
376
|
+
*/
|
|
377
|
+
type RunContext = {
|
|
378
|
+
/** Executing straight through in-process, without a queue. */
|
|
379
|
+
inline: boolean
|
|
380
|
+
replay?: {
|
|
381
|
+
/** How many times this walk has reached each logical step name. */
|
|
382
|
+
ordinals: Map<string, number>
|
|
383
|
+
/** The step key the walk last reached — the next step's predecessor. */
|
|
384
|
+
lastStep?: string
|
|
385
|
+
/** Every step of the run as this replay found it, keyed by step name. */
|
|
386
|
+
steps?: Map<string, StepState>
|
|
387
|
+
/** The run as this replay found it. Only its immutable half is reused. */
|
|
388
|
+
run?: WorkflowRun
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
345
392
|
export abstract class PikkuWorkflowService implements WorkflowService {
|
|
346
|
-
private inlineRuns = new Set<string>()
|
|
347
393
|
private runExtension?: WorkflowRunExtension
|
|
348
394
|
|
|
395
|
+
private runContexts = new Map<string, RunContext>()
|
|
396
|
+
|
|
397
|
+
private contextFor(runId: string): RunContext {
|
|
398
|
+
let context = this.runContexts.get(runId)
|
|
399
|
+
if (!context) {
|
|
400
|
+
context = { inline: false }
|
|
401
|
+
this.runContexts.set(runId, context)
|
|
402
|
+
}
|
|
403
|
+
return context
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Drop a run's context once nothing is holding it open. */
|
|
407
|
+
private releaseContext(runId: string): void {
|
|
408
|
+
const context = this.runContexts.get(runId)
|
|
409
|
+
if (!context) return
|
|
410
|
+
if (context.inline || context.replay) return
|
|
411
|
+
this.runContexts.delete(runId)
|
|
412
|
+
}
|
|
413
|
+
|
|
349
414
|
protected get logger() {
|
|
350
415
|
return getSingletonServices()?.logger
|
|
351
416
|
}
|
|
@@ -372,19 +437,36 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
372
437
|
}
|
|
373
438
|
}
|
|
374
439
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
440
|
+
/**
|
|
441
|
+
* Perform a state write, then shadow it to the mirror.
|
|
442
|
+
*
|
|
443
|
+
* The mirror is an observability sink, never a second source of truth, and
|
|
444
|
+
* both halves of that follow from this one shape: it is only ever told about
|
|
445
|
+
* a write that already landed, and a mirror that is down or throwing cannot
|
|
446
|
+
* fail — or even be seen by — the workflow it is watching.
|
|
447
|
+
*
|
|
448
|
+
* @param write - the authoritative write; its result is what the caller gets
|
|
449
|
+
* @param mirror - shadows the write, given the live mirror and what was written
|
|
450
|
+
*/
|
|
451
|
+
private async mirrored<T>(
|
|
452
|
+
write: () => Promise<T>,
|
|
453
|
+
mirror: (mirror: WorkflowRunMirror, written: T) => Promise<void>
|
|
454
|
+
): Promise<T> {
|
|
455
|
+
const written = await write()
|
|
456
|
+
if (this.mirror) {
|
|
380
457
|
try {
|
|
381
|
-
this.
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
458
|
+
await mirror(this.mirror, written)
|
|
459
|
+
} catch (err: any) {
|
|
460
|
+
try {
|
|
461
|
+
this.logger?.warn?.(
|
|
462
|
+
`[pikku] WorkflowRunMirror write failed: ${err?.message ?? err}`
|
|
463
|
+
)
|
|
464
|
+
} catch {
|
|
465
|
+
// logger unavailable (e.g. singleton services not initialized) — swallow
|
|
466
|
+
}
|
|
386
467
|
}
|
|
387
468
|
}
|
|
469
|
+
return written
|
|
388
470
|
}
|
|
389
471
|
|
|
390
472
|
/**
|
|
@@ -520,21 +602,24 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
520
602
|
* Check if a run is executing inline (without queues)
|
|
521
603
|
*/
|
|
522
604
|
protected isInline(runId: string): boolean {
|
|
523
|
-
return this.
|
|
605
|
+
return this.runContexts.get(runId)?.inline === true
|
|
524
606
|
}
|
|
525
607
|
|
|
526
608
|
/**
|
|
527
609
|
* Register a run as inline (for graph-runner to use)
|
|
528
610
|
*/
|
|
529
611
|
public registerInlineRun(runId: string): void {
|
|
530
|
-
this.
|
|
612
|
+
this.contextFor(runId).inline = true
|
|
531
613
|
}
|
|
532
614
|
|
|
533
615
|
/**
|
|
534
616
|
* Unregister a run from inline tracking
|
|
535
617
|
*/
|
|
536
618
|
public unregisterInlineRun(runId: string): void {
|
|
537
|
-
this.
|
|
619
|
+
const context = this.runContexts.get(runId)
|
|
620
|
+
if (!context) return
|
|
621
|
+
context.inline = false
|
|
622
|
+
this.releaseContext(runId)
|
|
538
623
|
}
|
|
539
624
|
|
|
540
625
|
public async registerWorkflowVersions(): Promise<void> {
|
|
@@ -556,26 +641,27 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
556
641
|
plannedSteps?: WorkflowPlannedStep[]
|
|
557
642
|
}
|
|
558
643
|
): Promise<string> {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
644
|
+
return this.mirrored(
|
|
645
|
+
() =>
|
|
646
|
+
this.createRunImpl(
|
|
647
|
+
workflowName,
|
|
648
|
+
input,
|
|
649
|
+
inline,
|
|
650
|
+
graphHash,
|
|
651
|
+
wire,
|
|
652
|
+
options
|
|
653
|
+
),
|
|
654
|
+
(mirror, runId) =>
|
|
655
|
+
mirror.createRun(
|
|
656
|
+
runId,
|
|
657
|
+
workflowName,
|
|
658
|
+
input,
|
|
659
|
+
inline,
|
|
660
|
+
graphHash,
|
|
661
|
+
wire,
|
|
662
|
+
options
|
|
663
|
+
)
|
|
577
664
|
)
|
|
578
|
-
return runId
|
|
579
665
|
}
|
|
580
666
|
|
|
581
667
|
protected abstract createRunImpl(
|
|
@@ -702,10 +788,17 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
702
788
|
output?: any,
|
|
703
789
|
error?: SerializedError
|
|
704
790
|
): Promise<void> {
|
|
705
|
-
await this.
|
|
706
|
-
|
|
707
|
-
|
|
791
|
+
await this.mirrored(
|
|
792
|
+
() => this.updateRunStatusImpl(id, status, output, error),
|
|
793
|
+
(mirror) => mirror.updateRunStatus(id, status, output, error)
|
|
708
794
|
)
|
|
795
|
+
if (WORKFLOW_TERMINAL_STATES.has(status)) {
|
|
796
|
+
// The run is over: release whatever this process opened for it. Queued
|
|
797
|
+
// runs never pass through the inline path that does this, so their
|
|
798
|
+
// context was held for the life of the process.
|
|
799
|
+
this.runExtension?.detachRunContext(id)
|
|
800
|
+
this.releaseContext(id)
|
|
801
|
+
}
|
|
709
802
|
}
|
|
710
803
|
|
|
711
804
|
protected abstract updateRunStatusImpl(
|
|
@@ -736,18 +829,19 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
736
829
|
stepOptions?: WorkflowStepOptions,
|
|
737
830
|
fromStepName?: string
|
|
738
831
|
): Promise<StepState> {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
832
|
+
return this.mirrored(
|
|
833
|
+
() =>
|
|
834
|
+
this.insertStepStateImpl(
|
|
835
|
+
runId,
|
|
836
|
+
stepName,
|
|
837
|
+
rpcName,
|
|
838
|
+
data,
|
|
839
|
+
stepOptions,
|
|
840
|
+
fromStepName
|
|
841
|
+
),
|
|
842
|
+
(mirror, step) =>
|
|
843
|
+
mirror.insertStepState(runId, { ...step, stepName, rpcName, data })
|
|
749
844
|
)
|
|
750
|
-
return step
|
|
751
845
|
}
|
|
752
846
|
|
|
753
847
|
protected abstract insertStepStateImpl(
|
|
@@ -773,8 +867,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
773
867
|
* @param stepId - Step ID
|
|
774
868
|
*/
|
|
775
869
|
public async setStepRunning(stepId: string): Promise<void> {
|
|
776
|
-
await this.
|
|
777
|
-
|
|
870
|
+
await this.mirrored(
|
|
871
|
+
() => this.setStepRunningImpl(stepId),
|
|
872
|
+
(mirror) => mirror.setStepRunning(stepId)
|
|
873
|
+
)
|
|
778
874
|
}
|
|
779
875
|
|
|
780
876
|
protected abstract setStepRunningImpl(stepId: string): Promise<void>
|
|
@@ -785,8 +881,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
785
881
|
* @param stepId - Step ID
|
|
786
882
|
*/
|
|
787
883
|
public async setStepScheduled(stepId: string): Promise<void> {
|
|
788
|
-
await this.
|
|
789
|
-
|
|
884
|
+
await this.mirrored(
|
|
885
|
+
() => this.setStepScheduledImpl(stepId),
|
|
886
|
+
(mirror) => mirror.setStepScheduled(stepId)
|
|
887
|
+
)
|
|
790
888
|
}
|
|
791
889
|
|
|
792
890
|
protected abstract setStepScheduledImpl(stepId: string): Promise<void>
|
|
@@ -798,8 +896,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
798
896
|
* @param result - Step result
|
|
799
897
|
*/
|
|
800
898
|
public async setStepResult(stepId: string, result: any): Promise<void> {
|
|
801
|
-
await this.
|
|
802
|
-
|
|
899
|
+
await this.mirrored(
|
|
900
|
+
() => this.setStepResultImpl(stepId, result),
|
|
901
|
+
(mirror) => mirror.setStepResult(stepId, result)
|
|
902
|
+
)
|
|
803
903
|
}
|
|
804
904
|
|
|
805
905
|
protected abstract setStepResultImpl(
|
|
@@ -816,9 +916,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
816
916
|
stepId: string,
|
|
817
917
|
childRunId: string
|
|
818
918
|
): Promise<void> {
|
|
819
|
-
await this.
|
|
820
|
-
|
|
821
|
-
|
|
919
|
+
await this.mirrored(
|
|
920
|
+
() => this.setStepChildRunIdImpl(stepId, childRunId),
|
|
921
|
+
(mirror) => mirror.setStepChildRunId(stepId, childRunId)
|
|
822
922
|
)
|
|
823
923
|
}
|
|
824
924
|
|
|
@@ -834,14 +934,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
834
934
|
* @param error - Error object
|
|
835
935
|
*/
|
|
836
936
|
public async setStepError(stepId: string, error: Error): Promise<void> {
|
|
837
|
-
await this.
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
937
|
+
await this.mirrored(
|
|
938
|
+
() => this.setStepErrorImpl(stepId, error),
|
|
939
|
+
(mirror) => {
|
|
940
|
+
const serialized: SerializedError = {
|
|
941
|
+
message: error.message,
|
|
942
|
+
stack: error.stack,
|
|
943
|
+
code: (error as any).code,
|
|
944
|
+
expected: isExpectedError(error),
|
|
945
|
+
}
|
|
946
|
+
return mirror.setStepError(stepId, serialized)
|
|
947
|
+
}
|
|
948
|
+
)
|
|
845
949
|
}
|
|
846
950
|
|
|
847
951
|
protected abstract setStepErrorImpl(
|
|
@@ -861,15 +965,14 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
861
965
|
failedStepId: string,
|
|
862
966
|
status: 'pending' | 'running'
|
|
863
967
|
): Promise<StepState> {
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
968
|
+
return this.mirrored(
|
|
969
|
+
() => this.createRetryAttemptImpl(failedStepId, status),
|
|
970
|
+
(mirror, newStep) =>
|
|
971
|
+
mirror.createRetryAttempt(failedStepId, {
|
|
972
|
+
...newStep,
|
|
973
|
+
stepName: (newStep as any).stepName ?? '',
|
|
974
|
+
})
|
|
871
975
|
)
|
|
872
|
-
return newStep
|
|
873
976
|
}
|
|
874
977
|
|
|
875
978
|
protected abstract createRetryAttemptImpl(
|
|
@@ -963,8 +1066,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
963
1066
|
stepId: string,
|
|
964
1067
|
branchKey: string
|
|
965
1068
|
): Promise<void> {
|
|
966
|
-
await this.
|
|
967
|
-
|
|
1069
|
+
await this.mirrored(
|
|
1070
|
+
() => this.setBranchTakenImpl(stepId, branchKey),
|
|
1071
|
+
(mirror) => mirror.setBranchTaken(stepId, branchKey)
|
|
1072
|
+
)
|
|
968
1073
|
}
|
|
969
1074
|
|
|
970
1075
|
protected abstract setBranchTakenImpl(
|
|
@@ -983,8 +1088,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
983
1088
|
name: string,
|
|
984
1089
|
value: unknown
|
|
985
1090
|
): Promise<void> {
|
|
986
|
-
await this.
|
|
987
|
-
|
|
1091
|
+
await this.mirrored(
|
|
1092
|
+
() => this.updateRunStateImpl(runId, name, value),
|
|
1093
|
+
(mirror) => mirror.updateRunState(runId, name, value)
|
|
1094
|
+
)
|
|
988
1095
|
}
|
|
989
1096
|
|
|
990
1097
|
protected abstract updateRunStateImpl(
|
|
@@ -1007,9 +1114,11 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1007
1114
|
source: string,
|
|
1008
1115
|
status?: WorkflowVersionStatus
|
|
1009
1116
|
): Promise<void> {
|
|
1010
|
-
await this.
|
|
1011
|
-
|
|
1012
|
-
|
|
1117
|
+
await this.mirrored(
|
|
1118
|
+
() =>
|
|
1119
|
+
this.upsertWorkflowVersionImpl(name, graphHash, graph, source, status),
|
|
1120
|
+
(mirror) =>
|
|
1121
|
+
mirror.upsertWorkflowVersion(name, graphHash, graph, source, status)
|
|
1013
1122
|
)
|
|
1014
1123
|
}
|
|
1015
1124
|
|
|
@@ -1026,9 +1135,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1026
1135
|
graphHash: string,
|
|
1027
1136
|
status: WorkflowVersionStatus
|
|
1028
1137
|
): Promise<void> {
|
|
1029
|
-
await this.
|
|
1030
|
-
|
|
1031
|
-
|
|
1138
|
+
await this.mirrored(
|
|
1139
|
+
() => this.updateWorkflowVersionStatusImpl(name, graphHash, status),
|
|
1140
|
+
(mirror) => mirror.updateWorkflowVersionStatus(name, graphHash, status)
|
|
1032
1141
|
)
|
|
1033
1142
|
}
|
|
1034
1143
|
|
|
@@ -1043,10 +1152,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1043
1152
|
graphHash: string
|
|
1044
1153
|
): Promise<{ graph: any; source: string } | null>
|
|
1045
1154
|
|
|
1046
|
-
abstract getAIGeneratedWorkflows(
|
|
1047
|
-
agentName?: string
|
|
1048
|
-
): Promise<Array<{ workflowName: string; graphHash: string; graph: any }>>
|
|
1049
|
-
|
|
1050
1155
|
// ============================================================================
|
|
1051
1156
|
// Workflow Lifecycle Methods
|
|
1052
1157
|
// ============================================================================
|
|
@@ -1116,9 +1221,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1116
1221
|
const queueService = this.verifyQueueService()
|
|
1117
1222
|
await queueService.add(
|
|
1118
1223
|
this.getStepWorkerQueueName(rpcName),
|
|
1119
|
-
|
|
1120
|
-
JSON.stringify({ runId, stepName, rpcName, data, fromStepName })
|
|
1121
|
-
),
|
|
1224
|
+
{ runId, stepName, rpcName, data, fromStepName },
|
|
1122
1225
|
{
|
|
1123
1226
|
...this.resolveStepJobOptions(stepOptions),
|
|
1124
1227
|
// Group by step function, mirroring how per-step queues split them —
|
|
@@ -1210,9 +1313,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1210
1313
|
try {
|
|
1211
1314
|
await getSingletonServices()!.queueService!.add(
|
|
1212
1315
|
this.getStepWorkerQueueName(rpcName),
|
|
1213
|
-
|
|
1214
|
-
JSON.stringify({ runId, stepName, rpcName, data, fromStepName })
|
|
1215
|
-
),
|
|
1316
|
+
{ runId, stepName, rpcName, data, fromStepName },
|
|
1216
1317
|
{
|
|
1217
1318
|
...this.resolveStepJobOptions(stepOptions),
|
|
1218
1319
|
group: this.getJobGroup(rpcName),
|
|
@@ -1299,27 +1400,16 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1299
1400
|
onRunCreated?: (runId: string) => void
|
|
1300
1401
|
}
|
|
1301
1402
|
): Promise<{ runId: string }> {
|
|
1302
|
-
// Resolve workflow from static meta (root or addon namespace)
|
|
1403
|
+
// Resolve workflow from static meta (root or addon namespace)
|
|
1303
1404
|
const resolved = resolveWorkflowMeta(name)
|
|
1304
|
-
|
|
1405
|
+
const workflowMeta = resolved?.meta
|
|
1305
1406
|
const packageName = resolved?.packageName ?? null
|
|
1306
1407
|
|
|
1307
|
-
if (!workflowMeta) {
|
|
1308
|
-
const dynamicWorkflows = await this.getAIGeneratedWorkflows()
|
|
1309
|
-
const match = dynamicWorkflows.find((w) => w.workflowName === name)
|
|
1310
|
-
if (match?.graph) {
|
|
1311
|
-
workflowMeta = match.graph
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
1408
|
if (!workflowMeta) {
|
|
1316
1409
|
throw new WorkflowNotFoundError(name)
|
|
1317
1410
|
}
|
|
1318
1411
|
|
|
1319
|
-
if (
|
|
1320
|
-
workflowMeta.source === 'graph' ||
|
|
1321
|
-
workflowMeta.source === 'dynamic-workflow'
|
|
1322
|
-
) {
|
|
1412
|
+
if (workflowMeta.source === 'graph') {
|
|
1323
1413
|
const shouldInline =
|
|
1324
1414
|
options?.inline || !getSingletonServices()?.queueService
|
|
1325
1415
|
return runWorkflowGraph(
|
|
@@ -1366,7 +1456,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1366
1456
|
await this.runExtension?.attachRunContext(runId, workflowMeta, options)
|
|
1367
1457
|
|
|
1368
1458
|
if (shouldInline) {
|
|
1369
|
-
this.
|
|
1459
|
+
this.registerInlineRun(runId)
|
|
1370
1460
|
try {
|
|
1371
1461
|
await this.runWorkflowJob(runId, rpcService)
|
|
1372
1462
|
} catch (error: any) {
|
|
@@ -1399,7 +1489,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1399
1489
|
throw error
|
|
1400
1490
|
}
|
|
1401
1491
|
} finally {
|
|
1402
|
-
this.
|
|
1492
|
+
this.unregisterInlineRun(runId)
|
|
1403
1493
|
this.runExtension?.detachRunContext(runId)
|
|
1404
1494
|
}
|
|
1405
1495
|
} else {
|
|
@@ -1415,7 +1505,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1415
1505
|
rpcService: any,
|
|
1416
1506
|
options?: { pollIntervalMs?: number; wire?: WorkflowRunWire }
|
|
1417
1507
|
): Promise<any> {
|
|
1418
|
-
const pollInterval = options?.pollIntervalMs ?? 1000
|
|
1419
1508
|
const { runId } = await this.startWorkflow(
|
|
1420
1509
|
name,
|
|
1421
1510
|
input,
|
|
@@ -1423,38 +1512,155 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1423
1512
|
rpcService,
|
|
1424
1513
|
{ inline: true }
|
|
1425
1514
|
)
|
|
1515
|
+
const run = await this.awaitRunEnd(runId, options?.pollIntervalMs ?? 1000)
|
|
1516
|
+
if (run.status === 'failed') {
|
|
1517
|
+
throw new WorkflowRunFailedError(run.error?.message)
|
|
1518
|
+
}
|
|
1519
|
+
if (run.status === 'cancelled') {
|
|
1520
|
+
throw new WorkflowRunCancelledError()
|
|
1521
|
+
}
|
|
1522
|
+
return run.output
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/**
|
|
1526
|
+
* Read a run until it reaches an end state, backing off as it drags on.
|
|
1527
|
+
*
|
|
1528
|
+
* A fixed interval is wrong at both ends: it makes a workflow that finished
|
|
1529
|
+
* in milliseconds wait out the whole interval anyway, and it keeps reading a
|
|
1530
|
+
* long-running one at full rate for as long as it lasts. Starting short and
|
|
1531
|
+
* growing to `maxIntervalMs` returns quick runs promptly while a slow run's
|
|
1532
|
+
* read cost grows logarithmically rather than linearly with its duration.
|
|
1533
|
+
*/
|
|
1534
|
+
protected async awaitRunEnd(
|
|
1535
|
+
runId: string,
|
|
1536
|
+
maxIntervalMs: number
|
|
1537
|
+
): Promise<WorkflowRun> {
|
|
1538
|
+
let interval = Math.min(WORKFLOW_POLL_MIN_MS, maxIntervalMs)
|
|
1426
1539
|
while (true) {
|
|
1427
1540
|
const run = await this.getRun(runId)
|
|
1428
1541
|
if (!run) {
|
|
1429
1542
|
throw new WorkflowRunNotFoundError(runId)
|
|
1430
1543
|
}
|
|
1431
1544
|
if (WORKFLOW_END_STATES.has(run.status)) {
|
|
1432
|
-
|
|
1433
|
-
throw new WorkflowRunFailedError(run.error?.message)
|
|
1434
|
-
}
|
|
1435
|
-
if (run.status === 'cancelled') {
|
|
1436
|
-
throw new WorkflowRunCancelledError()
|
|
1437
|
-
}
|
|
1438
|
-
return run.output
|
|
1545
|
+
return run
|
|
1439
1546
|
}
|
|
1440
|
-
await
|
|
1547
|
+
await this.waitBeforeNextRead(interval)
|
|
1548
|
+
interval = Math.min(interval * WORKFLOW_POLL_FACTOR, maxIntervalMs)
|
|
1441
1549
|
}
|
|
1442
1550
|
}
|
|
1443
1551
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1552
|
+
/**
|
|
1553
|
+
* Wait between two reads of a run.
|
|
1554
|
+
*
|
|
1555
|
+
* Its own method so the backoff schedule can be asserted on directly. Timing
|
|
1556
|
+
* a poll loop by the clock measures the host's scheduler as much as the
|
|
1557
|
+
* policy — `setTimeout(40)` routinely returns late on a loaded runner — which
|
|
1558
|
+
* makes the obvious test both slow and flaky.
|
|
1559
|
+
*/
|
|
1560
|
+
protected async waitBeforeNextRead(ms: number): Promise<void> {
|
|
1561
|
+
await new Promise((resolve) => setTimeout(resolve, ms))
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/**
|
|
1565
|
+
* Every step of a run in one read, or `null` if this backend has no bulk read.
|
|
1566
|
+
*
|
|
1567
|
+
* A replay walks the DSL body from the top, and each step it passes asks for
|
|
1568
|
+
* its own row — so a run of N steps costs N reads per replay and O(N^2) over
|
|
1569
|
+
* its lifetime. Backends that can answer this in a single query collapse that
|
|
1570
|
+
* to one read per replay.
|
|
1571
|
+
*/
|
|
1572
|
+
protected async listStepStates(
|
|
1573
|
+
_runId: string
|
|
1574
|
+
): Promise<Array<StepState & { stepName: string }> | null> {
|
|
1575
|
+
return null
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Begin a replay pass: fresh ordinal counters, and one read of the steps the
|
|
1580
|
+
* run has already taken so the walk back to where it left off is served from
|
|
1581
|
+
* memory. Safe because a pass reaches each step key at most once, and the
|
|
1582
|
+
* steps it replays past are `succeeded` and therefore immutable.
|
|
1583
|
+
*/
|
|
1584
|
+
private async beginReplay(runId: string): Promise<void> {
|
|
1585
|
+
const context = this.contextFor(runId)
|
|
1586
|
+
context.replay = { ordinals: new Map() }
|
|
1587
|
+
const steps = await this.listStepStates(runId)
|
|
1588
|
+
if (steps) {
|
|
1589
|
+
context.replay.steps = new Map(steps.map((step) => [step.stepName, step]))
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
private endReplay(runId: string): void {
|
|
1594
|
+
const context = this.runContexts.get(runId)
|
|
1595
|
+
if (!context) return
|
|
1596
|
+
context.replay = undefined
|
|
1597
|
+
this.releaseContext(runId)
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/**
|
|
1601
|
+
* The step row for `stepName`, creating it if the run has not reached it
|
|
1602
|
+
* before. Served from the replay snapshot when one is loaded.
|
|
1603
|
+
*/
|
|
1604
|
+
private async loadOrCreateStep(
|
|
1605
|
+
runId: string,
|
|
1606
|
+
stepName: string,
|
|
1607
|
+
create: () => Promise<StepState>
|
|
1608
|
+
): Promise<StepState> {
|
|
1609
|
+
const snapshot = this.runContexts.get(runId)?.replay?.steps
|
|
1610
|
+
if (snapshot) {
|
|
1611
|
+
const cached = snapshot.get(stepName)
|
|
1612
|
+
if (cached) {
|
|
1613
|
+
return cached
|
|
1614
|
+
}
|
|
1615
|
+
} else {
|
|
1616
|
+
try {
|
|
1617
|
+
return await this.getStepState(runId, stepName)
|
|
1618
|
+
} catch {
|
|
1619
|
+
// No row yet — fall through and create it.
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1449
1622
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1623
|
+
let step: StepState
|
|
1624
|
+
try {
|
|
1625
|
+
step = await create()
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
// A concurrent replay of this run created the row after the snapshot was
|
|
1628
|
+
// taken. Its state is the truth; if it isn't really there, the insert
|
|
1629
|
+
// failed for its own reasons and that error is the one worth seeing.
|
|
1630
|
+
try {
|
|
1631
|
+
step = await this.getStepState(runId, stepName)
|
|
1632
|
+
} catch {
|
|
1633
|
+
throw error
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
snapshot?.set(stepName, step)
|
|
1637
|
+
return step
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* The run's immutable half — which workflow it is, the wire it was started
|
|
1642
|
+
* on, its input. `getRun` is otherwise called several times per step for
|
|
1643
|
+
* answers that were all fixed at creation, so a replay reads it once and
|
|
1644
|
+
* hands the same object to everyone who only needs that half.
|
|
1645
|
+
*
|
|
1646
|
+
* Anyone who needs `status`, `output`, `error` or `state` must call `getRun`:
|
|
1647
|
+
* those move while the run executes, and a cached copy would be a lie.
|
|
1648
|
+
*/
|
|
1649
|
+
private async getRunIdentity(runId: string): Promise<WorkflowRun | null> {
|
|
1650
|
+
const replay = this.runContexts.get(runId)?.replay
|
|
1651
|
+
if (replay?.run) {
|
|
1652
|
+
return replay.run
|
|
1653
|
+
}
|
|
1654
|
+
const run = await this.getRun(runId)
|
|
1655
|
+
if (run && replay) {
|
|
1656
|
+
replay.run = run
|
|
1657
|
+
}
|
|
1658
|
+
return run
|
|
1453
1659
|
}
|
|
1454
1660
|
|
|
1455
1661
|
/** The step the DSL walk last reached (the predecessor for the next step). */
|
|
1456
1662
|
private lastStepName(runId: string): string | undefined {
|
|
1457
|
-
return this.
|
|
1663
|
+
return this.runContexts.get(runId)?.replay?.lastStep
|
|
1458
1664
|
}
|
|
1459
1665
|
|
|
1460
1666
|
/**
|
|
@@ -1464,26 +1670,26 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1464
1670
|
* the rows clobbering. Deterministic given a deterministic DSL body.
|
|
1465
1671
|
*/
|
|
1466
1672
|
private nextStepKey(runId: string, logicalStepName: string): string {
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
perRun.set(logicalStepName, ordinal + 1)
|
|
1673
|
+
const context = this.contextFor(runId)
|
|
1674
|
+
const replay: NonNullable<RunContext['replay']> = (context.replay ??= {
|
|
1675
|
+
ordinals: new Map(),
|
|
1676
|
+
})
|
|
1677
|
+
const ordinal = replay.ordinals.get(logicalStepName) ?? 0
|
|
1678
|
+
replay.ordinals.set(logicalStepName, ordinal + 1)
|
|
1474
1679
|
const stepName =
|
|
1475
1680
|
ordinal === 0 ? logicalStepName : `${logicalStepName}#${ordinal}`
|
|
1476
|
-
|
|
1681
|
+
replay.lastStep = stepName
|
|
1477
1682
|
return stepName
|
|
1478
1683
|
}
|
|
1479
1684
|
|
|
1480
1685
|
public async runWorkflowJob(runId: string, rpcService: any): Promise<void> {
|
|
1481
|
-
// Fresh ordinal counters per replay so step keys are deterministic
|
|
1482
|
-
|
|
1686
|
+
// Fresh ordinal counters per replay so step keys are deterministic, and one
|
|
1687
|
+
// read of the steps the run has already taken.
|
|
1688
|
+
await this.beginReplay(runId)
|
|
1483
1689
|
try {
|
|
1484
1690
|
await this.runWorkflowJobInner(runId, rpcService)
|
|
1485
1691
|
} finally {
|
|
1486
|
-
this.
|
|
1692
|
+
this.endReplay(runId)
|
|
1487
1693
|
}
|
|
1488
1694
|
}
|
|
1489
1695
|
|
|
@@ -1491,7 +1697,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1491
1697
|
runId: string,
|
|
1492
1698
|
rpcService: any
|
|
1493
1699
|
): Promise<void> {
|
|
1494
|
-
|
|
1700
|
+
// Caches the run for the rest of this replay, so the steps it walks don't
|
|
1701
|
+
// each re-read the workflow name and wire it already has.
|
|
1702
|
+
const run = await this.getRunIdentity(runId)
|
|
1495
1703
|
if (!run) {
|
|
1496
1704
|
throw new WorkflowRunNotFoundError(runId)
|
|
1497
1705
|
}
|
|
@@ -1509,10 +1717,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1509
1717
|
return
|
|
1510
1718
|
}
|
|
1511
1719
|
|
|
1512
|
-
if (
|
|
1513
|
-
workflowMeta?.source === 'graph' ||
|
|
1514
|
-
workflowMeta?.source === 'dynamic-workflow'
|
|
1515
|
-
) {
|
|
1720
|
+
if (workflowMeta?.source === 'graph') {
|
|
1516
1721
|
await continueGraph(this, runId, run.workflow)
|
|
1517
1722
|
const updatedRun = await this.getRun(runId)
|
|
1518
1723
|
if (updatedRun?.status === 'completed') {
|
|
@@ -1529,29 +1734,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1529
1734
|
return
|
|
1530
1735
|
}
|
|
1531
1736
|
|
|
1532
|
-
if (!workflowMeta) {
|
|
1533
|
-
const dynamicWorkflows = await this.getAIGeneratedWorkflows()
|
|
1534
|
-
const match = dynamicWorkflows.find(
|
|
1535
|
-
(w) => w.workflowName === run.workflow
|
|
1536
|
-
)
|
|
1537
|
-
if (match?.graph) {
|
|
1538
|
-
await continueGraph(this, runId, run.workflow, match.graph)
|
|
1539
|
-
const updatedRun = await this.getRun(runId)
|
|
1540
|
-
if (updatedRun?.status === 'completed') {
|
|
1541
|
-
await this.onChildWorkflowCompleted(updatedRun, updatedRun.output)
|
|
1542
|
-
} else if (
|
|
1543
|
-
updatedRun?.status === 'failed' ||
|
|
1544
|
-
updatedRun?.status === 'cancelled'
|
|
1545
|
-
) {
|
|
1546
|
-
await this.onChildWorkflowFailed(
|
|
1547
|
-
updatedRun,
|
|
1548
|
-
new Error(updatedRun.error?.message || 'Child workflow failed')
|
|
1549
|
-
)
|
|
1550
|
-
}
|
|
1551
|
-
return
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
1737
|
const registrations = pikkuState(pkgName, 'workflows', 'registrations')
|
|
1556
1738
|
const workflow = registrations.get(resolved?.resolvedName ?? run.workflow)
|
|
1557
1739
|
if (!workflow) {
|
|
@@ -1764,9 +1946,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1764
1946
|
const meta = pikkuState(null, 'workflows', 'meta')
|
|
1765
1947
|
const workflowMeta = meta[run.workflow]
|
|
1766
1948
|
|
|
1767
|
-
const isGraphWorkflow =
|
|
1768
|
-
workflowMeta?.source === 'graph' ||
|
|
1769
|
-
workflowMeta?.source === 'dynamic-workflow'
|
|
1949
|
+
const isGraphWorkflow = workflowMeta?.source === 'graph'
|
|
1770
1950
|
// Map the physical step key back to its logical node: a revisit instance
|
|
1771
1951
|
// is `node#N` (ordinal), which isn't a literal key in `nodes`.
|
|
1772
1952
|
let graphNodeId: string | undefined
|
|
@@ -1833,7 +2013,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1833
2013
|
stepState,
|
|
1834
2014
|
rpcName,
|
|
1835
2015
|
data,
|
|
1836
|
-
rpcService
|
|
2016
|
+
rpcService,
|
|
2017
|
+
run
|
|
1837
2018
|
)
|
|
1838
2019
|
}
|
|
1839
2020
|
}
|
|
@@ -1938,11 +2119,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1938
2119
|
stepState: StepState,
|
|
1939
2120
|
rpcName: string,
|
|
1940
2121
|
data: any,
|
|
1941
|
-
rpcService: any
|
|
2122
|
+
rpcService: any,
|
|
2123
|
+
knownRun?: WorkflowRun | null
|
|
1942
2124
|
): Promise<any> {
|
|
1943
2125
|
// Carry the run's pikkuUserId onto the step wire so authed steps rehydrate their
|
|
1944
2126
|
// session on the queued path too (the bare job wire lacks it; inline already has it).
|
|
1945
|
-
const run = await this.
|
|
2127
|
+
const run = knownRun ?? (await this.getRunIdentity(runId))
|
|
1946
2128
|
return rpcService.rpcWithWire(rpcName, data, {
|
|
1947
2129
|
...(run?.wire?.pikkuUserId ? { pikkuUserId: run.wire.pikkuUserId } : {}),
|
|
1948
2130
|
workflowStep: {
|
|
@@ -2052,13 +2234,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2052
2234
|
actor: stepOptions?.actor,
|
|
2053
2235
|
onError: stepOptions?.onError,
|
|
2054
2236
|
}
|
|
2055
|
-
//
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
stepState = await this.getStepState(runId, stepName)
|
|
2059
|
-
} catch {
|
|
2060
|
-
// Step doesn't exist - create it
|
|
2061
|
-
stepState = await this.insertStepState(
|
|
2237
|
+
// Reuse the step if the run already reached it, otherwise create it.
|
|
2238
|
+
const stepState = await this.loadOrCreateStep(runId, stepName, () =>
|
|
2239
|
+
this.insertStepState(
|
|
2062
2240
|
runId,
|
|
2063
2241
|
stepName,
|
|
2064
2242
|
rpcName,
|
|
@@ -2066,7 +2244,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2066
2244
|
resolvedStepOptions,
|
|
2067
2245
|
fromStepName
|
|
2068
2246
|
)
|
|
2069
|
-
|
|
2247
|
+
)
|
|
2070
2248
|
|
|
2071
2249
|
if (stepState.status === 'succeeded') {
|
|
2072
2250
|
// Return cached result
|
|
@@ -2160,24 +2338,17 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2160
2338
|
)
|
|
2161
2339
|
await this.setStepChildRunId(currentStepState.stepId, childRunId)
|
|
2162
2340
|
// Poll until child workflow completes
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
)
|
|
2173
|
-
}
|
|
2174
|
-
if (childRun.status === 'cancelled') {
|
|
2175
|
-
throw new Error('Sub-workflow was cancelled')
|
|
2176
|
-
}
|
|
2177
|
-
return childRun.output
|
|
2178
|
-
}
|
|
2179
|
-
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
2341
|
+
const childRun = await this.awaitRunEnd(
|
|
2342
|
+
childRunId,
|
|
2343
|
+
WORKFLOW_CHILD_POLL_MAX_MS
|
|
2344
|
+
)
|
|
2345
|
+
if (childRun.status === 'failed') {
|
|
2346
|
+
throw new Error(childRun.error?.message || 'Sub-workflow failed')
|
|
2347
|
+
}
|
|
2348
|
+
if (childRun.status === 'cancelled') {
|
|
2349
|
+
throw new Error('Sub-workflow was cancelled')
|
|
2180
2350
|
}
|
|
2351
|
+
return childRun.output
|
|
2181
2352
|
}
|
|
2182
2353
|
return this.invokeStepRpc(
|
|
2183
2354
|
runId,
|
|
@@ -2222,13 +2393,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2222
2393
|
): Promise<any> {
|
|
2223
2394
|
const fromStepName = this.lastStepName(runId)
|
|
2224
2395
|
const stepName = this.nextStepKey(runId, logicalStepName)
|
|
2225
|
-
//
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
} catch {
|
|
2230
|
-
// Step doesn't exist - create it (inline, so never dispatched)
|
|
2231
|
-
stepState = await this.insertStepState(
|
|
2396
|
+
// Reuse the step if the run already reached it, otherwise create it
|
|
2397
|
+
// (inline, so never dispatched).
|
|
2398
|
+
const stepState = await this.loadOrCreateStep(runId, stepName, () =>
|
|
2399
|
+
this.insertStepState(
|
|
2232
2400
|
runId,
|
|
2233
2401
|
stepName,
|
|
2234
2402
|
rpcName,
|
|
@@ -2236,7 +2404,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2236
2404
|
stepOptions,
|
|
2237
2405
|
fromStepName
|
|
2238
2406
|
)
|
|
2239
|
-
|
|
2407
|
+
)
|
|
2240
2408
|
|
|
2241
2409
|
if (stepState.status === 'succeeded') {
|
|
2242
2410
|
// Return cached result
|
|
@@ -2288,13 +2456,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2288
2456
|
) {
|
|
2289
2457
|
const fromStepName = this.lastStepName(runId)
|
|
2290
2458
|
const stepName = this.nextStepKey(runId, logicalStepName)
|
|
2291
|
-
//
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
} catch {
|
|
2296
|
-
// Step doesn't exist - create it (sleep step, no RPC)
|
|
2297
|
-
stepState = await this.insertStepState(
|
|
2459
|
+
// Reuse the step if the run already reached it, otherwise create it
|
|
2460
|
+
// (sleep step, no RPC).
|
|
2461
|
+
const stepState = await this.loadOrCreateStep(runId, stepName, () =>
|
|
2462
|
+
this.insertStepState(
|
|
2298
2463
|
runId,
|
|
2299
2464
|
stepName,
|
|
2300
2465
|
null,
|
|
@@ -2302,7 +2467,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2302
2467
|
undefined,
|
|
2303
2468
|
fromStepName
|
|
2304
2469
|
)
|
|
2305
|
-
|
|
2470
|
+
)
|
|
2306
2471
|
|
|
2307
2472
|
if (stepState.status === 'succeeded') {
|
|
2308
2473
|
// Sleep already completed, return immediately
|