digital-workers 2.0.1 → 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/team.js ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Team definition for digital workers
3
+ */
4
+ /**
5
+ * Define a team of workers
6
+ *
7
+ * Teams organize workers (AI agents and humans) into cohesive units
8
+ * with shared goals and responsibilities.
9
+ *
10
+ * @param definition - Team definition
11
+ * @returns The defined team
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const engineeringTeam = Team({
16
+ * name: 'Engineering',
17
+ * description: 'Product engineering team',
18
+ * members: [
19
+ * { id: 'alice', name: 'Alice', role: 'Senior Engineer', type: 'human' },
20
+ * { id: 'bob', name: 'Bob', role: 'Engineer', type: 'human' },
21
+ * { id: 'ai-reviewer', name: 'Code Reviewer', role: 'Code Reviewer', type: 'ai' },
22
+ * { id: 'ai-tester', name: 'Test Generator', role: 'Test Engineer', type: 'ai' },
23
+ * ],
24
+ * goals: [
25
+ * 'Ship features on schedule',
26
+ * 'Maintain code quality',
27
+ * 'Reduce technical debt',
28
+ * ],
29
+ * lead: 'alice',
30
+ * })
31
+ * ```
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const supportTeam = Team({
36
+ * name: 'Customer Support',
37
+ * description: '24/7 customer support team',
38
+ * members: [
39
+ * { id: 'support-ai-1', name: 'Support Bot Alpha', role: 'Support Agent', type: 'ai' },
40
+ * { id: 'support-ai-2', name: 'Support Bot Beta', role: 'Support Agent', type: 'ai' },
41
+ * { id: 'escalation-human', name: 'Jane', role: 'Senior Support', type: 'human' },
42
+ * ],
43
+ * goals: [
44
+ * 'Maintain 95% satisfaction rate',
45
+ * 'Response time under 5 minutes',
46
+ * 'First contact resolution > 80%',
47
+ * ],
48
+ * lead: 'escalation-human',
49
+ * })
50
+ * ```
51
+ */
52
+ export function Team(definition) {
53
+ return definition;
54
+ }
55
+ /**
56
+ * Add a member to a team
57
+ *
58
+ * @param team - The team to add to
59
+ * @param member - The member to add
60
+ * @returns Updated team
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const updatedTeam = Team.addMember(engineeringTeam, {
65
+ * id: 'charlie',
66
+ * name: 'Charlie',
67
+ * role: 'Junior Engineer',
68
+ * type: 'human',
69
+ * })
70
+ * ```
71
+ */
72
+ Team.addMember = (team, member) => ({
73
+ ...team,
74
+ members: [...team.members, member],
75
+ });
76
+ /**
77
+ * Remove a member from a team
78
+ *
79
+ * @param team - The team to remove from
80
+ * @param memberId - ID of the member to remove
81
+ * @returns Updated team
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const updatedTeam = Team.removeMember(engineeringTeam, 'bob')
86
+ * ```
87
+ */
88
+ Team.removeMember = (team, memberId) => ({
89
+ ...team,
90
+ members: team.members.filter((m) => m.id !== memberId),
91
+ });
92
+ /**
93
+ * Get all AI members of a team
94
+ *
95
+ * @param team - The team
96
+ * @returns Array of AI members
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * const aiMembers = Team.aiMembers(supportTeam)
101
+ * console.log(aiMembers) // [Support Bot Alpha, Support Bot Beta]
102
+ * ```
103
+ */
104
+ Team.aiMembers = (team) => team.members.filter((m) => m.type === 'agent');
105
+ /**
106
+ * Get all human members of a team
107
+ *
108
+ * @param team - The team
109
+ * @returns Array of human members
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * const humans = Team.humanMembers(engineeringTeam)
114
+ * console.log(humans) // [Alice, Bob]
115
+ * ```
116
+ */
117
+ Team.humanMembers = (team) => team.members.filter((m) => m.type === 'human');
118
+ /**
119
+ * Get members by role
120
+ *
121
+ * @param team - The team
122
+ * @param role - Role to filter by
123
+ * @returns Array of members with that role
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * const engineers = Team.byRole(engineeringTeam, 'Engineer')
128
+ * ```
129
+ */
130
+ Team.byRole = (team, role) => team.members.filter((m) => m.role === role);
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Communication Transports Bridge
3
+ *
4
+ * Connects digital-workers contact channels to digital-tools
5
+ * communication providers (Message, Call).
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ // =============================================================================
10
+ // Channel to Transport Mapping
11
+ // =============================================================================
12
+ /**
13
+ * Map contact channel to transport
14
+ */
15
+ export function channelToTransport(channel) {
16
+ const mapping = {
17
+ email: 'email',
18
+ slack: 'slack',
19
+ teams: 'teams',
20
+ discord: 'discord',
21
+ phone: 'voice',
22
+ sms: 'sms',
23
+ whatsapp: 'whatsapp',
24
+ telegram: 'telegram',
25
+ web: 'web',
26
+ api: 'webhook',
27
+ webhook: 'webhook',
28
+ };
29
+ return mapping[channel] || 'webhook';
30
+ }
31
+ /**
32
+ * Get available transports for a worker
33
+ */
34
+ export function getWorkerTransports(worker) {
35
+ const transports = [];
36
+ const contacts = worker.contacts;
37
+ if (contacts.email)
38
+ transports.push('email');
39
+ if (contacts.slack)
40
+ transports.push('slack');
41
+ if (contacts.teams)
42
+ transports.push('teams');
43
+ if (contacts.discord)
44
+ transports.push('discord');
45
+ if (contacts.phone)
46
+ transports.push('voice');
47
+ if (contacts.sms)
48
+ transports.push('sms');
49
+ if (contacts.whatsapp)
50
+ transports.push('whatsapp');
51
+ if (contacts.telegram)
52
+ transports.push('telegram');
53
+ if (contacts.web)
54
+ transports.push('web');
55
+ if (contacts.webhook)
56
+ transports.push('webhook');
57
+ return transports;
58
+ }
59
+ /**
60
+ * Get team transports (union of all member transports + team contacts)
61
+ */
62
+ export function getTeamTransports(team) {
63
+ const transports = new Set();
64
+ // Add team-level contacts
65
+ const contacts = team.contacts;
66
+ if (contacts.email)
67
+ transports.add('email');
68
+ if (contacts.slack)
69
+ transports.add('slack');
70
+ if (contacts.teams)
71
+ transports.add('teams');
72
+ if (contacts.discord)
73
+ transports.add('discord');
74
+ if (contacts.phone)
75
+ transports.add('voice');
76
+ if (contacts.sms)
77
+ transports.add('sms');
78
+ if (contacts.whatsapp)
79
+ transports.add('whatsapp');
80
+ if (contacts.telegram)
81
+ transports.add('telegram');
82
+ if (contacts.web)
83
+ transports.add('web');
84
+ if (contacts.webhook)
85
+ transports.add('webhook');
86
+ return Array.from(transports);
87
+ }
88
+ /**
89
+ * Resolve contact to address
90
+ */
91
+ export function resolveAddress(contacts, channel) {
92
+ const contact = contacts[channel];
93
+ if (!contact)
94
+ return null;
95
+ const transport = channelToTransport(channel);
96
+ if (typeof contact === 'string') {
97
+ return { transport, value: contact };
98
+ }
99
+ // Handle structured contact types
100
+ switch (channel) {
101
+ case 'email':
102
+ const emailContact = contact;
103
+ return { transport, value: emailContact.address, name: emailContact.name };
104
+ case 'phone':
105
+ case 'sms':
106
+ case 'whatsapp':
107
+ const phoneContact = contact;
108
+ return { transport, value: phoneContact.number };
109
+ case 'slack':
110
+ const slackContact = contact;
111
+ return {
112
+ transport,
113
+ value: slackContact.user || slackContact.channel || '',
114
+ metadata: { workspace: slackContact.workspace },
115
+ };
116
+ case 'teams':
117
+ const teamsContact = contact;
118
+ return {
119
+ transport,
120
+ value: teamsContact.user || teamsContact.channel || '',
121
+ metadata: { team: teamsContact.team },
122
+ };
123
+ case 'discord':
124
+ const discordContact = contact;
125
+ return {
126
+ transport,
127
+ value: discordContact.user || discordContact.channel || '',
128
+ metadata: { server: discordContact.server },
129
+ };
130
+ case 'telegram':
131
+ const telegramContact = contact;
132
+ return { transport, value: telegramContact.user || telegramContact.chat || '' };
133
+ case 'web':
134
+ const webContact = contact;
135
+ return { transport, value: webContact.userId || '', metadata: { url: webContact.url } };
136
+ case 'webhook':
137
+ const webhookContact = contact;
138
+ return { transport, value: webhookContact.url, metadata: { secret: webhookContact.secret } };
139
+ case 'api':
140
+ const apiContact = contact;
141
+ return { transport, value: apiContact.endpoint, metadata: { auth: apiContact.auth } };
142
+ default:
143
+ return null;
144
+ }
145
+ }
146
+ /**
147
+ * Resolve all addresses for a worker
148
+ */
149
+ export function resolveWorkerAddresses(worker) {
150
+ const addresses = [];
151
+ const channels = [
152
+ 'email', 'slack', 'teams', 'discord', 'phone', 'sms',
153
+ 'whatsapp', 'telegram', 'web', 'api', 'webhook',
154
+ ];
155
+ for (const channel of channels) {
156
+ const address = resolveAddress(worker.contacts, channel);
157
+ if (address)
158
+ addresses.push(address);
159
+ }
160
+ return addresses;
161
+ }
162
+ /**
163
+ * Get primary address for a worker based on preferences
164
+ */
165
+ export function getPrimaryAddress(worker) {
166
+ const preferences = worker.preferences;
167
+ if (preferences?.primary) {
168
+ return resolveAddress(worker.contacts, preferences.primary);
169
+ }
170
+ // Default priority: slack > email > teams > sms > phone
171
+ const defaultPriority = ['slack', 'email', 'teams', 'sms', 'phone'];
172
+ for (const channel of defaultPriority) {
173
+ const address = resolveAddress(worker.contacts, channel);
174
+ if (address)
175
+ return address;
176
+ }
177
+ // Fall back to any available
178
+ const addresses = resolveWorkerAddresses(worker);
179
+ return addresses[0] || null;
180
+ }
181
+ /**
182
+ * Transport registry
183
+ */
184
+ const transportRegistry = new Map();
185
+ /**
186
+ * Register a transport handler
187
+ */
188
+ export function registerTransport(transport, handler) {
189
+ transportRegistry.set(transport, handler);
190
+ }
191
+ /**
192
+ * Get transport handler
193
+ */
194
+ export function getTransportHandler(transport) {
195
+ return transportRegistry.get(transport);
196
+ }
197
+ /**
198
+ * Check if transport is registered
199
+ */
200
+ export function hasTransport(transport) {
201
+ return transportRegistry.has(transport);
202
+ }
203
+ /**
204
+ * List registered transports
205
+ */
206
+ export function listTransports() {
207
+ return Array.from(transportRegistry.keys());
208
+ }
209
+ // =============================================================================
210
+ // Default Transport Handlers (Stubs - implemented by providers)
211
+ // =============================================================================
212
+ /**
213
+ * Send via transport
214
+ */
215
+ export async function sendViaTransport(transport, payload, config) {
216
+ const handler = transportRegistry.get(transport);
217
+ if (!handler) {
218
+ return {
219
+ success: false,
220
+ transport,
221
+ error: `Transport '${transport}' not registered`,
222
+ };
223
+ }
224
+ try {
225
+ return await handler(payload, config || { transport });
226
+ }
227
+ catch (error) {
228
+ return {
229
+ success: false,
230
+ transport,
231
+ error: error instanceof Error ? error.message : String(error),
232
+ };
233
+ }
234
+ }
235
+ /**
236
+ * Send to multiple transports (fan-out)
237
+ */
238
+ export async function sendToMultipleTransports(transports, payload, configs) {
239
+ const results = await Promise.all(transports.map(transport => sendViaTransport(transport, payload, configs?.[transport])));
240
+ return results;
241
+ }
242
+ // =============================================================================
243
+ // Worker Action to Transport
244
+ // =============================================================================
245
+ /**
246
+ * Build message payload from notify action
247
+ */
248
+ export function buildNotifyPayload(action) {
249
+ return {
250
+ to: resolveActionTarget(action.object),
251
+ body: action.message,
252
+ type: 'notification',
253
+ priority: action.priority || 'normal',
254
+ metadata: action.metadata,
255
+ };
256
+ }
257
+ /**
258
+ * Build message payload from ask action
259
+ */
260
+ export function buildAskPayload(action) {
261
+ return {
262
+ to: resolveActionTarget(action.object),
263
+ body: action.question,
264
+ type: 'question',
265
+ schema: action.schema,
266
+ timeout: action.timeout,
267
+ metadata: action.metadata,
268
+ };
269
+ }
270
+ /**
271
+ * Build message payload from approve action
272
+ */
273
+ export function buildApprovePayload(action) {
274
+ return {
275
+ to: resolveActionTarget(action.object),
276
+ body: action.request,
277
+ type: 'approval',
278
+ timeout: action.timeout,
279
+ actions: [
280
+ { id: 'approve', label: 'Approve', style: 'primary', value: true },
281
+ { id: 'reject', label: 'Reject', style: 'danger', value: false },
282
+ ],
283
+ metadata: {
284
+ ...action.metadata,
285
+ context: action.context,
286
+ },
287
+ };
288
+ }
289
+ /**
290
+ * Resolve action target to address string
291
+ */
292
+ function resolveActionTarget(target) {
293
+ if (typeof target === 'string')
294
+ return target;
295
+ if ('contacts' in target) {
296
+ const address = getPrimaryAddress(target);
297
+ return address?.value || target.id;
298
+ }
299
+ return target.id;
300
+ }
301
+ // =============================================================================
302
+ // Integration with digital-tools Message/Call types
303
+ // =============================================================================
304
+ /**
305
+ * Message type mapping (from digital-tools)
306
+ */
307
+ export const MessageTypeMapping = {
308
+ email: 'email',
309
+ sms: 'text',
310
+ slack: 'chat',
311
+ teams: 'chat',
312
+ discord: 'chat',
313
+ whatsapp: 'text',
314
+ telegram: 'text',
315
+ voice: 'voicemail', // For voicemail messages
316
+ };
317
+ /**
318
+ * Call type mapping (from digital-tools)
319
+ */
320
+ export const CallTypeMapping = {
321
+ phone: 'phone',
322
+ voice: 'phone',
323
+ web: 'web',
324
+ video: 'video',
325
+ };
326
+ /**
327
+ * Convert worker notification to digital-tools Message format
328
+ */
329
+ export function toDigitalToolsMessage(payload, transport) {
330
+ const messageType = MessageTypeMapping[transport] || 'chat';
331
+ return {
332
+ type: messageType,
333
+ to: Array.isArray(payload.to) ? payload.to : [payload.to],
334
+ from: payload.from,
335
+ subject: payload.subject,
336
+ body: payload.body,
337
+ html: payload.html,
338
+ metadata: {
339
+ ...payload.metadata,
340
+ workerActionType: payload.type,
341
+ priority: payload.priority,
342
+ },
343
+ };
344
+ }
345
+ /**
346
+ * Convert digital-tools Message to worker notification format
347
+ */
348
+ export function fromDigitalToolsMessage(message) {
349
+ return {
350
+ to: message.to,
351
+ from: message.from,
352
+ subject: message.subject,
353
+ body: message.body,
354
+ html: message.html,
355
+ metadata: message.metadata,
356
+ };
357
+ }
package/src/types.js ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Type definitions for digital-workers
3
+ *
4
+ * Digital workers (Agents and Humans) communicate through Actions that integrate
5
+ * with the ai-workflows system. Worker actions (notify, ask, approve, decide)
6
+ * are durable workflow actions with Actor/Object semantics.
7
+ *
8
+ * ## Key Concepts
9
+ *
10
+ * - **Worker**: Common interface for Agent and Human
11
+ * - **Contacts**: How a worker can be reached (email, slack, phone, etc.)
12
+ * - **Action**: Durable workflow action (notify, ask, approve, decide)
13
+ * - **Team**: Group of workers with shared contacts
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+ // ============================================================================
18
+ // Worker Verbs - Following ai-database Verb pattern
19
+ // ============================================================================
20
+ /**
21
+ * Worker verbs following the ai-database conjugation pattern
22
+ *
23
+ * Each verb has:
24
+ * - action: Base form (notify, ask, approve, decide)
25
+ * - actor: Who does it (notifier, asker, approver, decider)
26
+ * - activity: Gerund (notifying, asking, approving, deciding)
27
+ * - reverse: Past forms (notifiedAt, notifiedBy, askedAt, etc.)
28
+ */
29
+ export const WorkerVerbs = {
30
+ notify: {
31
+ action: 'notify',
32
+ actor: 'notifier',
33
+ act: 'notifies',
34
+ activity: 'notifying',
35
+ result: 'notification',
36
+ reverse: { at: 'notifiedAt', by: 'notifiedBy', via: 'notifiedVia' },
37
+ },
38
+ ask: {
39
+ action: 'ask',
40
+ actor: 'asker',
41
+ act: 'asks',
42
+ activity: 'asking',
43
+ result: 'question',
44
+ reverse: { at: 'askedAt', by: 'askedBy', via: 'askedVia' },
45
+ },
46
+ approve: {
47
+ action: 'approve',
48
+ actor: 'approver',
49
+ act: 'approves',
50
+ activity: 'approving',
51
+ result: 'approval',
52
+ reverse: { at: 'approvedAt', by: 'approvedBy', via: 'approvedVia' },
53
+ inverse: 'reject',
54
+ },
55
+ decide: {
56
+ action: 'decide',
57
+ actor: 'decider',
58
+ act: 'decides',
59
+ activity: 'deciding',
60
+ result: 'decision',
61
+ reverse: { at: 'decidedAt', by: 'decidedBy' },
62
+ },
63
+ do: {
64
+ action: 'do',
65
+ actor: 'doer',
66
+ act: 'does',
67
+ activity: 'doing',
68
+ result: 'task',
69
+ reverse: { at: 'doneAt', by: 'doneBy' },
70
+ },
71
+ };