human-in-the-loop 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/human.js ADDED
@@ -0,0 +1,475 @@
1
+ /**
2
+ * Human-in-the-loop primitives implementation
3
+ */
4
+ import { InMemoryHumanStore } from './store.js';
5
+ /**
6
+ * Human-in-the-loop manager
7
+ *
8
+ * Provides primitives for integrating human oversight and intervention
9
+ * in AI workflows.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const human = Human({
14
+ * defaultTimeout: 3600000, // 1 hour
15
+ * autoEscalate: true,
16
+ * })
17
+ *
18
+ * // Request approval
19
+ * const approval = await human.approve({
20
+ * title: 'Deploy to production',
21
+ * description: 'Approve deployment of v2.0.0',
22
+ * subject: 'Production Deployment',
23
+ * assignee: 'tech-lead@example.com',
24
+ * priority: 'high',
25
+ * })
26
+ *
27
+ * if (approval.approved) {
28
+ * await deploy()
29
+ * }
30
+ * ```
31
+ */
32
+ export class HumanManager {
33
+ store;
34
+ options;
35
+ roles = new Map();
36
+ teams = new Map();
37
+ humans = new Map();
38
+ escalationPolicies = new Map();
39
+ workflows = new Map();
40
+ constructor(options = {}) {
41
+ this.store = options.store || new InMemoryHumanStore();
42
+ this.options = {
43
+ store: this.store,
44
+ defaultTimeout: options.defaultTimeout || 0, // No timeout by default
45
+ defaultPriority: options.defaultPriority || 'normal',
46
+ escalationPolicies: options.escalationPolicies || [],
47
+ autoEscalate: options.autoEscalate ?? false,
48
+ };
49
+ // Register escalation policies
50
+ for (const policy of this.options.escalationPolicies) {
51
+ this.escalationPolicies.set(policy.id, policy);
52
+ }
53
+ }
54
+ /**
55
+ * Define a role
56
+ */
57
+ defineRole(role) {
58
+ this.roles.set(role.id, role);
59
+ return role;
60
+ }
61
+ /**
62
+ * Get a role by ID
63
+ */
64
+ getRole(id) {
65
+ return this.roles.get(id);
66
+ }
67
+ /**
68
+ * Define a team
69
+ */
70
+ defineTeam(team) {
71
+ this.teams.set(team.id, team);
72
+ return team;
73
+ }
74
+ /**
75
+ * Get a team by ID
76
+ */
77
+ getTeam(id) {
78
+ return this.teams.get(id);
79
+ }
80
+ /**
81
+ * Register a human worker
82
+ */
83
+ registerHuman(human) {
84
+ this.humans.set(human.id, human);
85
+ return human;
86
+ }
87
+ /**
88
+ * Get a human by ID
89
+ */
90
+ getHuman(id) {
91
+ return this.humans.get(id);
92
+ }
93
+ /**
94
+ * Request approval from a human
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * const result = await human.approve({
99
+ * title: 'Approve expense',
100
+ * description: 'Employee expense claim for $150',
101
+ * subject: 'Expense Claim #1234',
102
+ * input: { amount: 150, category: 'Travel' },
103
+ * assignee: 'manager@example.com',
104
+ * priority: 'normal',
105
+ * })
106
+ * ```
107
+ */
108
+ async approve(params) {
109
+ const request = await this.store.create({
110
+ type: 'approval',
111
+ status: 'pending',
112
+ title: params.title,
113
+ description: params.description,
114
+ subject: params.subject,
115
+ input: params.input,
116
+ assignee: params.assignee,
117
+ role: params.role,
118
+ team: params.team,
119
+ priority: params.priority || this.options.defaultPriority,
120
+ timeout: params.timeout || this.options.defaultTimeout,
121
+ escalatesTo: params.escalatesTo,
122
+ requiresApproval: params.requiresApproval ?? true,
123
+ approvers: params.approvers,
124
+ currentApproverIndex: 0,
125
+ metadata: params.metadata,
126
+ });
127
+ // In a real implementation, this would:
128
+ // 1. Send notification to assignee
129
+ // 2. Wait for response (polling, webhook, or event)
130
+ // 3. Handle timeout and escalation
131
+ // 4. Return the response
132
+ // For now, return the request ID as a placeholder
133
+ return this.waitForResponse(request);
134
+ }
135
+ /**
136
+ * Ask a question to a human
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * const answer = await human.ask({
141
+ * title: 'Product naming',
142
+ * question: 'What should we name the new feature?',
143
+ * context: { feature: 'AI Assistant' },
144
+ * assignee: 'product-manager@example.com',
145
+ * })
146
+ * ```
147
+ */
148
+ async ask(params) {
149
+ const request = await this.store.create({
150
+ type: 'question',
151
+ status: 'pending',
152
+ title: params.title,
153
+ description: params.question,
154
+ question: params.question,
155
+ input: { question: params.question, context: params.context },
156
+ context: params.context,
157
+ suggestions: params.suggestions,
158
+ assignee: params.assignee,
159
+ role: params.role,
160
+ team: params.team,
161
+ priority: params.priority || this.options.defaultPriority,
162
+ timeout: params.timeout || this.options.defaultTimeout,
163
+ metadata: params.metadata,
164
+ });
165
+ return this.waitForResponse(request);
166
+ }
167
+ /**
168
+ * Request a human to perform a task
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * const result = await human.do({
173
+ * title: 'Review code',
174
+ * instructions: 'Review the PR and provide feedback',
175
+ * input: { prUrl: 'https://github.com/...' },
176
+ * assignee: 'senior-dev@example.com',
177
+ * })
178
+ * ```
179
+ */
180
+ async do(params) {
181
+ const request = await this.store.create({
182
+ type: 'task',
183
+ status: 'pending',
184
+ title: params.title,
185
+ description: params.instructions,
186
+ instructions: params.instructions,
187
+ input: params.input,
188
+ assignee: params.assignee,
189
+ role: params.role,
190
+ team: params.team,
191
+ priority: params.priority || this.options.defaultPriority,
192
+ timeout: params.timeout || this.options.defaultTimeout,
193
+ tools: params.tools,
194
+ estimatedEffort: params.estimatedEffort,
195
+ metadata: params.metadata,
196
+ });
197
+ return this.waitForResponse(request);
198
+ }
199
+ /**
200
+ * Request a human to make a decision
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * const choice = await human.decide({
205
+ * title: 'Pick deployment strategy',
206
+ * options: ['blue-green', 'canary', 'rolling'],
207
+ * context: { risk: 'high', users: 100000 },
208
+ * assignee: 'devops-lead@example.com',
209
+ * })
210
+ * ```
211
+ */
212
+ async decide(params) {
213
+ const request = await this.store.create({
214
+ type: 'decision',
215
+ status: 'pending',
216
+ title: params.title,
217
+ description: params.description || `Choose from: ${params.options.join(', ')}`,
218
+ input: { options: params.options, context: params.context },
219
+ options: params.options,
220
+ context: params.context,
221
+ criteria: params.criteria,
222
+ assignee: params.assignee,
223
+ role: params.role,
224
+ team: params.team,
225
+ priority: params.priority || this.options.defaultPriority,
226
+ timeout: params.timeout || this.options.defaultTimeout,
227
+ metadata: params.metadata,
228
+ });
229
+ return this.waitForResponse(request);
230
+ }
231
+ /**
232
+ * Request a human to review content
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * const review = await human.review({
237
+ * title: 'Review blog post',
238
+ * content: { title: 'My Post', body: '...' },
239
+ * reviewType: 'content',
240
+ * criteria: ['Grammar', 'Tone', 'Accuracy'],
241
+ * assignee: 'editor@example.com',
242
+ * })
243
+ * ```
244
+ */
245
+ async review(params) {
246
+ const request = await this.store.create({
247
+ type: 'review',
248
+ status: 'pending',
249
+ title: params.title,
250
+ description: params.description || `Review requested: ${params.reviewType || 'other'}`,
251
+ input: params.content,
252
+ content: params.content,
253
+ reviewType: params.reviewType,
254
+ criteria: params.criteria,
255
+ assignee: params.assignee,
256
+ role: params.role,
257
+ team: params.team,
258
+ priority: params.priority || this.options.defaultPriority,
259
+ timeout: params.timeout || this.options.defaultTimeout,
260
+ metadata: params.metadata,
261
+ });
262
+ return this.waitForResponse(request);
263
+ }
264
+ /**
265
+ * Send a notification to a human
266
+ *
267
+ * @example
268
+ * ```ts
269
+ * await human.notify({
270
+ * type: 'info',
271
+ * title: 'Deployment complete',
272
+ * message: 'Version 2.0.0 deployed successfully',
273
+ * recipient: 'team@example.com',
274
+ * channels: ['slack', 'email'],
275
+ * })
276
+ * ```
277
+ */
278
+ async notify(params) {
279
+ const notification = {
280
+ id: `notif_${Date.now()}_${Math.random().toString(36).slice(2)}`,
281
+ type: params.type,
282
+ title: params.title,
283
+ message: params.message,
284
+ recipient: params.recipient,
285
+ channels: params.channels,
286
+ priority: params.priority,
287
+ data: params.data,
288
+ createdAt: new Date(),
289
+ };
290
+ // In a real implementation, this would:
291
+ // 1. Send the notification via specified channels
292
+ // 2. Track delivery status
293
+ // 3. Handle failures and retries
294
+ return notification;
295
+ }
296
+ /**
297
+ * Get a review queue
298
+ *
299
+ * @example
300
+ * ```ts
301
+ * const queue = await human.getQueue({
302
+ * name: 'Pending Approvals',
303
+ * filters: {
304
+ * status: ['pending'],
305
+ * priority: ['high', 'critical'],
306
+ * },
307
+ * })
308
+ * ```
309
+ */
310
+ async getQueue(params) {
311
+ const items = await this.store.list(params.filters, params.limit);
312
+ return {
313
+ id: `queue_${Date.now()}`,
314
+ name: params.name,
315
+ description: params.description,
316
+ items,
317
+ filters: params.filters,
318
+ sortBy: params.sortBy,
319
+ sortDirection: params.sortDirection,
320
+ };
321
+ }
322
+ /**
323
+ * Get a request by ID
324
+ */
325
+ async getRequest(id) {
326
+ return this.store.get(id);
327
+ }
328
+ /**
329
+ * Complete a request with a response
330
+ */
331
+ async completeRequest(id, response) {
332
+ return this.store.complete(id, response);
333
+ }
334
+ /**
335
+ * Reject a request
336
+ */
337
+ async rejectRequest(id, reason) {
338
+ return this.store.reject(id, reason);
339
+ }
340
+ /**
341
+ * Escalate a request
342
+ */
343
+ async escalateRequest(id, to) {
344
+ return this.store.escalate(id, to);
345
+ }
346
+ /**
347
+ * Cancel a request
348
+ */
349
+ async cancelRequest(id) {
350
+ return this.store.cancel(id);
351
+ }
352
+ /**
353
+ * Define or update goals
354
+ */
355
+ defineGoals(goals) {
356
+ // In a real implementation, this would persist goals
357
+ return goals;
358
+ }
359
+ /**
360
+ * Track KPIs
361
+ */
362
+ trackKPIs(kpis) {
363
+ // In a real implementation, this would persist KPIs
364
+ return kpis;
365
+ }
366
+ /**
367
+ * Define or update OKRs
368
+ */
369
+ defineOKRs(okrs) {
370
+ // In a real implementation, this would persist OKRs
371
+ return okrs;
372
+ }
373
+ /**
374
+ * Create an approval workflow
375
+ */
376
+ createWorkflow(workflow) {
377
+ this.workflows.set(workflow.id, workflow);
378
+ return workflow;
379
+ }
380
+ /**
381
+ * Get a workflow by ID
382
+ */
383
+ getWorkflow(id) {
384
+ return this.workflows.get(id);
385
+ }
386
+ /**
387
+ * Wait for a human response
388
+ *
389
+ * In a real implementation, this would:
390
+ * 1. Poll the store for updates
391
+ * 2. Listen for webhooks/events
392
+ * 3. Handle timeouts and escalations
393
+ * 4. Return the response when available
394
+ *
395
+ * For now, this throws an error to indicate manual completion is needed
396
+ */
397
+ async waitForResponse(request) {
398
+ // Check if there's a timeout
399
+ if (request.timeout && request.timeout > 0) {
400
+ // Set up timeout handler
401
+ const timeoutPromise = new Promise((_, reject) => {
402
+ setTimeout(() => {
403
+ reject(new Error(`Request ${request.id} timed out after ${request.timeout}ms`));
404
+ }, request.timeout);
405
+ });
406
+ // Poll for completion
407
+ const pollPromise = this.pollForCompletion(request.id);
408
+ // Race between timeout and completion
409
+ try {
410
+ return await Promise.race([pollPromise, timeoutPromise]);
411
+ }
412
+ catch (error) {
413
+ // On timeout, escalate if configured
414
+ if (this.options.autoEscalate && request.escalatesTo) {
415
+ const escalateTo = Array.isArray(request.escalatesTo)
416
+ ? request.escalatesTo[0]
417
+ : request.escalatesTo;
418
+ if (escalateTo) {
419
+ await this.store.escalate(request.id, escalateTo);
420
+ }
421
+ else {
422
+ await this.store.update(request.id, { status: 'timeout' });
423
+ }
424
+ }
425
+ else {
426
+ await this.store.update(request.id, { status: 'timeout' });
427
+ }
428
+ throw error;
429
+ }
430
+ }
431
+ // No timeout, just poll indefinitely
432
+ return this.pollForCompletion(request.id);
433
+ }
434
+ /**
435
+ * Poll for request completion
436
+ */
437
+ async pollForCompletion(requestId) {
438
+ // In a real implementation, use webhooks, WebSockets, or event emitters
439
+ // This is a simplified polling implementation
440
+ const pollInterval = 1000; // 1 second
441
+ while (true) {
442
+ const request = await this.store.get(requestId);
443
+ if (!request) {
444
+ throw new Error(`Request ${requestId} not found`);
445
+ }
446
+ if (request.status === 'completed' && request.response) {
447
+ return request.response;
448
+ }
449
+ if (request.status === 'rejected') {
450
+ throw new Error(`Request ${requestId} was rejected: ${request.rejectionReason}`);
451
+ }
452
+ if (request.status === 'cancelled') {
453
+ throw new Error(`Request ${requestId} was cancelled`);
454
+ }
455
+ // Wait before polling again
456
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
457
+ }
458
+ }
459
+ }
460
+ /**
461
+ * Create a Human-in-the-loop manager instance
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * import { Human } from 'human-in-the-loop'
466
+ *
467
+ * const human = Human({
468
+ * defaultTimeout: 3600000, // 1 hour
469
+ * autoEscalate: true,
470
+ * })
471
+ * ```
472
+ */
473
+ export function Human(options) {
474
+ return new HumanManager(options);
475
+ }