ai-workflows 1.1.0 → 2.0.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 (55) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-test.log +104 -0
  3. package/README.md +285 -24
  4. package/dist/context.d.ts +26 -0
  5. package/dist/context.d.ts.map +1 -0
  6. package/dist/context.js +84 -0
  7. package/dist/context.js.map +1 -0
  8. package/dist/every.d.ts +67 -0
  9. package/dist/every.d.ts.map +1 -0
  10. package/dist/every.js +268 -0
  11. package/dist/every.js.map +1 -0
  12. package/dist/index.d.ts +68 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +72 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/on.d.ts +49 -0
  17. package/dist/on.d.ts.map +1 -0
  18. package/dist/on.js +80 -0
  19. package/dist/on.js.map +1 -0
  20. package/dist/send.d.ts +59 -0
  21. package/dist/send.d.ts.map +1 -0
  22. package/dist/send.js +112 -0
  23. package/dist/send.js.map +1 -0
  24. package/dist/types.d.ts +229 -0
  25. package/dist/types.d.ts.map +1 -0
  26. package/dist/types.js +5 -0
  27. package/dist/types.js.map +1 -0
  28. package/dist/workflow.d.ts +79 -0
  29. package/dist/workflow.d.ts.map +1 -0
  30. package/dist/workflow.js +456 -0
  31. package/dist/workflow.js.map +1 -0
  32. package/package.json +25 -70
  33. package/src/context.ts +108 -0
  34. package/src/every.ts +299 -0
  35. package/src/index.ts +106 -0
  36. package/src/on.ts +100 -0
  37. package/src/send.ts +131 -0
  38. package/src/types.ts +244 -0
  39. package/src/workflow.ts +569 -0
  40. package/test/context.test.ts +151 -0
  41. package/test/every.test.ts +361 -0
  42. package/test/on.test.ts +100 -0
  43. package/test/send.test.ts +118 -0
  44. package/test/workflow.test.ts +288 -0
  45. package/tsconfig.json +9 -0
  46. package/vitest.config.ts +8 -0
  47. package/LICENSE +0 -21
  48. package/dist/scripts/generate-types.d.ts +0 -1
  49. package/dist/scripts/generate-types.js +0 -57
  50. package/dist/src/ai-proxy.d.ts +0 -5
  51. package/dist/src/ai-proxy.js +0 -94
  52. package/dist/src/index.d.ts +0 -9
  53. package/dist/src/index.js +0 -11
  54. package/dist/src/index.test.d.ts +0 -1
  55. package/dist/src/index.test.js +0 -35
