ai-workflows 2.0.2 → 2.1.1

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