ai-workflows 2.1.3 → 2.4.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 (188) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +14 -1
  3. package/README.md +2 -0
  4. package/dist/barrier.d.ts +6 -0
  5. package/dist/barrier.d.ts.map +1 -1
  6. package/dist/barrier.js +45 -7
  7. package/dist/barrier.js.map +1 -1
  8. package/dist/cascade-context.d.ts.map +1 -1
  9. package/dist/cascade-context.js +25 -25
  10. package/dist/cascade-context.js.map +1 -1
  11. package/dist/cascade-executor.d.ts.map +1 -1
  12. package/dist/cascade-executor.js +1 -1
  13. package/dist/cascade-executor.js.map +1 -1
  14. package/dist/context.d.ts.map +1 -1
  15. package/dist/context.js +23 -7
  16. package/dist/context.js.map +1 -1
  17. package/dist/cron-parser.d.ts +65 -0
  18. package/dist/cron-parser.d.ts.map +1 -0
  19. package/dist/cron-parser.js +294 -0
  20. package/dist/cron-parser.js.map +1 -0
  21. package/dist/cron-scheduler.d.ts +117 -0
  22. package/dist/cron-scheduler.d.ts.map +1 -0
  23. package/dist/cron-scheduler.js +176 -0
  24. package/dist/cron-scheduler.js.map +1 -0
  25. package/dist/database-context.d.ts +184 -0
  26. package/dist/database-context.d.ts.map +1 -0
  27. package/dist/database-context.js +428 -0
  28. package/dist/database-context.js.map +1 -0
  29. package/dist/digital-objects-adapter.d.ts +159 -0
  30. package/dist/digital-objects-adapter.d.ts.map +1 -0
  31. package/dist/digital-objects-adapter.js +229 -0
  32. package/dist/digital-objects-adapter.js.map +1 -0
  33. package/dist/durable-execution-cloudflare.d.ts +427 -0
  34. package/dist/durable-execution-cloudflare.d.ts.map +1 -0
  35. package/dist/durable-execution-cloudflare.js +510 -0
  36. package/dist/durable-execution-cloudflare.js.map +1 -0
  37. package/dist/durable-execution.d.ts +482 -0
  38. package/dist/durable-execution.d.ts.map +1 -0
  39. package/dist/durable-execution.js +594 -0
  40. package/dist/durable-execution.js.map +1 -0
  41. package/dist/durable-workflow.d.ts +176 -0
  42. package/dist/durable-workflow.d.ts.map +1 -0
  43. package/dist/durable-workflow.js +552 -0
  44. package/dist/durable-workflow.js.map +1 -0
  45. package/dist/graph/topological-sort.d.ts.map +1 -1
  46. package/dist/graph/topological-sort.js +5 -5
  47. package/dist/graph/topological-sort.js.map +1 -1
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +15 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/logger.d.ts +101 -0
  53. package/dist/logger.d.ts.map +1 -0
  54. package/dist/logger.js +115 -0
  55. package/dist/logger.js.map +1 -0
  56. package/dist/on.d.ts.map +1 -1
  57. package/dist/on.js +3 -3
  58. package/dist/on.js.map +1 -1
  59. package/dist/runtime.d.ts +169 -0
  60. package/dist/runtime.d.ts.map +1 -0
  61. package/dist/runtime.js +275 -0
  62. package/dist/runtime.js.map +1 -0
  63. package/dist/send.d.ts.map +1 -1
  64. package/dist/send.js +4 -3
  65. package/dist/send.js.map +1 -1
  66. package/dist/telemetry.d.ts +150 -0
  67. package/dist/telemetry.d.ts.map +1 -0
  68. package/dist/telemetry.js +388 -0
  69. package/dist/telemetry.js.map +1 -0
  70. package/dist/timer-registry.d.ts +25 -0
  71. package/dist/timer-registry.d.ts.map +1 -1
  72. package/dist/timer-registry.js +42 -8
  73. package/dist/timer-registry.js.map +1 -1
  74. package/dist/types.d.ts +17 -6
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js +1 -1
  77. package/dist/types.js.map +1 -1
  78. package/dist/worker/durable-step.d.ts +481 -0
  79. package/dist/worker/durable-step.d.ts.map +1 -0
  80. package/dist/worker/durable-step.js +606 -0
  81. package/dist/worker/durable-step.js.map +1 -0
  82. package/dist/worker/index.d.ts +106 -0
  83. package/dist/worker/index.d.ts.map +1 -0
  84. package/dist/worker/index.js +124 -0
  85. package/dist/worker/index.js.map +1 -0
  86. package/dist/worker/state-adapter.d.ts +230 -0
  87. package/dist/worker/state-adapter.d.ts.map +1 -0
  88. package/dist/worker/state-adapter.js +409 -0
  89. package/dist/worker/state-adapter.js.map +1 -0
  90. package/dist/worker/topological-executor.d.ts +282 -0
  91. package/dist/worker/topological-executor.d.ts.map +1 -0
  92. package/dist/worker/topological-executor.js +396 -0
  93. package/dist/worker/topological-executor.js.map +1 -0
  94. package/dist/worker/workflow-builder.d.ts +286 -0
  95. package/dist/worker/workflow-builder.d.ts.map +1 -0
  96. package/dist/worker/workflow-builder.js +565 -0
  97. package/dist/worker/workflow-builder.js.map +1 -0
  98. package/dist/worker.d.ts +800 -0
  99. package/dist/worker.d.ts.map +1 -0
  100. package/dist/worker.js +2428 -0
  101. package/dist/worker.js.map +1 -0
  102. package/dist/workflow-builder.d.ts +287 -0
  103. package/dist/workflow-builder.d.ts.map +1 -0
  104. package/dist/workflow-builder.js +762 -0
  105. package/dist/workflow-builder.js.map +1 -0
  106. package/dist/workflow.d.ts +14 -30
  107. package/dist/workflow.d.ts.map +1 -1
  108. package/dist/workflow.js +132 -292
  109. package/dist/workflow.js.map +1 -1
  110. package/examples/01-ecommerce-order-pipeline.ts +358 -0
  111. package/examples/02-content-moderation-cascade.ts +454 -0
  112. package/examples/03-scheduled-reporting-dependencies.ts +479 -0
  113. package/examples/04-database-persistence.ts +518 -0
  114. package/examples/README.md +173 -0
  115. package/package.json +30 -13
  116. package/src/__tests__/digital-objects-adapter.test.ts +274 -0
  117. package/src/__tests__/durable-workflow.test.ts +297 -0
  118. package/src/barrier.ts +48 -7
  119. package/src/cascade-context.ts +36 -29
  120. package/src/cascade-executor.ts +3 -2
  121. package/src/context.ts +41 -12
  122. package/src/cron-parser.ts +347 -0
  123. package/src/cron-scheduler.ts +239 -0
  124. package/src/database-context.ts +658 -0
  125. package/src/digital-objects-adapter.ts +351 -0
  126. package/src/durable-execution-cloudflare.ts +855 -0
  127. package/src/durable-execution.ts +1042 -0
  128. package/src/durable-workflow.ts +717 -0
  129. package/src/graph/topological-sort.ts +6 -8
  130. package/src/index.ts +69 -0
  131. package/src/logger.ts +148 -0
  132. package/src/on.ts +8 -9
  133. package/src/runtime.ts +436 -0
  134. package/src/send.ts +4 -5
  135. package/src/telemetry.ts +577 -0
  136. package/src/timer-registry.ts +44 -10
  137. package/src/types.ts +32 -17
  138. package/src/worker/durable-step.ts +976 -0
  139. package/src/worker/index.ts +216 -0
  140. package/src/worker/state-adapter.ts +589 -0
  141. package/src/worker/topological-executor.ts +625 -0
  142. package/src/worker/workflow-builder.ts +871 -0
  143. package/src/worker.ts +2906 -0
  144. package/src/workflow-builder.ts +1068 -0
  145. package/src/workflow.ts +188 -351
  146. package/test/barrier-join.test.ts +32 -24
  147. package/test/cascade-executor.test.ts +9 -16
  148. package/test/cron-parser.test.ts +314 -0
  149. package/test/cron-scheduler.test.ts +291 -0
  150. package/test/database-context.test.ts +770 -0
  151. package/test/db-provider-adapter.test.ts +862 -0
  152. package/test/durable-execution-cloudflare.test.ts +606 -0
  153. package/test/durable-execution-in-process.test.ts +286 -0
  154. package/test/durable-execution.test.ts +247 -0
  155. package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
  156. package/test/integration.test.ts +442 -0
  157. package/test/rpc-surface.test.ts +946 -0
  158. package/test/runtime.test.ts +262 -0
  159. package/test/schedule-timer-cleanup.test.ts +30 -21
  160. package/test/send-race-conditions.test.ts +30 -40
  161. package/test/worker/durable-cascade.test.ts +1117 -0
  162. package/test/worker/durable-step.test.ts +723 -0
  163. package/test/worker/topological-executor.test.ts +1240 -0
  164. package/test/worker/workflow-builder.test.ts +1067 -0
  165. package/test/worker.test.ts +608 -0
  166. package/test/workflow-builder.test.ts +1670 -0
  167. package/test/workflow-cron.test.ts +256 -0
  168. package/test/workflow-state-adapter.test.ts +923 -0
  169. package/test/workflow.test.ts +25 -22
  170. package/tsconfig.json +3 -1
  171. package/vitest.config.ts +38 -1
  172. package/vitest.workers.config.ts +44 -0
  173. package/wrangler.jsonc +22 -0
  174. package/.turbo/turbo-test.log +0 -169
  175. package/LICENSE +0 -21
  176. package/src/context.js +0 -83
  177. package/src/every.js +0 -267
  178. package/src/index.js +0 -71
  179. package/src/on.js +0 -79
  180. package/src/send.js +0 -111
  181. package/src/types.js +0 -4
  182. package/src/workflow.js +0 -455
  183. package/test/context.test.js +0 -116
  184. package/test/every.test.js +0 -282
  185. package/test/on.test.js +0 -80
  186. package/test/send.test.js +0 -89
  187. package/test/workflow.test.js +0 -224
  188. package/vitest.config.js +0 -7
