@bratsos/workflow-engine 0.5.1 → 0.7.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.
Files changed (41) hide show
  1. package/README.md +65 -12
  2. package/dist/{chunk-RZY5YRGL.js → chunk-2HEV5ZJL.js} +2 -2
  3. package/dist/chunk-2HEV5ZJL.js.map +1 -0
  4. package/dist/{chunk-WQPZ6KON.js → chunk-5C7LRNM7.js} +280 -93
  5. package/dist/chunk-5C7LRNM7.js.map +1 -0
  6. package/dist/{chunk-PHLNTR5Z.js → chunk-Q2XDO3UF.js} +28 -7
  7. package/dist/chunk-Q2XDO3UF.js.map +1 -0
  8. package/dist/{chunk-ZYMT2PAO.js → chunk-WWK2SPN7.js} +16 -37
  9. package/dist/chunk-WWK2SPN7.js.map +1 -0
  10. package/dist/{client-oLD5ilXp.d.ts → client-DYs5wlHp.d.ts} +17 -99
  11. package/dist/client.d.ts +4 -3
  12. package/dist/client.js +1 -1
  13. package/dist/events-D_P24UaY.d.ts +105 -0
  14. package/dist/{index-CVkkGnxx.d.ts → index-aNuJ2QgN.d.ts} +11 -1
  15. package/dist/index.d.ts +184 -32
  16. package/dist/index.js +41 -9
  17. package/dist/index.js.map +1 -1
  18. package/dist/{interface-TsryH4d7.d.ts → interface-BeEPzTFy.d.ts} +9 -3
  19. package/dist/kernel/index.d.ts +6 -5
  20. package/dist/kernel/index.js +2 -1
  21. package/dist/kernel/testing/index.d.ts +3 -2
  22. package/dist/persistence/index.d.ts +2 -2
  23. package/dist/persistence/index.js +2 -2
  24. package/dist/persistence/prisma/index.d.ts +2 -2
  25. package/dist/persistence/prisma/index.js +2 -2
  26. package/dist/{plugins-C94AT8Wr.d.ts → plugins-Cl0WVVrE.d.ts} +9 -6
  27. package/dist/{ports-855bktyD.d.ts → ports-swhiWFw4.d.ts} +5 -106
  28. package/dist/{stage-BPw7m9Wx.d.ts → stage-_7BKqqUG.d.ts} +2 -2
  29. package/dist/testing/index.d.ts +2 -1
  30. package/dist/testing/index.js +25 -6
  31. package/dist/testing/index.js.map +1 -1
  32. package/package.json +1 -1
  33. package/skills/workflow-engine/SKILL.md +30 -11
  34. package/skills/workflow-engine/references/02-workflow-builder.md +2 -0
  35. package/skills/workflow-engine/references/03-runtime-setup.md +1 -1
  36. package/skills/workflow-engine/references/08-common-patterns.md +2 -1
  37. package/skills/workflow-engine/references/09-troubleshooting.md +4 -3
  38. package/dist/chunk-PHLNTR5Z.js.map +0 -1
  39. package/dist/chunk-RZY5YRGL.js.map +0 -1
  40. package/dist/chunk-WQPZ6KON.js.map +0 -1
  41. package/dist/chunk-ZYMT2PAO.js.map +0 -1