package/src/every.ts ADDED
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Schedule registration using natural language
3
+ *
4
+ * Usage:
5
+ * every.hour($ => { ... })
6
+ * every.Thursday.at8am($ => { ... })
7
+ * every.weekday.at9am($ => { ... })
8
+ * every('hour during business hours', $ => { ... })
9
+ * every('first Monday of the month at 9am', $ => { ... })
10
+ */
11
+
12
+ import type { ScheduleHandler, ScheduleRegistration, ScheduleInterval } from './types.js'
13
+
14
+ /**
15
+ * Registry of schedule handlers
16
+ */
17
+ const scheduleRegistry: ScheduleRegistration[] = []
18
+
19
+ /**
20
+ * Get all registered schedule handlers
21
+ */
22
+ export function getScheduleHandlers(): ScheduleRegistration[] {
23
+ return [...scheduleRegistry]
24
+ }
25
+
26
+ /**
27
+ * Clear all registered schedule handlers
28
+ */
29
+ export function clearScheduleHandlers(): void {
30
+ scheduleRegistry.length = 0
31
+ }
32
+
33
+ /**
34
+ * Register a schedule handler directly
35
+ */
36
+ export function registerScheduleHandler(
37
+ interval: ScheduleInterval,
38
+ handler: ScheduleHandler
39
+ ): void {
40
+ scheduleRegistry.push({
41
+ interval,
42
+ handler,
43
+ source: handler.toString(),
44
+ })
45
+ }
46
+
47
+ /**
48
+ * Well-known cron patterns for common schedules
49
+ */
50
+ const KNOWN_PATTERNS: Record<string, string> = {
51
+ // Time units
52
+ 'second': '* * * * * *',
53
+ 'minute': '* * * * *',
54
+ 'hour': '0 * * * *',
55
+ 'day': '0 0 * * *',
56
+ 'week': '0 0 * * 0',
57
+ 'month': '0 0 1 * *',
58
+ 'year': '0 0 1 1 *',
59
+
60
+ // Days of week
61
+ 'Monday': '0 0 * * 1',
62
+ 'Tuesday': '0 0 * * 2',
63
+ 'Wednesday': '0 0 * * 3',
64
+ 'Thursday': '0 0 * * 4',
65
+ 'Friday': '0 0 * * 5',
66
+ 'Saturday': '0 0 * * 6',
67
+ 'Sunday': '0 0 * * 0',
68
+
69
+ // Common patterns
70
+ 'weekday': '0 0 * * 1-5',
71
+ 'weekend': '0 0 * * 0,6',
72
+ 'midnight': '0 0 * * *',
73
+ 'noon': '0 12 * * *',
74
+ }
75
+
76
+ /**
77
+ * Time suffixes for day-based schedules
78
+ */
79
+ const TIME_PATTERNS: Record<string, { hour: number; minute: number }> = {
80
+ 'at6am': { hour: 6, minute: 0 },
81
+ 'at7am': { hour: 7, minute: 0 },
82
+ 'at8am': { hour: 8, minute: 0 },
83
+ 'at9am': { hour: 9, minute: 0 },
84
+ 'at10am': { hour: 10, minute: 0 },
85
+ 'at11am': { hour: 11, minute: 0 },
86
+ 'at12pm': { hour: 12, minute: 0 },
87
+ 'atnoon': { hour: 12, minute: 0 },
88
+ 'at1pm': { hour: 13, minute: 0 },
89
+ 'at2pm': { hour: 14, minute: 0 },
90
+ 'at3pm': { hour: 15, minute: 0 },
91
+ 'at4pm': { hour: 16, minute: 0 },
92
+ 'at5pm': { hour: 17, minute: 0 },
93
+ 'at6pm': { hour: 18, minute: 0 },
94
+ 'at7pm': { hour: 19, minute: 0 },
95
+ 'at8pm': { hour: 20, minute: 0 },
96
+ 'at9pm': { hour: 21, minute: 0 },
97
+ 'atmidnight': { hour: 0, minute: 0 },
98
+ }
99
+
100
+ /**
101
+ * Parse a known pattern or return null
102
+ */
103
+ function parseKnownPattern(pattern: string): string | null {
104
+ return KNOWN_PATTERNS[pattern] ?? null
105
+ }
106
+
107
+ /**
108
+ * Combine a day pattern with a time pattern
109
+ */
110
+ function combineWithTime(baseCron: string, time: { hour: number; minute: number }): string {
111
+ const parts = baseCron.split(' ')
112
+ parts[0] = String(time.minute)
113
+ parts[1] = String(time.hour)
114
+ return parts.join(' ')
115
+ }
116
+
117
+ /**
118
+ * AI-powered cron conversion (placeholder - will use ai-functions)
119
+ */
120
+ let cronConverter: ((description: string) => Promise<string>) | null = null
121
+
122
+ /**
123
+ * Set the AI cron converter function
124
+ */
125
+ export function setCronConverter(converter: (description: string) => Promise<string>): void {
126
+ cronConverter = converter
127
+ }
128
+
129
+ /**
130
+ * Convert natural language to cron expression
131
+ */
132
+ export async function toCron(description: string): Promise<string> {
133
+ // First check known patterns
134
+ const known = parseKnownPattern(description)
135
+ if (known) return known
136
+
137
+ // If we have an AI converter, use it
138
+ if (cronConverter) {
139
+ return cronConverter(description)
140
+ }
141
+
142
+ // Otherwise, assume it's already a cron expression or throw
143
+ if (/^[\d\*\-\/\,\s]+$/.test(description)) {
144
+ return description
145
+ }
146
+
147
+ throw new Error(
148
+ `Unknown schedule pattern: "${description}". ` +
149
+ `Set up AI conversion with setCronConverter() for natural language schedules.`
150
+ )
151
+ }
152
+
153
+ /**
154
+ * Create the `every` proxy
155
+ */
156
+ function createEveryProxy() {
157
+ const handler = {
158
+ get(_target: unknown, prop: string) {
159
+ // Check if it's a known pattern
160
+ const pattern = KNOWN_PATTERNS[prop]
161
+ if (pattern) {
162
+ // Return an object that can either be called directly or have time accessors
163
+ const result = (handlerFn: ScheduleHandler) => {
164
+ registerScheduleHandler({ type: 'cron', expression: pattern, natural: prop }, handlerFn)
165
+ }
166
+ // Add time accessors
167
+ return new Proxy(result, {
168
+ get(_t, timeKey: string) {
169
+ const time = TIME_PATTERNS[timeKey]
170
+ if (time) {
171
+ const cron = combineWithTime(pattern, time)
172
+ return (handlerFn: ScheduleHandler) => {
173
+ registerScheduleHandler({ type: 'cron', expression: cron, natural: `${prop}.${timeKey}` }, handlerFn)
174
+ }
175
+ }
176
+ return undefined
177
+ },
178
+ apply(_t, _thisArg, args) {
179
+ registerScheduleHandler({ type: 'cron', expression: pattern, natural: prop }, args[0])
180
+ }
181
+ })
182
+ }
183
+
184
+ // Check for plural time units (e.g., seconds(5), minutes(30))
185
+ const pluralUnits: Record<string, string> = {
186
+ seconds: 'second',
187
+ minutes: 'minute',
188
+ hours: 'hour',
189
+ days: 'day',
190
+ weeks: 'week',
191
+ }
192
+ if (pluralUnits[prop]) {
193
+ return (value: number) => (handlerFn: ScheduleHandler) => {
194
+ registerScheduleHandler({ type: pluralUnits[prop] as any, value, natural: `${value} ${prop}` }, handlerFn)
195
+ }
196
+ }
197
+
198
+ return undefined
199
+ },
200
+
201
+ apply(_target: unknown, _thisArg: unknown, args: unknown[]) {
202
+ // Called as every('natural language description', handler)
203
+ const [description, handler] = args as [string, ScheduleHandler]
204
+
205
+ if (typeof description === 'string' && typeof handler === 'function') {
206
+ // Register with natural language - will be converted to cron at runtime
207
+ registerScheduleHandler({ type: 'natural', description }, handler)
208
+ }
209
+ }
210
+ }
211
+
212
+ return new Proxy(function() {} as any, handler)
213
+ }
214
+
215
+ /**
216
+ * The `every` function/object for registering scheduled handlers
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * import { every } from 'ai-workflows'
221
+ *
222
+ * // Simple intervals
223
+ * every.hour($ => $.log('Hourly task'))
224
+ * every.day($ => $.log('Daily task'))
225
+ *
226
+ * // Day + time combinations
227
+ * every.Monday.at9am($ => $.log('Monday morning standup'))
228
+ * every.weekday.at8am($ => $.log('Workday start'))
229
+ * every.Friday.at5pm($ => $.log('End of week report'))
230
+ *
231
+ * // Plural intervals with values
232
+ * every.minutes(30)($ => $.log('Every 30 minutes'))
233
+ * every.hours(4)($ => $.log('Every 4 hours'))
234
+ *
235
+ * // Natural language (requires AI converter)
236
+ * every('hour during business hours', $ => { ... })
237
+ * every('first Monday of the month at 9am', $ => { ... })
238
+ * every('every 15 minutes between 9am and 5pm on weekdays', $ => { ... })
239
+ * ```
240
+ */
241
+ export const every = createEveryProxy()
242
+
243
+ /**
244
+ * Convert interval to milliseconds (for simulation/testing)
245
+ */
246
+ export function intervalToMs(interval: ScheduleInterval): number {
247
+ switch (interval.type) {
248
+ case 'second':
249
+ return (interval.value ?? 1) * 1000
250
+ case 'minute':
251
+ return (interval.value ?? 1) * 60 * 1000
252
+ case 'hour':
253
+ return (interval.value ?? 1) * 60 * 60 * 1000
254
+ case 'day':
255
+ return (interval.value ?? 1) * 24 * 60 * 60 * 1000
256
+ case 'week':
257
+ return (interval.value ?? 1) * 7 * 24 * 60 * 60 * 1000
258
+ case 'cron':
259
+ case 'natural':
260
+ // Cron/natural expressions need special handling
261
+ return 0
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Format interval for display
267
+ */
268
+ export function formatInterval(interval: ScheduleInterval): string {
269
+ if ('natural' in interval && interval.natural) {
270
+ return interval.natural
271
+ }
272
+
273
+ switch (interval.type) {
274
+ case 'second':
275
+ return interval.value && interval.value > 1
276
+ ? `every ${interval.value} seconds`
277
+ : 'every second'
278
+ case 'minute':
279
+ return interval.value && interval.value > 1
280
+ ? `every ${interval.value} minutes`
281
+ : 'every minute'
282
+ case 'hour':
283
+ return interval.value && interval.value > 1
284
+ ? `every ${interval.value} hours`
285
+ : 'every hour'
286
+ case 'day':
287
+ return interval.value && interval.value > 1
288
+ ? `every ${interval.value} days`
289
+ : 'every day'
290
+ case 'week':
291
+ return interval.value && interval.value > 1
292
+ ? `every ${interval.value} weeks`
293
+ : 'every week'
294
+ case 'cron':
295
+ return `cron: ${interval.expression}`
296
+ case 'natural':
297
+ return interval.description
298
+ }
299
+ }
package/src/index.ts ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * ai-workflows - Event-driven workflows with $ context
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { Workflow } from 'ai-workflows'
7
+ *
8
+ * // Create a workflow with $ context
9
+ * const workflow = Workflow($ => {
10
+ * // Register event handlers
11
+ * $.on.Customer.created(async (customer, $) => {
12
+ * $.log('New customer:', customer.name)
13
+ * await $.send('Email.welcome', { to: customer.email })
14
+ * })
15
+ *
16
+ * $.on.Order.completed(async (order, $) => {
17
+ * $.log('Order completed:', order.id)
18
+ * })
19
+ *
20
+ * // Register scheduled tasks
21
+ * $.every.hour(async ($) => {
22
+ * $.log('Hourly check')
23
+ * })
24
+ *
25
+ * $.every.Monday.at9am(async ($) => {
26
+ * $.log('Weekly standup reminder')
27
+ * })
28
+ *
29
+ * $.every.minutes(30)(async ($) => {
30
+ * $.log('Every 30 minutes')
31
+ * })
32
+ *
33
+ * // Natural language scheduling
34
+ * $.every('first Monday of the month', async ($) => {
35
+ * $.log('Monthly report')
36
+ * })
37
+ * })
38
+ *
39
+ * // Start the workflow
40
+ * await workflow.start()
41
+ *
42
+ * // Emit events
43
+ * await workflow.send('Customer.created', { name: 'John', email: 'john@example.com' })
44
+ * ```
45
+ *
46
+ * @example
47
+ * // Alternative: Use standalone on/every for global registration
48
+ * ```ts
49
+ * import { on, every, send } from 'ai-workflows'
50
+ *
51
+ * on.Customer.created(async (customer, $) => {
52
+ * await $.send('Email.welcome', { to: customer.email })
53
+ * })
54
+ *
55
+ * every.hour(async ($) => {
56
+ * $.log('Hourly task')
57
+ * })
58
+ *
59
+ * await send('Customer.created', { name: 'John' })
60
+ * ```
61
+ */
62
+
63
+ // Main Workflow API
64
+ export { Workflow, createTestContext, parseEvent, type WorkflowInstance } from './workflow.js'
65
+
66
+ // Standalone event handling (for global registration)
67
+ export { on, registerEventHandler, getEventHandlers, clearEventHandlers } from './on.js'
68
+
69
+ // Standalone scheduling (for global registration)
70
+ export {
71
+ every,
72
+ registerScheduleHandler,
73
+ getScheduleHandlers,
74
+ clearScheduleHandlers,
75
+ setCronConverter,
76
+ toCron,
77
+ intervalToMs,
78
+ formatInterval,
79
+ } from './every.js'
80
+
81
+ // Event emission
82
+ export { send, getEventBus } from './send.js'
83
+
84
+ // Context
85
+ export { createWorkflowContext, createIsolatedContext } from './context.js'
86
+
87
+ // Types
88
+ export type {
89
+ EventHandler,
90
+ ScheduleHandler,
91
+ WorkflowContext,
92
+ WorkflowState,
93
+ WorkflowHistoryEntry,
94
+ EventRegistration,
95
+ ScheduleRegistration,
96
+ ScheduleInterval,
97
+ WorkflowDefinition,
98
+ WorkflowOptions,
99
+ ParsedEvent,
100
+ OnProxy,
101
+ EveryProxy,
102
+ HandlerFunction,
103
+ DatabaseContext,
104
+ ActionData,
105
+ ArtifactData,
106
+ } from './types.js'
package/src/on.ts ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Event handler registration using on.Noun.event syntax
3
+ *
4
+ * Usage:
5
+ * on.Customer.created(customer => { ... })
6
+ * on.Order.completed(order => { ... })
7
+ * on.Payment.failed(payment => { ... })
8
+ */
9
+
10
+ import type { EventHandler, EventRegistration } from './types.js'
11
+
12
+ /**
13
+ * Registry of event handlers
14
+ */
15
+ const eventRegistry: EventRegistration[] = []
16
+
17
+ /**
18
+ * Get all registered event handlers
19
+ */
20
+ export function getEventHandlers(): EventRegistration[] {
21
+ return [...eventRegistry]
22
+ }
23
+
24
+ /**
25
+ * Clear all registered event handlers
26
+ */
27
+ export function clearEventHandlers(): void {
28
+ eventRegistry.length = 0
29
+ }
30
+
31
+ /**
32
+ * Register an event handler directly
33
+ */
34
+ export function registerEventHandler(
35
+ noun: string,
36
+ event: string,
37
+ handler: EventHandler
38
+ ): void {
39
+ eventRegistry.push({
40
+ noun,
41
+ event,
42
+ handler,
43
+ source: handler.toString(),
44
+ })
45
+ }
46
+
47
+ /**
48
+ * Event proxy type for on.Noun.event pattern
49
+ */
50
+ type EventProxy = {
51
+ [noun: string]: {
52
+ [event: string]: (handler: EventHandler) => void
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Create the `on` proxy
58
+ *
59
+ * This creates a proxy that allows:
60
+ * on.Customer.created(handler)
61
+ * on.Order.shipped(handler)
62
+ *
63
+ * The first property access captures the noun (Customer, Order)
64
+ * The second property access captures the event (created, shipped)
65
+ * The function call registers the handler
66
+ */
67
+ function createOnProxy(): EventProxy {
68
+ return new Proxy({} as EventProxy, {
69
+ get(_target, noun: string) {
70
+ // Return a proxy for the event level
71
+ return new Proxy({}, {
72
+ get(_eventTarget, event: string) {
73
+ // Return a function that registers the handler
74
+ return (handler: EventHandler) => {
75
+ registerEventHandler(noun, event, handler)
76
+ }
77
+ }
78
+ })
79
+ }
80
+ })
81
+ }
82
+
83
+ /**
84
+ * The `on` object for registering event handlers
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * import { on } from 'ai-workflows'
89
+ *
90
+ * on.Customer.created(async (customer, $) => {
91
+ * $.log('Customer created:', customer.name)
92
+ * await $.send('Email.welcome', { to: customer.email })
93
+ * })
94
+ *
95
+ * on.Order.completed(async (order, $) => {
96
+ * $.log('Order completed:', order.id)
97
+ * })
98
+ * ```
99
+ */
100
+ export const on = createOnProxy()
package/src/send.ts ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Event emission using send('Noun.event', data)
3
+ *
4
+ * Usage:
5
+ * send('Customer.created', customer)
6
+ * send('Order.completed', order)
7
+ */
8
+
9
+ import type { ParsedEvent } from './types.js'
10
+ import { getEventHandlers } from './on.js'
11
+ import { createWorkflowContext } from './context.js'
12
+
13
+ /**
14
+ * Event bus for managing event delivery
15
+ */
16
+ class EventBus {
17
+ private pending: Array<{ event: string; data: unknown }> = []
18
+ private processing = false
19
+
20
+ /**
21
+ * Emit an event
22
+ */
23
+ async emit(event: string, data: unknown): Promise<void> {
24
+ this.pending.push({ event, data })
25
+
26
+ if (!this.processing) {
27
+ await this.process()
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Process pending events
33
+ */
34
+ private async process(): Promise<void> {
35
+ this.processing = true
36
+
37
+ while (this.pending.length > 0) {
38
+ const item = this.pending.shift()!
39
+ await this.deliver(item.event, item.data)
40
+ }
41
+
42
+ this.processing = false
43
+ }
44
+
45
+ /**
46
+ * Deliver an event to matching handlers
47
+ */
48
+ private async deliver(event: string, data: unknown): Promise<void> {
49
+ const parsed = parseEvent(event)
50
+ if (!parsed) {
51
+ console.warn(`Invalid event format: ${event}. Expected Noun.event`)
52
+ return
53
+ }
54
+
55
+ const handlers = getEventHandlers()
56
+ const matching = handlers.filter(
57
+ h => h.noun === parsed.noun && h.event === parsed.event
58
+ )
59
+
60
+ if (matching.length === 0) {
61
+ // No handlers registered - that's okay, event is just not handled
62
+ return
63
+ }
64
+
65
+ // Create workflow context for handlers
66
+ const ctx = createWorkflowContext(this)
67
+
68
+ // Execute all matching handlers
69
+ await Promise.all(
70
+ matching.map(async ({ handler }) => {
71
+ try {
72
+ await handler(data, ctx)
73
+ } catch (error) {
74
+ console.error(`Error in handler for ${event}:`, error)
75
+ }
76
+ })
77
+ )
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Parse event string into noun and event
83
+ */
84
+ export function parseEvent(event: string): ParsedEvent | null {
85
+ const parts = event.split('.')
86
+ if (parts.length !== 2) {
87
+ return null
88
+ }
89
+ const [noun, eventName] = parts
90
+ if (!noun || !eventName) {
91
+ return null
92
+ }
93
+ return { noun, event: eventName }
94
+ }
95
+
96
+ /**
97
+ * Global event bus instance
98
+ */
99
+ const globalEventBus = new EventBus()
100
+
101
+ /**
102
+ * Send an event
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * import { send } from 'ai-workflows'
107
+ *
108
+ * // Emit a customer created event
109
+ * await send('Customer.created', {
110
+ * id: '123',
111
+ * name: 'John Doe',
112
+ * email: 'john@example.com'
113
+ * })
114
+ *
115
+ * // Emit an order completed event
116
+ * await send('Order.completed', {
117
+ * id: 'order-456',
118
+ * total: 99.99
119
+ * })
120
+ * ```
121
+ */
122
+ export async function send<T = unknown>(event: string, data: T): Promise<void> {
123
+ await globalEventBus.emit(event, data)
124
+ }
125
+
126
+ /**
127
+ * Get the global event bus (for testing/internal use)
128
+ */
129
+ export function getEventBus(): EventBus {
130
+ return globalEventBus
131
+ }