package/src/runtime.ts ADDED
@@ -0,0 +1,436 @@
1
+ /**
2
+ * WorkflowRuntime - Owns the `$` runtime contract
3
+ *
4
+ * Purpose: Single module that owns end-to-end construction of the `$` context
5
+ * a workflow handler receives. Before this module existed the answer to
6
+ * "what does the handler see when it runs?" was stitched together from three
7
+ * separate modules:
8
+ *
9
+ * - cascade-context.ts (5W+H tracing)
10
+ * - database-context.ts (DB injection)
11
+ * - on.ts / send.ts / every.ts (event dispatch)
12
+ *
13
+ * Callers reading `$.on.Order.placed(...)` had no single place to discover
14
+ * what `$` contains. Tests had to assemble three contexts independently to
15
+ * exercise a handler.
16
+ *
17
+ * `WorkflowRuntime` collapses these into one port:
18
+ *
19
+ * - {@link createWorkflowRuntime} builds the runtime, owning the event/
20
+ * schedule registries, the optional `DatabaseContext` injection, and the
21
+ * optional cascade context.
22
+ * - The runtime exposes `$` (a {@link WorkflowContext}) and `dispatch()` —
23
+ * the latter is the test surface for exercising a handler against an
24
+ * event without spinning up a full {@link Workflow}.
25
+ *
26
+ * The cascade-context, database-context, and on/send/every modules continue
27
+ * to exist as **internal seams** — private adapters the runtime composes.
28
+ * `DatabaseContext` stays as an injected port so callers can wire either
29
+ * `ai-database`'s adapter or an in-memory adapter; this keeps `ai-workflows`
30
+ * Layer 0.
31
+ *
32
+ * @example Basic usage
33
+ * ```ts
34
+ * import { createWorkflowRuntime } from 'ai-workflows'
35
+ *
36
+ * const runtime = createWorkflowRuntime()
37
+ * runtime.register('Customer', 'created', async (customer, $) => {
38
+ * $.log('New customer:', customer)
39
+ * })
40
+ * await runtime.dispatch('Customer.created', { id: '123' })
41
+ * ```
42
+ *
43
+ * @example With injected DatabaseContext
44
+ * ```ts
45
+ * import { createWorkflowRuntime, createMemoryDatabaseContext } from 'ai-workflows'
46
+ *
47
+ * const runtime = createWorkflowRuntime({ db: createMemoryDatabaseContext() })
48
+ * ```
49
+ *
50
+ * @example Direct handler dispatch (test surface)
51
+ * ```ts
52
+ * const runtime = createWorkflowRuntime()
53
+ * const handler = vi.fn()
54
+ * runtime.register('Order', 'placed', handler)
55
+ * await runtime.dispatch('Order.placed', { id: 'o-1' })
56
+ * expect(handler).toHaveBeenCalled()
57
+ * ```
58
+ *
59
+ * @packageDocumentation
60
+ */
61
+
62
+ import type {
63
+ DatabaseContext,
64
+ EventHandler,
65
+ EventRegistration,
66
+ ParsedEvent,
67
+ ScheduleHandler,
68
+ ScheduleInterval,
69
+ ScheduleRegistration,
70
+ WorkflowContext,
71
+ WorkflowHistoryEntry,
72
+ WorkflowState,
73
+ DependencyConfig,
74
+ OnProxy,
75
+ EveryProxy,
76
+ } from './types.js'
77
+ import { createTypedOnProxy } from './on.js'
78
+ import { createTypedEveryProxy } from './every.js'
79
+ import { createCascadeContext, type CascadeContext } from './cascade-context.js'
80
+ import { getLogger } from './logger.js'
81
+
82
+ /**
83
+ * Parse an event string in `Noun.event` form. Returns `null` for invalid
84
+ * input. Lives on the runtime because dispatch is the only thing that needs
85
+ * to crack event names; `workflow.ts` re-exports it for back-compat.
86
+ */
87
+ export function parseEvent(event: string): ParsedEvent | null {
88
+ const parts = event.split('.')
89
+ if (parts.length !== 2) {
90
+ return null
91
+ }
92
+ const [noun, eventName] = parts
93
+ if (!noun || !eventName) {
94
+ return null
95
+ }
96
+ return { noun, event: eventName }
97
+ }
98
+
99
+ /**
100
+ * Options for constructing a {@link WorkflowRuntime}.
101
+ *
102
+ * All options are optional — a runtime constructed with no arguments runs
103
+ * fully in-memory with no persistence and no parent trace.
104
+ */
105
+ export interface WorkflowRuntimeOptions {
106
+ /**
107
+ * Initial state context (key/value bag exposed as `$.state`).
108
+ * Cloned defensively at construction so the caller's object is not mutated.
109
+ */
110
+ context?: Record<string, unknown>
111
+
112
+ /**
113
+ * Optional persistence port. When provided, `$.send` records events and
114
+ * `$.do` records actions through this adapter. When omitted, events and
115
+ * actions are still delivered in-memory but not persisted.
116
+ */
117
+ db?: DatabaseContext
118
+
119
+ /**
120
+ * Optional cascade context for distributed tracing / 5W+H step recording.
121
+ * If omitted a fresh root cascade context is created.
122
+ */
123
+ cascade?: CascadeContext
124
+
125
+ /**
126
+ * Optional name for the cascade context (used when cascade is auto-created).
127
+ */
128
+ name?: string
129
+ }
130
+
131
+ /**
132
+ * Public surface of the workflow runtime.
133
+ *
134
+ * The runtime owns construction of `$` and provides a single dispatch port
135
+ * for delivering events. Internal modules (cascade-context, database-context,
136
+ * on/send/every) are composed here, not exposed.
137
+ */
138
+ export interface WorkflowRuntime {
139
+ /**
140
+ * The `$` context handed to event and schedule handlers. This is the
141
+ * single source of truth for "what does a handler see when it runs."
142
+ */
143
+ readonly $: WorkflowContext
144
+
145
+ /**
146
+ * The cascade context owned by this runtime. Exposed for distributed
147
+ * tracing integration; not part of the handler-facing surface.
148
+ */
149
+ readonly cascade: CascadeContext
150
+
151
+ /**
152
+ * Register an event handler under a noun/event pair.
153
+ *
154
+ * Equivalent to `$.on.<Noun>.<event>(handler)`; provided as a direct method
155
+ * for tests and callers that hold the runtime reference rather than `$`.
156
+ */
157
+ register(
158
+ noun: string,
159
+ event: string,
160
+ handler: EventHandler,
161
+ dependencies?: DependencyConfig
162
+ ): void
163
+
164
+ /**
165
+ * Register a schedule handler under an interval.
166
+ * Schedule timers are not started by the runtime — the {@link Workflow}
167
+ * instance handles that. The runtime only tracks registrations.
168
+ */
169
+ registerSchedule(interval: ScheduleInterval, handler: ScheduleHandler): void
170
+
171
+ /**
172
+ * Dispatch an event to all matching registered handlers.
173
+ *
174
+ * This is the canonical test surface — exercise a handler by registering
175
+ * it on the runtime and calling `dispatch`. Awaits all handlers; rethrows
176
+ * the first error if any handler throws.
177
+ */
178
+ dispatch(event: string, data: unknown): Promise<void>
179
+
180
+ /**
181
+ * Dispatch an event and return the result of the first matching handler.
182
+ * Used by `$.do` and `$.try` semantics — `do` is the durable variant that
183
+ * also persists through the database adapter when one is configured.
184
+ */
185
+ execute<TResult = unknown>(event: string, data: unknown, durable: boolean): Promise<TResult>
186
+
187
+ /**
188
+ * All event handlers registered on this runtime.
189
+ * Returns the live array (mutated as new handlers register) so callers
190
+ * such as the {@link Workflow} lifecycle wrapper can read it as a definition.
191
+ */
192
+ getEventRegistry(): EventRegistration[]
193
+
194
+ /**
195
+ * All schedule handlers registered on this runtime. See {@link getEventRegistry}
196
+ * for sharing semantics.
197
+ */
198
+ getScheduleRegistry(): ScheduleRegistration[]
199
+
200
+ /** Mutable workflow state (context bag + history). */
201
+ readonly state: WorkflowState
202
+ }
203
+
204
+ /**
205
+ * Internal: append a history entry with the current timestamp.
206
+ */
207
+ function pushHistory(state: WorkflowState, entry: Omit<WorkflowHistoryEntry, 'timestamp'>): void {
208
+ state.history.push({ ...entry, timestamp: Date.now() })
209
+ }
210
+
211
+ /**
212
+ * Construct a {@link WorkflowRuntime}.
213
+ *
214
+ * The runtime is the single owner of the `$` contract — it composes:
215
+ *
216
+ * 1. Event/schedule registries (the dispatch half of on/send/every).
217
+ * 2. The optional injected `DatabaseContext` for durable record-keeping.
218
+ * 3. A cascade context for 5W+H tracing.
219
+ *
220
+ * The returned `runtime.$` is what handlers receive; `runtime.dispatch` is
221
+ * the canonical test surface.
222
+ */
223
+ export function createWorkflowRuntime(options: WorkflowRuntimeOptions = {}): WorkflowRuntime {
224
+ // ---------------------------------------------------------------------------
225
+ // State + registries (the runtime is the sole owner)
226
+ // ---------------------------------------------------------------------------
227
+
228
+ const state: WorkflowState = {
229
+ context: { ...(options.context ?? {}) },
230
+ history: [],
231
+ }
232
+
233
+ const eventRegistry: EventRegistration[] = []
234
+ const scheduleRegistry: ScheduleRegistration[] = []
235
+
236
+ // Cascade context (auto-created if not supplied)
237
+ const cascade =
238
+ options.cascade ??
239
+ createCascadeContext(options.name !== undefined ? { name: options.name } : {})
240
+
241
+ const db = options.db
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Registration (these are the "right halves" of on / every — the registry
245
+ // halves. The proxy halves live in on.ts/every.ts as factories we reuse.)
246
+ // ---------------------------------------------------------------------------
247
+
248
+ const register: WorkflowRuntime['register'] = (noun, event, handler, dependencies) => {
249
+ eventRegistry.push({
250
+ noun,
251
+ event,
252
+ handler,
253
+ source: handler.toString(),
254
+ ...(dependencies !== undefined && { dependencies }),
255
+ })
256
+ }
257
+
258
+ const registerSchedule: WorkflowRuntime['registerSchedule'] = (interval, handler) => {
259
+ scheduleRegistry.push({
260
+ interval,
261
+ handler,
262
+ source: handler.toString(),
263
+ })
264
+ }
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // Dispatch (the runtime's core: take an event, find handlers, run them)
268
+ // ---------------------------------------------------------------------------
269
+
270
+ const findMatching = (event: string): EventHandler[] => {
271
+ const parsed = parseEvent(event)
272
+ if (!parsed) {
273
+ getLogger().warn(`Invalid event format: ${event}. Expected Noun.event`)
274
+ return []
275
+ }
276
+ return eventRegistry
277
+ .filter((h) => h.noun === parsed.noun && h.event === parsed.event)
278
+ .map((h) => h.handler)
279
+ }
280
+
281
+ const dispatch: WorkflowRuntime['dispatch'] = async (event, data) => {
282
+ const matching = findMatching(event)
283
+ if (matching.length === 0) return
284
+
285
+ await Promise.all(
286
+ matching.map(async (handler) => {
287
+ try {
288
+ await handler(data, $)
289
+ } catch (error) {
290
+ getLogger().error(`Error in handler for ${event}:`, error)
291
+ }
292
+ })
293
+ )
294
+ }
295
+
296
+ const execute: WorkflowRuntime['execute'] = async <TResult = unknown>(
297
+ event: string,
298
+ data: unknown,
299
+ durable: boolean
300
+ ): Promise<TResult> => {
301
+ const parsed = parseEvent(event)
302
+ if (!parsed) {
303
+ throw new Error(`Invalid event format: ${event}. Expected Noun.event`)
304
+ }
305
+ const matching = eventRegistry.filter((h) => h.noun === parsed.noun && h.event === parsed.event)
306
+ if (matching.length === 0) {
307
+ throw new Error(`No handler registered for ${event}`)
308
+ }
309
+
310
+ const { handler } = matching[0]!
311
+
312
+ if (durable && db) {
313
+ await db.createAction({
314
+ actor: 'workflow',
315
+ object: event,
316
+ action: 'execute',
317
+ metadata: { data },
318
+ })
319
+ }
320
+
321
+ try {
322
+ const result = await handler(data, $)
323
+ return result as TResult
324
+ } catch (error) {
325
+ if (durable) {
326
+ getLogger().error(`[runtime] Durable action failed for ${event}:`, error)
327
+ }
328
+ throw error
329
+ }
330
+ }
331
+
332
+ // ---------------------------------------------------------------------------
333
+ // Build the `$` context. This is the WHOLE answer to "what does a handler
334
+ // see when it runs?" — every property below is a deliberate part of the
335
+ // handler-facing surface.
336
+ // ---------------------------------------------------------------------------
337
+
338
+ const onProxy: OnProxy = createTypedOnProxy((noun, event, handler, deps) => {
339
+ register(noun, event, handler, deps)
340
+ })
341
+
342
+ const everyProxy: EveryProxy = createTypedEveryProxy((interval, handler) => {
343
+ registerSchedule(interval, handler)
344
+ })
345
+
346
+ const $: WorkflowContext = {
347
+ track(event: string, data: unknown): void {
348
+ try {
349
+ pushHistory(state, { type: 'event', name: `track:${event}`, data })
350
+ dispatch(event, data).catch(() => {
351
+ // track() swallows errors by design
352
+ })
353
+ } catch {
354
+ // Silently swallow errors
355
+ }
356
+ },
357
+
358
+ send<T = unknown>(event: string, data: T): string {
359
+ const eventId = crypto.randomUUID()
360
+ pushHistory(state, { type: 'event', name: event, data })
361
+
362
+ const payload = { ...(data as object), _eventId: eventId }
363
+
364
+ // Persist if a DatabaseContext is wired
365
+ if (db) {
366
+ db.recordEvent(event, payload).catch((err) => {
367
+ getLogger().error(`[runtime] Failed to record event ${event}:`, err)
368
+ })
369
+ }
370
+
371
+ // Deliver
372
+ dispatch(event, payload).catch((err) => {
373
+ getLogger().error(`[runtime] Failed to deliver event ${event}:`, err)
374
+ })
375
+
376
+ return eventId
377
+ },
378
+
379
+ async do<TResult = unknown, TInput = unknown>(event: string, data: TInput): Promise<TResult> {
380
+ pushHistory(state, { type: 'action', name: `do:${event}`, data })
381
+ if (db) {
382
+ await db.recordEvent(event, data)
383
+ }
384
+ return execute<TResult>(event, data, true)
385
+ },
386
+
387
+ async try<TResult = unknown, TInput = unknown>(event: string, data: TInput): Promise<TResult> {
388
+ pushHistory(state, { type: 'action', name: `try:${event}`, data })
389
+ return execute<TResult>(event, data, false)
390
+ },
391
+
392
+ on: onProxy,
393
+ every: everyProxy,
394
+
395
+ state: state.context,
396
+
397
+ getState(): WorkflowState {
398
+ return structuredClone({
399
+ ...(state.current !== undefined && { current: state.current }),
400
+ context: state.context,
401
+ history: state.history,
402
+ })
403
+ },
404
+
405
+ set<T = unknown>(key: string, value: T): void {
406
+ state.context[key] = value
407
+ },
408
+
409
+ get<T = unknown>(key: string): T | undefined {
410
+ return state.context[key] as T | undefined
411
+ },
412
+
413
+ log(message: string, data?: unknown): void {
414
+ pushHistory(state, { type: 'action', name: 'log', data: { message, data } })
415
+ getLogger().log(`[workflow] ${message}`, data ?? '')
416
+ },
417
+
418
+ ...(db !== undefined && { db }),
419
+ }
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // Public surface
423
+ // ---------------------------------------------------------------------------
424
+
425
+ return {
426
+ $,
427
+ cascade,
428
+ register,
429
+ registerSchedule,
430
+ dispatch,
431
+ execute,
432
+ getEventRegistry: () => eventRegistry,
433
+ getScheduleRegistry: () => scheduleRegistry,
434
+ state,
435
+ }
436
+ }
package/src/send.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  import { getEventHandlers } from './on.js'
10
10
  import { createWorkflowContext } from './context.js'
11
11
  import { parseEvent } from './workflow.js'
12
+ import { getLogger } from './logger.js'
12
13
 
13
14
  /**
14
15
  * Event bus for managing event delivery
@@ -48,14 +49,12 @@ class EventBus {
48
49
  private async deliver(event: string, data: unknown): Promise<void> {
49
50
  const parsed = parseEvent(event)
50
51
  if (!parsed) {
51
- console.warn(`Invalid event format: ${event}. Expected Noun.event`)
52
+ getLogger().warn(`Invalid event format: ${event}. Expected Noun.event`)
52
53
  return
53
54
  }
54
55
 
55
56
  const handlers = getEventHandlers()
56
- const matching = handlers.filter(
57
- h => h.noun === parsed.noun && h.event === parsed.event
58
- )
57
+ const matching = handlers.filter((h) => h.noun === parsed.noun && h.event === parsed.event)
59
58
 
60
59
  if (matching.length === 0) {
61
60
  // No handlers registered - that's okay, event is just not handled
@@ -71,7 +70,7 @@ class EventBus {
71
70
  try {
72
71
  await handler(data, ctx)
73
72
  } catch (error) {
74
- console.error(`Error in handler for ${event}:`, error)
73
+ getLogger().error(`Error in handler for ${event}:`, error)
75
74
  }
76
75
  })
77
76
  )