@strav/durable 0.4.31 → 1.0.0-alpha.8

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.
@@ -1,111 +0,0 @@
1
- import { transaction } from '@strav/database'
2
- import type { RunRow } from '../types.ts'
3
- import { writeJournal } from '../models/journal.ts'
4
- import { enqueueAdvance, enqueueCompensate } from './enqueue.ts'
5
- import { lockRun, type Tx } from './run_store.ts'
6
-
7
- /** Merge a result patch into a run's accumulated state. */
8
- export function applyPatch(
9
- state: Record<string, unknown>,
10
- patch: Record<string, unknown>
11
- ): Record<string, unknown> {
12
- return { ...state, ...patch }
13
- }
14
-
15
- /**
16
- * Transition a run into compensation, rolling back from the step before the
17
- * one that failed. If nothing was completed before it, fail the run directly.
18
- */
19
- export async function beginCompensation(
20
- trx: Tx,
21
- run: RunRow,
22
- failedStepIndex: number,
23
- failure: string
24
- ): Promise<void> {
25
- const cursor = failedStepIndex - 1
26
- if (cursor < 0) {
27
- await failRun(trx, run, failure)
28
- return
29
- }
30
- await trx`
31
- UPDATE "_strav_workflow_runs"
32
- SET "status" = 'compensating', "compensation_cursor" = ${cursor},
33
- "error" = ${failure}, "updated_at" = NOW()
34
- WHERE "id" = ${run.id}
35
- `
36
- await enqueueCompensate(trx, run.id, cursor)
37
- }
38
-
39
- /** Mark a run failed and, if it is a child, propagate failure to its parent. */
40
- export async function failRun(trx: Tx, run: RunRow, failure: string): Promise<void> {
41
- await trx`
42
- UPDATE "_strav_workflow_runs"
43
- SET "status" = 'failed', "error" = ${failure}, "updated_at" = NOW()
44
- WHERE "id" = ${run.id}
45
- `
46
- if (run.parentRunId != null) {
47
- await finalizeChildIntoParent(trx, run, 'failed')
48
- }
49
- }
50
-
51
- /**
52
- * Complete a run. Opens its own transaction (called as the terminal advance).
53
- * If the run is a child, fan in: write its result into the parent journal and
54
- * resume the parent — all in the same transaction, so the parent's
55
- * continuation job is visible only once the child result is.
56
- */
57
- export async function completeRun(runId: number): Promise<void> {
58
- await transaction(async (trx: Tx) => {
59
- const run = await lockRun(trx, runId)
60
- if (!run || run.status !== 'running') return
61
- await trx`
62
- UPDATE "_strav_workflow_runs"
63
- SET "status" = 'completed', "result" = ${JSON.stringify(run.state)},
64
- "updated_at" = NOW()
65
- WHERE "id" = ${runId}
66
- `
67
- if (run.parentRunId != null) {
68
- await finalizeChildIntoParent(trx, run, 'completed')
69
- }
70
- })
71
- }
72
-
73
- /**
74
- * Fan a finished child run into its parent. Locks the parent (child→parent
75
- * lock order is consistent everywhere, so no deadlock).
76
- */
77
- export async function finalizeChildIntoParent(
78
- trx: Tx,
79
- childRun: RunRow,
80
- outcome: 'completed' | 'failed'
81
- ): Promise<void> {
82
- const parentId = childRun.parentRunId
83
- const childStepId = childRun.parentStepId
84
- if (parentId == null || childStepId == null) return
85
-
86
- const parent = await lockRun(trx, parentId)
87
- if (!parent || parent.status !== 'suspended') return
88
-
89
- const parentStepIndex = parent.currentStep
90
-
91
- if (outcome === 'completed') {
92
- await writeJournal(trx, parentId, [
93
- { stepId: childStepId, status: 'completed', result: childRun.state, attempt: 1 },
94
- ])
95
- const newState = applyPatch(parent.state, { [childStepId]: childRun.state })
96
- const next = parentStepIndex + 1
97
- await trx`
98
- UPDATE "_strav_workflow_runs"
99
- SET "status" = 'running', "state" = ${JSON.stringify(newState)},
100
- "current_step" = ${next}, "updated_at" = NOW()
101
- WHERE "id" = ${parentId}
102
- `
103
- await enqueueAdvance(trx, parentId, next)
104
- } else {
105
- const failure = `child workflow "${childRun.workflowName}" failed`
106
- await writeJournal(trx, parentId, [
107
- { stepId: childStepId, status: 'failed', error: failure, attempt: 1 },
108
- ])
109
- await beginCompensation(trx, parent, parentStepIndex, failure)
110
- }
111
- }
@@ -1,20 +0,0 @@
1
- export { advanceHandler } from './advance_handler.ts'
2
- export type { AdvancePayload } from './advance_handler.ts'
3
- export { compensateHandler } from './compensate_handler.ts'
4
- export type { CompensatePayload } from './compensate_handler.ts'
5
- export { runDurableStep } from './step_driver.ts'
6
- export type { StepOutcome } from './step_driver.ts'
7
- export { runCompensator } from './compensation_driver.ts'
8
- export { buildContext } from './context.ts'
9
- export { isSuspendedRun } from './suspended_run.ts'
10
- export type { SuspendedRunLike } from './suspended_run.ts'
11
- export { enqueueAdvance, enqueueCompensate } from './enqueue.ts'
12
- export { loadRun, lockRun, hydrateRun } from './run_store.ts'
13
- export type { Tx } from './run_store.ts'
14
- export {
15
- applyPatch,
16
- beginCompensation,
17
- completeRun,
18
- failRun,
19
- finalizeChildIntoParent,
20
- } from './finalize.ts'
@@ -1,42 +0,0 @@
1
- import { sql } from '@strav/database'
2
- import type { RunRow } from '../types.ts'
3
- import { parseJson } from '../util.ts'
4
-
5
- /** A Bun SQL transaction handle (tagged-template callable). */
6
- export type Tx = (strings: TemplateStringsArray, ...values: unknown[]) => Promise<unknown>
7
-
8
- /** Hydrate a raw `_strav_workflow_runs` row into a typed `RunRow`. */
9
- export function hydrateRun(row: Record<string, unknown>): RunRow {
10
- return {
11
- id: Number(row.id),
12
- workflowName: row.workflow_name as string,
13
- input: parseJson<Record<string, unknown>>(row.input) ?? {},
14
- status: row.status as RunRow['status'],
15
- state: parseJson<Record<string, unknown>>(row.state) ?? {},
16
- currentStep: Number(row.current_step),
17
- compensationCursor:
18
- row.compensation_cursor == null ? null : Number(row.compensation_cursor),
19
- parentRunId: row.parent_run_id == null ? null : Number(row.parent_run_id),
20
- parentStepId: (row.parent_step_id as string | null) ?? null,
21
- awaitingSignal: (row.awaiting_signal as string | null) ?? null,
22
- wakeAt: (row.wake_at as Date | null) ?? null,
23
- error: (row.error as string | null) ?? null,
24
- result: parseJson<Record<string, unknown>>(row.result) ?? null,
25
- }
26
- }
27
-
28
- /** Load a run by id (unlocked read). */
29
- export async function loadRun(runId: number): Promise<RunRow | null> {
30
- const rows = (await sql`
31
- SELECT * FROM "_strav_workflow_runs" WHERE "id" = ${runId}
32
- `) as Record<string, unknown>[]
33
- return rows.length > 0 ? hydrateRun(rows[0]!) : null
34
- }
35
-
36
- /** Load a run `FOR UPDATE` inside a transaction (row-locked). */
37
- export async function lockRun(trx: Tx, runId: number): Promise<RunRow | null> {
38
- const rows = (await trx`
39
- SELECT * FROM "_strav_workflow_runs" WHERE "id" = ${runId} FOR UPDATE
40
- `) as Record<string, unknown>[]
41
- return rows.length > 0 ? hydrateRun(rows[0]!) : null
42
- }
@@ -1,291 +0,0 @@
1
- import type {
2
- DurableContext,
3
- DurableStep,
4
- JournalRecord,
5
- JournalWrite,
6
- } from '../types.ts'
7
- import { backoffDelay } from '../util.ts'
8
- import { isSuspendedRun } from './suspended_run.ts'
9
-
10
- /**
11
- * The result of executing one top-level step. The engine applies it
12
- * atomically in Phase B (a row-locked transaction).
13
- */
14
- export type StepOutcome =
15
- /** Step (and all sub-units) completed — journal results and move forward. */
16
- | { kind: 'advance'; journal: JournalWrite[]; resultPatch: Record<string, unknown> }
17
- /** A durable timer — journal, move forward, enqueue a delayed continuation. */
18
- | { kind: 'sleep'; journal: JournalWrite[]; resultPatch: Record<string, unknown>; wakeAt: Date }
19
- /** Suspend awaiting an external signal (human-in-the-loop). */
20
- | { kind: 'suspend-signal'; signal: string }
21
- /** Suspend on a brain agent `SuspendedRun` — resume re-enters this step. */
22
- | { kind: 'suspend-agent'; stepName: string; snapshot: unknown }
23
- /** Spawn a child workflow and wait for it. */
24
- | { kind: 'await-child'; childName: string; childInput: Record<string, unknown>; childStepId: string }
25
- /** Step failed but retries remain — re-enqueue the same step with backoff. */
26
- | { kind: 'retry'; journal: JournalWrite[]; attempt: number; backoffMs: number; failure: string }
27
- /** Step failed terminally — begin saga compensation. */
28
- | { kind: 'compensate'; journal: JournalWrite[]; failure: string }
29
-
30
- type RetryableStep = {
31
- name: string
32
- maxRetries: number
33
- retryBackoff: 'exponential' | 'linear'
34
- }
35
-
36
- function message(err: unknown): string {
37
- return err instanceof Error ? err.message : String(err)
38
- }
39
-
40
- /** Decide between retry and compensation when a step's handler throws. */
41
- function failureOutcome(
42
- step: RetryableStep,
43
- attempt: number,
44
- err: unknown,
45
- partialJournal: JournalWrite[]
46
- ): StepOutcome {
47
- const failure = message(err)
48
- if (attempt < step.maxRetries) {
49
- return {
50
- kind: 'retry',
51
- journal: partialJournal,
52
- attempt: attempt + 1,
53
- backoffMs: backoffDelay(attempt, step.retryBackoff),
54
- failure,
55
- }
56
- }
57
- return {
58
- kind: 'compensate',
59
- journal: [
60
- ...partialJournal,
61
- { stepId: step.name, status: 'failed', error: failure, attempt },
62
- ],
63
- failure,
64
- }
65
- }
66
-
67
- /**
68
- * Execute one top-level step against the journal. Completed sub-units
69
- * (parallel entries, loop iterations, the route decision) are read back from
70
- * the journal rather than re-run, so redelivery fast-forwards to the first
71
- * incomplete unit.
72
- */
73
- export async function runDurableStep(
74
- step: DurableStep,
75
- ctx: DurableContext,
76
- journal: Map<string, JournalRecord>
77
- ): Promise<StepOutcome> {
78
- switch (step.type) {
79
- case 'step':
80
- return runSequential(step, ctx)
81
- case 'parallel':
82
- return runParallel(step, ctx, journal)
83
- case 'route':
84
- return runRoute(step, ctx, journal)
85
- case 'loop':
86
- return runLoop(step, ctx, journal)
87
- case 'sleep':
88
- return runSleep(step)
89
- case 'signal':
90
- return { kind: 'suspend-signal', signal: step.signal }
91
- case 'child':
92
- return runChild(step, ctx)
93
- }
94
- }
95
-
96
- // ── step ────────────────────────────────────────────────────────────────────
97
-
98
- async function runSequential(
99
- step: Extract<DurableStep, { type: 'step' }>,
100
- ctx: DurableContext
101
- ): Promise<StepOutcome> {
102
- try {
103
- const result = await step.handler(ctx)
104
- if (isSuspendedRun(result)) {
105
- return { kind: 'suspend-agent', stepName: step.name, snapshot: result }
106
- }
107
- return {
108
- kind: 'advance',
109
- journal: [{ stepId: step.name, status: 'completed', result, attempt: ctx.attempt }],
110
- resultPatch: { [step.name]: result },
111
- }
112
- } catch (err) {
113
- return failureOutcome(step, ctx.attempt, err, [])
114
- }
115
- }
116
-
117
- // ── parallel ────────────────────────────────────────────────────────────────
118
-
119
- async function runParallel(
120
- step: Extract<DurableStep, { type: 'parallel' }>,
121
- ctx: DurableContext,
122
- journal: Map<string, JournalRecord>
123
- ): Promise<StepOutcome> {
124
- const settled = await Promise.all(
125
- step.entries.map(async entry => {
126
- const jid = `${step.name}#${entry.name}`
127
- const existing = journal.get(jid)
128
- if (existing?.status === 'completed') {
129
- return { entry, ok: true as const, result: existing.result, fresh: false }
130
- }
131
- try {
132
- const result = await entry.handler(ctx)
133
- return { entry, ok: true as const, result, fresh: true }
134
- } catch (err) {
135
- return { entry, ok: false as const, err, fresh: true }
136
- }
137
- })
138
- )
139
-
140
- const writes: JournalWrite[] = []
141
- const resultPatch: Record<string, unknown> = {}
142
- let firstError: unknown
143
-
144
- for (const s of settled) {
145
- if (s.ok) {
146
- resultPatch[s.entry.name] = s.result
147
- if (s.fresh) {
148
- writes.push({
149
- stepId: `${step.name}#${s.entry.name}`,
150
- status: 'completed',
151
- result: s.result,
152
- attempt: ctx.attempt,
153
- })
154
- }
155
- } else if (firstError === undefined) {
156
- firstError = s.err
157
- }
158
- }
159
-
160
- if (firstError === undefined) {
161
- writes.push({
162
- stepId: step.name,
163
- status: 'completed',
164
- result: resultPatch,
165
- attempt: ctx.attempt,
166
- })
167
- return { kind: 'advance', journal: writes, resultPatch }
168
- }
169
- return failureOutcome(step, ctx.attempt, firstError, writes)
170
- }
171
-
172
- // ── route ───────────────────────────────────────────────────────────────────
173
-
174
- async function runRoute(
175
- step: Extract<DurableStep, { type: 'route' }>,
176
- ctx: DurableContext,
177
- journal: Map<string, JournalRecord>
178
- ): Promise<StepOutcome> {
179
- const routeJid = `${step.name}#route`
180
- const writes: JournalWrite[] = []
181
- let routeKey: string
182
-
183
- const existingRoute = journal.get(routeJid)
184
- try {
185
- if (existingRoute?.status === 'completed') {
186
- routeKey = existingRoute.result as string
187
- } else {
188
- routeKey = await step.resolver(ctx)
189
- writes.push({ stepId: routeJid, status: 'completed', result: routeKey, attempt: ctx.attempt })
190
- }
191
- } catch (err) {
192
- return failureOutcome(step, ctx.attempt, err, [])
193
- }
194
-
195
- const branch = step.branches[routeKey]
196
- try {
197
- let result: unknown = null
198
- const existingBranch = journal.get(step.name)
199
- if (existingBranch?.status === 'completed') {
200
- result = existingBranch.result
201
- } else if (branch) {
202
- result = await branch(ctx)
203
- }
204
- writes.push({ stepId: step.name, status: 'completed', result, attempt: ctx.attempt })
205
- return {
206
- kind: 'advance',
207
- journal: writes,
208
- resultPatch: branch ? { [step.name]: result } : {},
209
- }
210
- } catch (err) {
211
- // Persist the route decision so a retry takes the same branch.
212
- return failureOutcome(step, ctx.attempt, err, writes)
213
- }
214
- }
215
-
216
- // ── loop ────────────────────────────────────────────────────────────────────
217
-
218
- async function runLoop(
219
- step: Extract<DurableStep, { type: 'loop' }>,
220
- ctx: DurableContext,
221
- journal: Map<string, JournalRecord>
222
- ): Promise<StepOutcome> {
223
- let currentInput: unknown = step.mapInput ? step.mapInput(ctx) : ctx.input
224
- let lastResult: unknown
225
- let ran = false
226
- const writes: JournalWrite[] = []
227
-
228
- for (let i = 0; i < step.maxIterations; i++) {
229
- ran = true
230
- const jid = `${step.name}#iter${i}`
231
- const existing = journal.get(jid)
232
-
233
- if (existing?.status === 'completed') {
234
- lastResult = existing.result
235
- } else {
236
- try {
237
- lastResult = await step.handler(currentInput, ctx)
238
- } catch (err) {
239
- return failureOutcome(step, ctx.attempt, err, writes)
240
- }
241
- writes.push({ stepId: jid, status: 'completed', result: lastResult, attempt: ctx.attempt })
242
- }
243
-
244
- if (step.until?.(lastResult, i + 1)) break
245
- if (step.feedback) currentInput = step.feedback(lastResult)
246
- }
247
-
248
- writes.push({ stepId: step.name, status: 'completed', result: lastResult ?? null, attempt: ctx.attempt })
249
- return {
250
- kind: 'advance',
251
- journal: writes,
252
- resultPatch: ran ? { [step.name]: lastResult } : {},
253
- }
254
- }
255
-
256
- // ── sleep ───────────────────────────────────────────────────────────────────
257
-
258
- function runSleep(step: Extract<DurableStep, { type: 'sleep' }>): StepOutcome {
259
- const wakeAt =
260
- step.duration instanceof Date
261
- ? step.duration
262
- : new Date(Date.now() + step.duration)
263
- return {
264
- kind: 'sleep',
265
- journal: [
266
- {
267
- stepId: step.name,
268
- status: 'completed',
269
- result: { wakeAt: wakeAt.toISOString() },
270
- attempt: 1,
271
- },
272
- ],
273
- resultPatch: {},
274
- wakeAt,
275
- }
276
- }
277
-
278
- // ── child ───────────────────────────────────────────────────────────────────
279
-
280
- function runChild(
281
- step: Extract<DurableStep, { type: 'child' }>,
282
- ctx: DurableContext
283
- ): StepOutcome {
284
- const childInput = step.mapInput ? step.mapInput(ctx) : ctx.input
285
- return {
286
- kind: 'await-child',
287
- childName: step.childName,
288
- childInput,
289
- childStepId: step.name,
290
- }
291
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * Brain composition point — duck-typed, with no `@strav/brain` dependency.
3
- *
4
- * `@strav/brain`'s `AgentRunner.run()` returns either an `AgentResult` or a
5
- * `SuspendedRun` ({ status: 'suspended', pendingToolCalls, state }). When a
6
- * durable `.step` handler returns a `SuspendedRun`, the engine suspends the
7
- * whole run and journals the snapshot; resuming the run re-enters the handler
8
- * so it can call `runner.resume(...)`. The per-agent suspend nests inside the
9
- * per-workflow suspend.
10
- */
11
-
12
- /** A structural view of a `@strav/brain` `SuspendedRun`. */
13
- export interface SuspendedRunLike {
14
- status: 'suspended'
15
- state: unknown
16
- pendingToolCalls: unknown
17
- }
18
-
19
- /** Structurally detect a brain `SuspendedRun` without importing `@strav/brain`. */
20
- export function isSuspendedRun(value: unknown): value is SuspendedRunLike {
21
- if (typeof value !== 'object' || value === null) return false
22
- const v = value as Record<string, unknown>
23
- return v.status === 'suspended' && 'state' in v && 'pendingToolCalls' in v
24
- }
package/src/errors.ts DELETED
@@ -1,21 +0,0 @@
1
- import { StravError } from '@strav/kernel'
2
-
3
- /** Base error for the durable execution engine. */
4
- export class DurableError extends StravError {}
5
-
6
- /** Thrown when a run id does not exist. */
7
- export class RunNotFoundError extends DurableError {
8
- constructor(public readonly runId: number) {
9
- super(`Durable run ${runId} not found.`)
10
- }
11
- }
12
-
13
- /** Thrown when a workflow name has no registered definition in this process. */
14
- export class WorkflowNotRegisteredError extends DurableError {
15
- constructor(public readonly workflowName: string) {
16
- super(
17
- `Durable workflow "${workflowName}" is not registered. ` +
18
- `Import the module that defines it before starting or advancing a run.`
19
- )
20
- }
21
- }
package/src/helpers.ts DELETED
@@ -1,16 +0,0 @@
1
- import { DurableWorkflow } from './builder.ts'
2
- import { registry } from './registry.ts'
3
-
4
- /**
5
- * Create a durable workflow and register it under `name`.
6
- *
7
- * @example
8
- * export const milestone = durable('milestone')
9
- * .step('discover', async (ctx) => discover(ctx.input))
10
- * .step('ship', async (ctx) => ship(ctx))
11
- */
12
- export function durable(name: string): DurableWorkflow {
13
- const workflow = new DurableWorkflow(name)
14
- registry.register(workflow)
15
- return workflow
16
- }
@@ -1,3 +0,0 @@
1
- export { WorkflowRun } from './workflow_run.ts'
2
- export { runMachine } from './run_machine.ts'
3
- export { loadJournal, writeJournal } from './journal.ts'
@@ -1,54 +0,0 @@
1
- import { sql } from '@strav/database'
2
- import type { JournalRecord, JournalWrite } from '../types.ts'
3
- import { parseJson } from '../util.ts'
4
-
5
- /**
6
- * Load every journal entry for a run, keyed by `step_id`. Used (unlocked) at
7
- * the start of an advance to short-circuit completed steps and sub-units.
8
- */
9
- export async function loadJournal(runId: number): Promise<Map<string, JournalRecord>> {
10
- const rows = (await sql`
11
- SELECT "step_id", "status", "result", "error", "attempt"
12
- FROM "_strav_workflow_journal"
13
- WHERE "run_id" = ${runId}
14
- `) as Record<string, unknown>[]
15
-
16
- const map = new Map<string, JournalRecord>()
17
- for (const r of rows) {
18
- map.set(r.step_id as string, {
19
- stepId: r.step_id as string,
20
- status: r.status as JournalRecord['status'],
21
- result: parseJson(r.result),
22
- error: (r.error as string | null) ?? null,
23
- attempt: Number(r.attempt),
24
- })
25
- }
26
- return map
27
- }
28
-
29
- /**
30
- * Append journal entries inside a transaction. `ON CONFLICT DO NOTHING` on
31
- * `UNIQUE (run_id, step_id)` makes a redelivered step idempotent — the first
32
- * write wins, later writes for the same `step_id` are no-ops.
33
- */
34
- export async function writeJournal(
35
- trx: { (strings: TemplateStringsArray, ...values: unknown[]): Promise<unknown> },
36
- runId: number,
37
- writes: JournalWrite[]
38
- ): Promise<void> {
39
- for (const w of writes) {
40
- await trx`
41
- INSERT INTO "_strav_workflow_journal"
42
- ("run_id", "step_id", "status", "result", "error", "attempt")
43
- VALUES (
44
- ${runId},
45
- ${w.stepId},
46
- ${w.status},
47
- ${w.result === undefined ? null : JSON.stringify(w.result)},
48
- ${w.error ?? null},
49
- ${w.attempt}
50
- )
51
- ON CONFLICT ("run_id", "step_id") DO NOTHING
52
- `
53
- }
54
- }
@@ -1,39 +0,0 @@
1
- import { defineMachine } from '@strav/machine'
2
-
3
- /**
4
- * The run-status lifecycle, modeled with `@strav/machine`.
5
- *
6
- * pending → running ⇄ suspended
7
- * running → completed
8
- * running/suspended → compensating → failed
9
- * any non-terminal → canceled
10
- *
11
- * The engine writes status columns via raw SQL inside its atomic, locked
12
- * transactions; this machine is the single declarative source of truth for
13
- * which transitions are legal and powers the `WorkflowRun` `stateful()` model.
14
- */
15
- export const runMachine = defineMachine({
16
- field: 'status',
17
- initial: 'pending',
18
- states: [
19
- 'pending',
20
- 'running',
21
- 'suspended',
22
- 'compensating',
23
- 'completed',
24
- 'failed',
25
- 'canceled',
26
- ],
27
- transitions: {
28
- begin: { from: 'pending', to: 'running' },
29
- suspend: { from: 'running', to: 'suspended' },
30
- wake: { from: 'suspended', to: 'running' },
31
- complete: { from: 'running', to: 'completed' },
32
- rollback: { from: ['running', 'suspended'], to: 'compensating' },
33
- fail: { from: ['running', 'suspended', 'compensating'], to: 'failed' },
34
- cancel: {
35
- from: ['pending', 'running', 'suspended', 'compensating'],
36
- to: 'canceled',
37
- },
38
- },
39
- })
@@ -1,36 +0,0 @@
1
- import type { DateTime } from 'luxon'
2
- import { BaseModel, cast } from '@strav/database'
3
- import { stateful } from '@strav/machine'
4
- import type { Machine } from '@strav/machine'
5
- import { runMachine } from './run_machine.ts'
6
-
7
- /**
8
- * ORM model over `_strav_workflow_runs` — the durable, queryable record of a
9
- * workflow run (Albastr's "pollable open-Turn live record").
10
- *
11
- * Mixed with `stateful()` so a run's status is a first-class state machine:
12
- * `run.is('running')`, `run.availableTransitions()`, `WorkflowRun.inState(...)`.
13
- * The engine hot path writes status via raw SQL for atomicity; this model is
14
- * for reads and for application code that wants to poll or query runs.
15
- */
16
- export class WorkflowRun extends stateful(BaseModel, runMachine as Machine) {
17
- static override get tableName(): string {
18
- return '_strav_workflow_runs'
19
- }
20
-
21
- declare id: number
22
- declare workflowName: string
23
- @cast('json') declare input: Record<string, unknown>
24
- declare status: string
25
- @cast('json') declare state: Record<string, unknown>
26
- declare currentStep: number
27
- declare compensationCursor: number | null
28
- declare parentRunId: number | null
29
- declare parentStepId: string | null
30
- declare awaitingSignal: string | null
31
- declare wakeAt: DateTime | null
32
- declare error: string | null
33
- @cast('json') declare result: Record<string, unknown> | null
34
- declare createdAt: DateTime
35
- declare updatedAt: DateTime
36
- }