package/README.md CHANGED
@@ -24,6 +24,7 @@ A **type-safe, distributed workflow engine** for AI-orchestrated processes. Feat
24
24
  - [Common Patterns](#common-patterns)
25
25
  - [Accessing Previous Stage Output](#accessing-previous-stage-output)
26
26
  - [Parallel Execution](#parallel-execution)
27
+ - [Stage ID Utilities](#stage-id-utilities)
27
28
  - [AI Integration](#ai-integration)
28
29
  - [Long-Running Batch Jobs](#long-running-batch-jobs)
29
30
  - [Config Presets](#config-presets)
@@ -110,6 +111,7 @@ model WorkflowRun {
110
111
  id String @id @default(cuid())
111
112
  createdAt DateTime @default(now())
112
113
  updatedAt DateTime @updatedAt
114
+ version Int @default(1)
113
115
  workflowId String
114
116
  workflowName String
115
117
  workflowType String
@@ -137,6 +139,7 @@ model WorkflowStage {
137
139
  id String @id @default(cuid())
138
140
  createdAt DateTime @default(now())
139
141
  updatedAt DateTime @updatedAt
142
+ version Int @default(1)
140
143
  workflowRunId String
141
144
  workflowRun WorkflowRun @relation(fields: [workflowRunId], references: [id], onDelete: Cascade)
142
145
  stageId String
@@ -219,7 +222,7 @@ model JobQueue {
219
222
  stageId String
220
223
  status Status @default(PENDING)
221
224
  priority Int @default(5)
222
- attempt Int @default(1)
225
+ attempt Int @default(0)
223
226
  maxAttempts Int @default(3)
224
227
  workerId String?
225
228
  lockedAt DateTime?
@@ -404,17 +407,27 @@ A stage is the atomic unit of work. Every stage has typed input, output, and con
404
407
 
405
408
  ### Workflows
406
409
 
407
- A workflow is a directed graph of stages built using the fluent `WorkflowBuilder` API:
410
+ Workflows are built as a linear pipeline of **execution groups**. Each group contains one or more stages. Sequential stages (`.pipe()`) form single-stage groups. Parallel stages (`.parallel()`) form multi-stage groups where all stages run concurrently.
408
411
 
409
412
  ```typescript
410
413
  new WorkflowBuilder(id, name, description, inputSchema, outputSchema)
411
- .pipe(stageA) // Sequential: stageA runs first
412
- .pipe(stageB) // Sequential: stageB runs after stageA
413
- .parallel([stageC, stageD]) // Parallel: stageC and stageD run concurrently
414
- .pipe(stageE) // Sequential: stageE runs after both complete
414
+ .pipe(stageA) // Group 0: stageA runs first
415
+ .pipe(stageB) // Group 1: stageB runs after stageA
416
+ .parallel([stageC, stageD]) // Group 2: stageC and stageD run concurrently
417
+ .pipe(stageE) // Group 3: stageE runs after both complete
415
418
  .build();
416
419
  ```
417
420
 
421
+ The output of each execution group is stored in the workflow context keyed by stage ID. For parallel groups, the merged output is an object keyed by each stage's ID:
422
+
423
+ ```typescript
424
+ // After group 2 completes, stageE receives:
425
+ ctx.require("stageC") // output of stageC
426
+ ctx.require("stageD") // output of stageD
427
+ ```
428
+
429
+ When a workflow completes, the final execution group's output is persisted in `WorkflowRun.output` and included in the `workflow:completed` event.
430
+
418
431
  ### Kernel
419
432
 
420
433
  The `Kernel` is a pure command dispatcher. All operations are expressed as typed commands:
@@ -504,16 +517,42 @@ export const analyzeStage = defineStage({
504
517
 
505
518
  ### Parallel Execution
506
519
 
520
+ Parallel stages run concurrently in the same execution group. Their outputs are keyed by stage ID in the workflow context:
521
+
507
522
  ```typescript
508
523
  const workflow = new WorkflowBuilder(/* ... */)
509
524
  .pipe(extractStage)
510
525
  .parallel([
511
- sentimentAnalysisStage,
512
- keywordExtractionStage,
513
- languageDetectionStage,
526
+ sentimentAnalysisStage, // id: "sentiment"
527
+ keywordExtractionStage, // id: "keywords"
528
+ languageDetectionStage, // id: "language"
514
529
  ])
515
530
  .pipe(aggregateResultsStage)
516
531
  .build();
532
+
533
+ // In aggregateResultsStage:
534
+ async execute(ctx) {
535
+ const sentiment = ctx.require("sentiment"); // output of sentimentAnalysisStage
536
+ const keywords = ctx.require("keywords"); // output of keywordExtractionStage
537
+ const language = ctx.require("language"); // output of languageDetectionStage
538
+ // ...
539
+ }
540
+ ```
541
+
542
+ ### Stage ID Utilities
543
+
544
+ Use `createStageIds` or `defineStageIds` for type-safe stage ID constants with autocomplete:
545
+
546
+ ```typescript
547
+ import { createStageIds, defineStageIds } from "@bratsos/workflow-engine";
548
+
549
+ // From an existing workflow
550
+ const STAGES = createStageIds(myWorkflow);
551
+ STAGES.EXTRACT_TEXT // "extract-text" (autocomplete + type-safe)
552
+ STAGES.SUMMARIZE // "summarize"
553
+
554
+ // Or define upfront
555
+ const STAGES = defineStageIds(["extract-text", "summarize"] as const);
517
556
  ```
518
557
 
519
558
  ### AI Integration
@@ -558,7 +597,12 @@ export const batchStage = defineAsyncBatchStage({
558
597
  const batch = await submitBatch(ctx.input.prompts);
559
598
  return {
560
599
  suspended: true,
561
- state: { batchId: batch.id },
600
+ state: {
601
+ batchId: batch.id,
602
+ submittedAt: new Date().toISOString(),
603
+ pollInterval: 3600000,
604
+ maxWaitTime: 86400000,
605
+ },
562
606
  pollConfig: { pollInterval: 3600000, maxWaitTime: 86400000, nextPollAt: new Date(Date.now() + 3600000) },
563
607
  };
564
608
  },
@@ -640,11 +684,12 @@ async execute(ctx) {
640
684
  | `run.create` | Create a new workflow run | `idempotencyKey`, `workflowId`, `input`, `config?`, `priority?` |
641
685
  | `run.claimPending` | Claim pending runs for processing | `workerId`, `maxClaims?` |
642
686
  | `run.transition` | Advance to next stage group | `workflowRunId` |
643
- | `run.cancel` | Cancel a running workflow | `workflowRunId`, `reason?` |
644
- | `run.rerunFrom` | Rerun from a specific stage | `workflowRunId`, `fromStageId` |
687
+ | `run.cancel` | Cancel a running workflow (cascades to stages + jobs) | `workflowRunId`, `reason?` |
688
+ | `run.rerunFrom` | Rerun from a specific stage (cleans up artifacts) | `workflowRunId`, `fromStageId` |
645
689
  | `job.execute` | Execute a single stage (multi-phase transactions) | `idempotencyKey?`, `workflowRunId`, `workflowId`, `stageId`, `config` |
646
690
  | `stage.pollSuspended` | Poll suspended stages (per-stage transactions) | `maxChecks?` (returns `resumedWorkflowRunIds`) |
647
691
  | `lease.reapStale` | Release stale job leases | `staleThresholdMs` |
692
+ | `run.reapStuck` | Fail runs stuck RUNNING with no activity | `stuckThresholdMs?` |
648
693
  | `outbox.flush` | Publish pending events | `maxEvents?` |
649
694
  | `plugin.replayDLQ` | Replay dead-letter queue events | `maxEvents?` |
650
695
 
@@ -657,6 +702,11 @@ Transaction behavior:
657
702
  - `job.execute` uses multi-phase transactions: Phase 1 commits `RUNNING` status immediately, Phase 2 runs `stageDef.execute()` outside any transaction, Phase 3 commits the final status. This avoids holding a database connection during long-running stage execution.
658
703
  - `stage.pollSuspended` uses per-stage transactions: `checkCompletion()` runs outside any transaction (external HTTP calls to batch providers), then DB updates + outbox events are committed in a short transaction per stage. This prevents P2028 timeout errors when batch APIs are slow.
659
704
 
705
+ Cancellation semantics:
706
+ - `run.cancel` is **authoritative**: it marks the run as `CANCELLED`, cascades to all non-terminal stages (setting them to `CANCELLED` and clearing `nextPollAt`), and cancels all queued/suspended jobs via `jobTransport.cancelByRun()`.
707
+ - `stage.pollSuspended` skips stages whose run has been cancelled.
708
+ - `job.execute` re-checks run status after stage execution. If the run was cancelled during execution, the result is discarded and a `ghost: true` flag is returned. Hosts use this flag to prevent retries.
709
+
660
710
  ### Node Host Config
661
711
 
662
712
  | Option | Type | Default | Description |
@@ -703,6 +753,9 @@ import { createPrismaWorkflowPersistence, createPrismaJobQueue, createPrismaAICa
703
753
  // AI Helper
704
754
  import { createAIHelper, type AIHelper } from "@bratsos/workflow-engine";
705
755
 
756
+ // Stage ID utilities
757
+ import { createStageIds, defineStageIds, isValidStageId, assertValidStageId } from "@bratsos/workflow-engine";
758
+
706
759
  // Testing
707
760
  import { InMemoryWorkflowPersistence, InMemoryJobQueue } from "@bratsos/workflow-engine/testing";
708
761
  import { FakeClock, InMemoryBlobStore, CollectingEventSink, NoopScheduler } from "@bratsos/workflow-engine/kernel/testing";
@@ -13,5 +13,5 @@ var StaleVersionError = class extends Error {
13
13
  };
14
14
 
15
15
  export { StaleVersionError };
16
- //# sourceMappingURL=chunk-RZY5YRGL.js.map
17
- //# sourceMappingURL=chunk-RZY5YRGL.js.map
16
+ //# sourceMappingURL=chunk-2HEV5ZJL.js.map
17
+ //# sourceMappingURL=chunk-2HEV5ZJL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/persistence/interface.ts"],"names":[],"mappings":";AAkDO,IAAM,iBAAA,GAAN,cAAgC,KAAA,CAAM;AAAA,EAC3C,WAAA,CACkB,MAAA,EACA,EAAA,EACA,QAAA,EACA,MAAA,EAChB;AACA,IAAA,KAAA;AAAA,MACE,oBAAoB,MAAM,CAAA,CAAA,EAAI,EAAE,CAAA,WAAA,EAAc,QAAQ,SAAS,MAAM,CAAA;AAAA,KACvE;AAPgB,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AACA,IAAA,IAAA,CAAA,EAAA,GAAA,EAAA;AACA,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AACA,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAKhB,IAAA,IAAA,CAAK,IAAA,GAAO,mBAAA;AAAA,EACd;AACF","file":"chunk-2HEV5ZJL.js","sourcesContent":["/**\n * Persistence Interfaces for Workflow Engine\n *\n * These interfaces abstract database operations to enable:\n * - Testing with mock implementations\n * - Future extraction into @bratsos/workflow-engine package\n * - Alternative database backends\n *\n * Implementations:\n * - PrismaWorkflowPersistence (default, in ./prisma/)\n * - InMemoryPersistence (for testing)\n */\n\n// ============================================================================\n// Unified Status Type\n// ============================================================================\n\n/**\n * Unified status type for workflows, stages, and jobs.\n *\n * - PENDING: Not started yet\n * - RUNNING: Currently executing\n * - SUSPENDED: Paused, waiting for external event (e.g., batch job completion)\n * - COMPLETED: Finished successfully\n * - FAILED: Finished with error\n * - CANCELLED: Manually stopped by user\n * - SKIPPED: Stage-specific - bypassed due to condition\n */\nexport type Status =\n | \"PENDING\"\n | \"RUNNING\"\n | \"SUSPENDED\"\n | \"COMPLETED\"\n | \"FAILED\"\n | \"CANCELLED\"\n | \"SKIPPED\";\n\n/** @deprecated Use Status instead */\nexport type WorkflowStatus = Status;\n\n/** @deprecated Use Status instead */\nexport type WorkflowStageStatus = Status;\n\n/** @deprecated Use Status instead. Note: PROCESSING is now RUNNING. */\nexport type JobStatus = Status;\n\nexport type LogLevel = \"DEBUG\" | \"INFO\" | \"WARN\" | \"ERROR\";\n\nexport type ArtifactType = \"STAGE_OUTPUT\" | \"ARTIFACT\" | \"METADATA\";\n\nexport class StaleVersionError extends Error {\n constructor(\n public readonly entity: string,\n public readonly id: string,\n public readonly expected: number,\n public readonly actual: number,\n ) {\n super(\n `Stale version on ${entity} ${id}: expected ${expected}, got ${actual}`,\n );\n this.name = \"StaleVersionError\";\n }\n}\n\n// ============================================================================\n// Record Types (minimal fields needed by the workflow engine)\n// ============================================================================\n\nexport interface WorkflowRunRecord {\n id: string;\n createdAt: Date;\n updatedAt: Date;\n version: number;\n workflowId: string;\n workflowName: string;\n workflowType: string;\n status: WorkflowStatus;\n startedAt: Date | null;\n completedAt: Date | null;\n duration: number | null;\n input: unknown;\n output: unknown | null;\n config: unknown;\n totalCost: number;\n totalTokens: number;\n priority: number;\n metadata: unknown | null;\n}\n\nexport interface WorkflowStageRecord {\n id: string;\n createdAt: Date;\n updatedAt: Date;\n version: number;\n workflowRunId: string;\n stageId: string;\n stageName: string;\n stageNumber: number;\n executionGroup: number;\n status: WorkflowStageStatus;\n startedAt: Date | null;\n completedAt: Date | null;\n duration: number | null;\n inputData: unknown | null;\n outputData: unknown | null;\n config: unknown | null;\n suspendedState: unknown | null;\n resumeData: unknown | null;\n nextPollAt: Date | null;\n pollInterval: number | null;\n maxWaitUntil: Date | null;\n metrics: unknown | null;\n embeddingInfo: unknown | null;\n errorMessage: string | null;\n}\n\nexport interface WorkflowLogRecord {\n id: string;\n createdAt: Date;\n workflowStageId: string | null;\n workflowRunId: string | null;\n level: LogLevel;\n message: string;\n metadata: unknown | null;\n}\n\nexport interface WorkflowArtifactRecord {\n id: string;\n createdAt: Date;\n updatedAt: Date;\n workflowRunId: string;\n workflowStageId: string | null;\n key: string;\n type: ArtifactType;\n data: unknown;\n size: number;\n metadata: unknown | null;\n}\n\n// ============================================================================\n// Outbox and Idempotency Record Types (for kernel transactional outbox)\n// ============================================================================\n\nexport interface OutboxRecord {\n id: string;\n workflowRunId: string;\n sequence: number;\n eventType: string;\n payload: unknown;\n causationId: string;\n occurredAt: Date;\n publishedAt: Date | null;\n retryCount: number;\n dlqAt: Date | null;\n}\n\nexport interface CreateOutboxEventInput {\n workflowRunId: string;\n eventType: string;\n payload: unknown;\n causationId: string;\n occurredAt: Date;\n}\n\nexport interface IdempotencyRecord {\n key: string;\n commandType: string;\n result: unknown;\n createdAt: Date;\n}\n\n// ============================================================================\n// AI Call Record Types\n// ============================================================================\n\nexport interface AICallRecord {\n id: string;\n createdAt: Date;\n topic: string;\n callType: string;\n modelKey: string;\n modelId: string;\n prompt: string;\n response: string;\n inputTokens: number;\n outputTokens: number;\n cost: number;\n metadata: unknown | null;\n}\n\nexport interface JobRecord {\n id: string;\n createdAt: Date;\n updatedAt: Date;\n workflowRunId: string;\n workflowId: string;\n stageId: string;\n status: JobStatus;\n priority: number;\n workerId: string | null;\n lockedAt: Date | null;\n startedAt: Date | null;\n completedAt: Date | null;\n attempt: number;\n maxAttempts: number;\n lastError: string | null;\n nextPollAt: Date | null;\n payload: Record<string, unknown>;\n}\n\n// ============================================================================\n// Input Types (for creating/updating records)\n// ============================================================================\n\nexport interface CreateRunInput {\n id?: string;\n workflowId: string;\n workflowName: string;\n workflowType: string;\n input: unknown;\n config?: unknown;\n priority?: number;\n /** Optional metadata stored as JSON on the run record. NOT spread into Prisma fields. */\n metadata?: Record<string, unknown>;\n}\n\nexport interface UpdateRunInput {\n status?: WorkflowStatus;\n startedAt?: Date;\n completedAt?: Date | null;\n duration?: number | null;\n output?: unknown;\n totalCost?: number;\n totalTokens?: number;\n expectedVersion?: number;\n}\n\nexport interface CreateStageInput {\n workflowRunId: string;\n stageId: string;\n stageName: string;\n stageNumber: number;\n executionGroup: number;\n status?: WorkflowStageStatus;\n startedAt?: Date;\n config?: unknown;\n inputData?: unknown;\n}\n\nexport interface UpdateStageInput {\n status?: WorkflowStageStatus;\n startedAt?: Date;\n completedAt?: Date;\n duration?: number;\n outputData?: unknown;\n config?: unknown;\n suspendedState?: unknown;\n resumeData?: unknown;\n nextPollAt?: Date | null;\n pollInterval?: number;\n maxWaitUntil?: Date;\n metrics?: unknown;\n embeddingInfo?: unknown;\n artifacts?: unknown;\n errorMessage?: string;\n expectedVersion?: number;\n}\n\nexport interface UpsertStageInput {\n workflowRunId: string;\n stageId: string;\n create: CreateStageInput;\n update: UpdateStageInput;\n}\n\nexport interface CreateLogInput {\n workflowRunId?: string;\n workflowStageId?: string;\n level: LogLevel;\n message: string;\n metadata?: unknown;\n}\n\nexport interface SaveArtifactInput {\n workflowRunId: string;\n workflowStageId?: string;\n key: string;\n type: ArtifactType;\n data: unknown;\n size: number;\n metadata?: unknown;\n}\n\nexport interface CreateAICallInput {\n topic: string;\n callType: string;\n modelKey: string;\n modelId: string;\n prompt: string;\n response: string;\n inputTokens: number;\n outputTokens: number;\n cost: number;\n metadata?: unknown;\n}\n\nexport interface EnqueueJobInput {\n workflowRunId: string;\n workflowId: string;\n stageId: string;\n priority?: number;\n payload?: Record<string, unknown>;\n scheduledFor?: Date;\n}\n\nexport interface DequeueResult {\n jobId: string;\n workflowRunId: string;\n workflowId: string;\n stageId: string;\n priority: number;\n attempt: number;\n maxAttempts: number;\n payload: Record<string, unknown>;\n}\n\n// ============================================================================\n// WorkflowPersistence Interface\n// ============================================================================\n\nexport interface WorkflowPersistence {\n /** Execute operations within a transaction boundary. */\n withTransaction<T>(fn: (tx: WorkflowPersistence) => Promise<T>): Promise<T>;\n\n // WorkflowRun operations\n createRun(data: CreateRunInput): Promise<WorkflowRunRecord>;\n updateRun(id: string, data: UpdateRunInput): Promise<void>;\n getRun(id: string): Promise<WorkflowRunRecord | null>;\n getRunStatus(id: string): Promise<WorkflowStatus | null>;\n getRunsByStatus(status: WorkflowStatus): Promise<WorkflowRunRecord[]>;\n getStuckRuns(stuckSince: Date): Promise<WorkflowRunRecord[]>;\n\n /**\n * Atomically claim a pending workflow run for processing.\n * Uses atomic update with WHERE status = 'PENDING' to prevent race conditions.\n *\n * @param id - The workflow run ID to claim\n * @returns true if successfully claimed, false if already claimed by another worker\n */\n claimPendingRun(id: string): Promise<boolean>;\n\n /**\n * Atomically find and claim the next pending workflow run.\n * Uses FOR UPDATE SKIP LOCKED pattern (in Postgres) to prevent race conditions\n * when multiple workers try to claim workflows simultaneously.\n *\n * Priority ordering: higher priority first, then oldest (FIFO within same priority).\n *\n * @returns The claimed workflow run (now with status RUNNING), or null if no pending runs\n */\n claimNextPendingRun(): Promise<WorkflowRunRecord | null>;\n\n // WorkflowStage operations\n createStage(data: CreateStageInput): Promise<WorkflowStageRecord>;\n upsertStage(data: UpsertStageInput): Promise<WorkflowStageRecord>;\n updateStage(id: string, data: UpdateStageInput): Promise<void>;\n updateStageByRunAndStageId(\n workflowRunId: string,\n stageId: string,\n data: UpdateStageInput,\n ): Promise<void>;\n getStage(runId: string, stageId: string): Promise<WorkflowStageRecord | null>;\n getStageById(id: string): Promise<WorkflowStageRecord | null>;\n getStagesByRun(\n runId: string,\n options?: { status?: WorkflowStageStatus; orderBy?: \"asc\" | \"desc\" },\n ): Promise<WorkflowStageRecord[]>;\n getSuspendedStages(beforeDate: Date): Promise<WorkflowStageRecord[]>;\n getFirstSuspendedStageReadyToResume(\n runId: string,\n ): Promise<WorkflowStageRecord | null>;\n getFirstFailedStage(runId: string): Promise<WorkflowStageRecord | null>;\n getLastCompletedStage(runId: string): Promise<WorkflowStageRecord | null>;\n getLastCompletedStageBefore(\n runId: string,\n executionGroup: number,\n ): Promise<WorkflowStageRecord | null>;\n deleteStage(id: string): Promise<void>;\n\n // WorkflowLog operations\n createLog(data: CreateLogInput): Promise<void>;\n\n // WorkflowArtifact operations (for StageStorage)\n saveArtifact(data: SaveArtifactInput): Promise<void>;\n loadArtifact(runId: string, key: string): Promise<unknown>;\n hasArtifact(runId: string, key: string): Promise<boolean>;\n deleteArtifact(runId: string, key: string): Promise<void>;\n listArtifacts(runId: string): Promise<WorkflowArtifactRecord[]>;\n getStageIdForArtifact(runId: string, stageId: string): Promise<string | null>;\n\n // Stage output convenience methods (replaces separate StageStorage)\n saveStageOutput(\n runId: string,\n workflowType: string,\n stageId: string,\n output: unknown,\n ): Promise<string>;\n\n // Outbox DLQ operations\n /** Increment retry count for a failed outbox event. Returns new count. */\n incrementOutboxRetryCount(id: string): Promise<number>;\n\n /** Move an outbox event to DLQ (sets dlqAt). */\n moveOutboxEventToDLQ(id: string): Promise<void>;\n\n /** Reset DLQ events so they can be reprocessed by outbox.flush. Returns count reset. */\n replayDLQEvents(maxEvents: number): Promise<number>;\n\n // Outbox operations\n /** Write events to the outbox. Sequences are auto-assigned per workflowRunId. */\n appendOutboxEvents(events: CreateOutboxEventInput[]): Promise<void>;\n\n /** Read unpublished events ordered by (workflowRunId, sequence). */\n getUnpublishedOutboxEvents(limit?: number): Promise<OutboxRecord[]>;\n\n /** Mark events as published. */\n markOutboxEventsPublished(ids: string[]): Promise<void>;\n\n // Idempotency operations\n /** Atomically acquire an idempotency key for command execution. */\n acquireIdempotencyKey(\n key: string,\n commandType: string,\n ): Promise<\n | { status: \"acquired\" }\n | { status: \"replay\"; result: unknown }\n | { status: \"in_progress\" }\n >;\n\n /** Mark an idempotency key as completed and cache the command result. */\n completeIdempotencyKey(\n key: string,\n commandType: string,\n result: unknown,\n ): Promise<void>;\n\n /** Release an in-progress idempotency key after command failure. */\n releaseIdempotencyKey(key: string, commandType: string): Promise<void>;\n}\n\n// ============================================================================\n// AICallLogger Interface\n// ============================================================================\n\nexport interface AIHelperStats {\n totalCalls: number;\n totalInputTokens: number;\n totalOutputTokens: number;\n totalCost: number;\n perModel: Record<\n string,\n { calls: number; inputTokens: number; outputTokens: number; cost: number }\n >;\n}\n\nexport interface AICallLogger {\n /**\n * Log a single AI call (fire and forget)\n */\n logCall(call: CreateAICallInput): void;\n\n /**\n * Log batch results (for recording batch API results)\n */\n logBatchResults(batchId: string, results: CreateAICallInput[]): Promise<void>;\n\n /**\n * Get aggregated stats for a topic prefix\n */\n getStats(topicPrefix: string): Promise<AIHelperStats>;\n\n /**\n * Check if batch results are already recorded\n */\n isRecorded(batchId: string): Promise<boolean>;\n}\n\n// ============================================================================\n// JobQueue Interface\n// ============================================================================\n\nexport interface JobQueue {\n /**\n * Add a new job to the queue\n */\n enqueue(options: EnqueueJobInput): Promise<string>;\n\n /**\n * Enqueue multiple stages in parallel (same execution group)\n */\n enqueueParallel(jobs: EnqueueJobInput[]): Promise<string[]>;\n\n /**\n * Atomically dequeue the next available job\n */\n dequeue(): Promise<DequeueResult | null>;\n\n /**\n * Mark job as completed\n */\n complete(jobId: string): Promise<void>;\n\n /**\n * Mark job as suspended (for async-batch)\n */\n suspend(jobId: string, nextPollAt: Date): Promise<void>;\n\n /**\n * Mark job as failed\n */\n fail(jobId: string, error: string, shouldRetry?: boolean): Promise<void>;\n\n /**\n * Get suspended jobs that are ready to be checked\n */\n getSuspendedJobsReadyToPoll(): Promise<\n Array<{ jobId: string; stageId: string; workflowRunId: string }>\n >;\n\n /**\n * Release stale locks (for crashed workers)\n */\n releaseStaleJobs(staleThresholdMs?: number): Promise<number>;\n\n /**\n * Cancel all pending/suspended jobs for a workflow run.\n * Returns count of cancelled jobs.\n */\n cancelByRun(workflowRunId: string): Promise<number>;\n}\n\n// ============================================================================\n// Default Implementations (lazy loaded to avoid circular deps)\n// ============================================================================\n\n// Re-export from prisma implementations for convenience\n// These will be the default implementations used when no custom persistence is provided\n"]}