human-in-the-loop 0.1.0 → 2.0.2
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/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +17 -0
- package/README.md +600 -166
- package/dist/helpers.d.ts +308 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +275 -0
- package/dist/helpers.js.map +1 -0
- package/dist/human.d.ts +315 -0
- package/dist/human.d.ts.map +1 -0
- package/dist/human.js +476 -0
- package/dist/human.js.map +1 -0
- package/dist/index.d.ts +48 -343
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -793
- package/dist/index.js.map +1 -0
- package/dist/store.d.ts +61 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +162 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +399 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/examples/basic-usage.ts +386 -0
- package/package.json +23 -54
- package/src/helpers.ts +379 -0
- package/src/human.test.ts +384 -0
- package/src/human.ts +626 -0
- package/src/index.ts +102 -6
- package/src/store.ts +201 -0
- package/src/types.ts +431 -0
- package/tsconfig.json +6 -11
- package/TODO.md +0 -53
- package/dist/index.cjs +0 -899
- package/dist/index.d.cts +0 -344
- package/src/core/factory.test.ts +0 -69
- package/src/core/factory.ts +0 -30
- package/src/core/types.ts +0 -191
- package/src/platforms/email/index.tsx +0 -137
- package/src/platforms/react/index.tsx +0 -218
- package/src/platforms/slack/index.ts +0 -84
- package/src/platforms/teams/index.ts +0 -84
- package/vitest.config.ts +0 -15
package/src/human.ts
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-in-the-loop primitives implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Role,
|
|
7
|
+
Team,
|
|
8
|
+
Human as HumanType,
|
|
9
|
+
Goals,
|
|
10
|
+
KPIs,
|
|
11
|
+
OKRs,
|
|
12
|
+
HumanOptions,
|
|
13
|
+
HumanStore,
|
|
14
|
+
ApprovalRequest,
|
|
15
|
+
ApprovalResponse,
|
|
16
|
+
QuestionRequest,
|
|
17
|
+
TaskRequest,
|
|
18
|
+
DecisionRequest,
|
|
19
|
+
ReviewRequest,
|
|
20
|
+
ReviewResponse,
|
|
21
|
+
Notification,
|
|
22
|
+
ReviewQueue,
|
|
23
|
+
EscalationPolicy,
|
|
24
|
+
ApprovalWorkflow,
|
|
25
|
+
Priority,
|
|
26
|
+
HumanRequest,
|
|
27
|
+
} from './types.js'
|
|
28
|
+
import { InMemoryHumanStore } from './store.js'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Human-in-the-loop manager
|
|
32
|
+
*
|
|
33
|
+
* Provides primitives for integrating human oversight and intervention
|
|
34
|
+
* in AI workflows.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const human = Human({
|
|
39
|
+
* defaultTimeout: 3600000, // 1 hour
|
|
40
|
+
* autoEscalate: true,
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* // Request approval
|
|
44
|
+
* const approval = await human.approve({
|
|
45
|
+
* title: 'Deploy to production',
|
|
46
|
+
* description: 'Approve deployment of v2.0.0',
|
|
47
|
+
* subject: 'Production Deployment',
|
|
48
|
+
* assignee: 'tech-lead@example.com',
|
|
49
|
+
* priority: 'high',
|
|
50
|
+
* })
|
|
51
|
+
*
|
|
52
|
+
* if (approval.approved) {
|
|
53
|
+
* await deploy()
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export class HumanManager {
|
|
58
|
+
private store: HumanStore
|
|
59
|
+
private options: Required<HumanOptions>
|
|
60
|
+
private roles = new Map<string, Role>()
|
|
61
|
+
private teams = new Map<string, Team>()
|
|
62
|
+
private humans = new Map<string, HumanType>()
|
|
63
|
+
private escalationPolicies = new Map<string, EscalationPolicy>()
|
|
64
|
+
private workflows = new Map<string, ApprovalWorkflow>()
|
|
65
|
+
|
|
66
|
+
constructor(options: HumanOptions = {}) {
|
|
67
|
+
this.store = options.store || new InMemoryHumanStore()
|
|
68
|
+
this.options = {
|
|
69
|
+
store: this.store,
|
|
70
|
+
defaultTimeout: options.defaultTimeout || 0, // No timeout by default
|
|
71
|
+
defaultPriority: options.defaultPriority || 'normal',
|
|
72
|
+
escalationPolicies: options.escalationPolicies || [],
|
|
73
|
+
autoEscalate: options.autoEscalate ?? false,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Register escalation policies
|
|
77
|
+
for (const policy of this.options.escalationPolicies) {
|
|
78
|
+
this.escalationPolicies.set(policy.id, policy)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Define a role
|
|
84
|
+
*/
|
|
85
|
+
defineRole(role: Role): Role {
|
|
86
|
+
this.roles.set(role.id, role)
|
|
87
|
+
return role
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get a role by ID
|
|
92
|
+
*/
|
|
93
|
+
getRole(id: string): Role | undefined {
|
|
94
|
+
return this.roles.get(id)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Define a team
|
|
99
|
+
*/
|
|
100
|
+
defineTeam(team: Team): Team {
|
|
101
|
+
this.teams.set(team.id, team)
|
|
102
|
+
return team
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get a team by ID
|
|
107
|
+
*/
|
|
108
|
+
getTeam(id: string): Team | undefined {
|
|
109
|
+
return this.teams.get(id)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Register a human worker
|
|
114
|
+
*/
|
|
115
|
+
registerHuman(human: HumanType): HumanType {
|
|
116
|
+
this.humans.set(human.id, human)
|
|
117
|
+
return human
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get a human by ID
|
|
122
|
+
*/
|
|
123
|
+
getHuman(id: string): HumanType | undefined {
|
|
124
|
+
return this.humans.get(id)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Request approval from a human
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* const result = await human.approve({
|
|
133
|
+
* title: 'Approve expense',
|
|
134
|
+
* description: 'Employee expense claim for $150',
|
|
135
|
+
* subject: 'Expense Claim #1234',
|
|
136
|
+
* input: { amount: 150, category: 'Travel' },
|
|
137
|
+
* assignee: 'manager@example.com',
|
|
138
|
+
* priority: 'normal',
|
|
139
|
+
* })
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
async approve<TData = unknown>(params: {
|
|
143
|
+
title: string
|
|
144
|
+
description: string
|
|
145
|
+
subject: string
|
|
146
|
+
input: TData
|
|
147
|
+
assignee?: string | string[]
|
|
148
|
+
role?: string
|
|
149
|
+
team?: string
|
|
150
|
+
priority?: Priority
|
|
151
|
+
timeout?: number
|
|
152
|
+
escalatesTo?: string | string[]
|
|
153
|
+
requiresApproval?: boolean
|
|
154
|
+
approvers?: string[]
|
|
155
|
+
metadata?: Record<string, unknown>
|
|
156
|
+
}): Promise<ApprovalResponse> {
|
|
157
|
+
const request = await this.store.create<ApprovalRequest<TData>>({
|
|
158
|
+
type: 'approval',
|
|
159
|
+
status: 'pending',
|
|
160
|
+
title: params.title,
|
|
161
|
+
description: params.description,
|
|
162
|
+
subject: params.subject,
|
|
163
|
+
input: params.input,
|
|
164
|
+
assignee: params.assignee,
|
|
165
|
+
role: params.role,
|
|
166
|
+
team: params.team,
|
|
167
|
+
priority: params.priority || this.options.defaultPriority,
|
|
168
|
+
timeout: params.timeout || this.options.defaultTimeout,
|
|
169
|
+
escalatesTo: params.escalatesTo,
|
|
170
|
+
requiresApproval: params.requiresApproval ?? true,
|
|
171
|
+
approvers: params.approvers,
|
|
172
|
+
currentApproverIndex: 0,
|
|
173
|
+
metadata: params.metadata,
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// In a real implementation, this would:
|
|
177
|
+
// 1. Send notification to assignee
|
|
178
|
+
// 2. Wait for response (polling, webhook, or event)
|
|
179
|
+
// 3. Handle timeout and escalation
|
|
180
|
+
// 4. Return the response
|
|
181
|
+
|
|
182
|
+
// For now, return the request ID as a placeholder
|
|
183
|
+
return this.waitForResponse<ApprovalRequest<TData>, ApprovalResponse>(request)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Ask a question to a human
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* const answer = await human.ask({
|
|
192
|
+
* title: 'Product naming',
|
|
193
|
+
* question: 'What should we name the new feature?',
|
|
194
|
+
* context: { feature: 'AI Assistant' },
|
|
195
|
+
* assignee: 'product-manager@example.com',
|
|
196
|
+
* })
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
async ask(params: {
|
|
200
|
+
title: string
|
|
201
|
+
question: string
|
|
202
|
+
context?: unknown
|
|
203
|
+
assignee?: string | string[]
|
|
204
|
+
role?: string
|
|
205
|
+
team?: string
|
|
206
|
+
priority?: Priority
|
|
207
|
+
timeout?: number
|
|
208
|
+
suggestions?: string[]
|
|
209
|
+
metadata?: Record<string, unknown>
|
|
210
|
+
}): Promise<string> {
|
|
211
|
+
const request = await this.store.create<QuestionRequest>({
|
|
212
|
+
type: 'question',
|
|
213
|
+
status: 'pending',
|
|
214
|
+
title: params.title,
|
|
215
|
+
description: params.question,
|
|
216
|
+
question: params.question,
|
|
217
|
+
input: { question: params.question, context: params.context },
|
|
218
|
+
context: params.context,
|
|
219
|
+
suggestions: params.suggestions,
|
|
220
|
+
assignee: params.assignee,
|
|
221
|
+
role: params.role,
|
|
222
|
+
team: params.team,
|
|
223
|
+
priority: params.priority || this.options.defaultPriority,
|
|
224
|
+
timeout: params.timeout || this.options.defaultTimeout,
|
|
225
|
+
metadata: params.metadata,
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
return this.waitForResponse<QuestionRequest, string>(request)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Request a human to perform a task
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```ts
|
|
236
|
+
* const result = await human.do({
|
|
237
|
+
* title: 'Review code',
|
|
238
|
+
* instructions: 'Review the PR and provide feedback',
|
|
239
|
+
* input: { prUrl: 'https://github.com/...' },
|
|
240
|
+
* assignee: 'senior-dev@example.com',
|
|
241
|
+
* })
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
async do<TInput = unknown, TOutput = unknown>(params: {
|
|
245
|
+
title: string
|
|
246
|
+
instructions: string
|
|
247
|
+
input: TInput
|
|
248
|
+
assignee?: string | string[]
|
|
249
|
+
role?: string
|
|
250
|
+
team?: string
|
|
251
|
+
priority?: Priority
|
|
252
|
+
timeout?: number
|
|
253
|
+
tools?: any[]
|
|
254
|
+
estimatedEffort?: string
|
|
255
|
+
metadata?: Record<string, unknown>
|
|
256
|
+
}): Promise<TOutput> {
|
|
257
|
+
const request = await this.store.create<TaskRequest<TInput, TOutput>>({
|
|
258
|
+
type: 'task',
|
|
259
|
+
status: 'pending',
|
|
260
|
+
title: params.title,
|
|
261
|
+
description: params.instructions,
|
|
262
|
+
instructions: params.instructions,
|
|
263
|
+
input: params.input,
|
|
264
|
+
assignee: params.assignee,
|
|
265
|
+
role: params.role,
|
|
266
|
+
team: params.team,
|
|
267
|
+
priority: params.priority || this.options.defaultPriority,
|
|
268
|
+
timeout: params.timeout || this.options.defaultTimeout,
|
|
269
|
+
tools: params.tools,
|
|
270
|
+
estimatedEffort: params.estimatedEffort,
|
|
271
|
+
metadata: params.metadata,
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
return this.waitForResponse<TaskRequest<TInput, TOutput>, TOutput>(request)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Request a human to make a decision
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* ```ts
|
|
282
|
+
* const choice = await human.decide({
|
|
283
|
+
* title: 'Pick deployment strategy',
|
|
284
|
+
* options: ['blue-green', 'canary', 'rolling'],
|
|
285
|
+
* context: { risk: 'high', users: 100000 },
|
|
286
|
+
* assignee: 'devops-lead@example.com',
|
|
287
|
+
* })
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
async decide<TOptions extends string = string>(params: {
|
|
291
|
+
title: string
|
|
292
|
+
description?: string
|
|
293
|
+
options: TOptions[]
|
|
294
|
+
context?: unknown
|
|
295
|
+
assignee?: string | string[]
|
|
296
|
+
role?: string
|
|
297
|
+
team?: string
|
|
298
|
+
priority?: Priority
|
|
299
|
+
timeout?: number
|
|
300
|
+
criteria?: string[]
|
|
301
|
+
metadata?: Record<string, unknown>
|
|
302
|
+
}): Promise<TOptions> {
|
|
303
|
+
const request = await this.store.create<DecisionRequest<TOptions>>({
|
|
304
|
+
type: 'decision',
|
|
305
|
+
status: 'pending',
|
|
306
|
+
title: params.title,
|
|
307
|
+
description: params.description || `Choose from: ${params.options.join(', ')}`,
|
|
308
|
+
input: { options: params.options, context: params.context },
|
|
309
|
+
options: params.options,
|
|
310
|
+
context: params.context,
|
|
311
|
+
criteria: params.criteria,
|
|
312
|
+
assignee: params.assignee,
|
|
313
|
+
role: params.role,
|
|
314
|
+
team: params.team,
|
|
315
|
+
priority: params.priority || this.options.defaultPriority,
|
|
316
|
+
timeout: params.timeout || this.options.defaultTimeout,
|
|
317
|
+
metadata: params.metadata,
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
return this.waitForResponse<DecisionRequest<TOptions>, TOptions>(request)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Request a human to review content
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```ts
|
|
328
|
+
* const review = await human.review({
|
|
329
|
+
* title: 'Review blog post',
|
|
330
|
+
* content: { title: 'My Post', body: '...' },
|
|
331
|
+
* reviewType: 'content',
|
|
332
|
+
* criteria: ['Grammar', 'Tone', 'Accuracy'],
|
|
333
|
+
* assignee: 'editor@example.com',
|
|
334
|
+
* })
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
async review<TContent = unknown>(params: {
|
|
338
|
+
title: string
|
|
339
|
+
description?: string
|
|
340
|
+
content: TContent
|
|
341
|
+
reviewType?: 'code' | 'content' | 'design' | 'data' | 'other'
|
|
342
|
+
criteria?: string[]
|
|
343
|
+
assignee?: string | string[]
|
|
344
|
+
role?: string
|
|
345
|
+
team?: string
|
|
346
|
+
priority?: Priority
|
|
347
|
+
timeout?: number
|
|
348
|
+
metadata?: Record<string, unknown>
|
|
349
|
+
}): Promise<ReviewResponse> {
|
|
350
|
+
const request = await this.store.create<ReviewRequest<TContent>>({
|
|
351
|
+
type: 'review',
|
|
352
|
+
status: 'pending',
|
|
353
|
+
title: params.title,
|
|
354
|
+
description: params.description || `Review requested: ${params.reviewType || 'other'}`,
|
|
355
|
+
input: params.content,
|
|
356
|
+
content: params.content,
|
|
357
|
+
reviewType: params.reviewType,
|
|
358
|
+
criteria: params.criteria,
|
|
359
|
+
assignee: params.assignee,
|
|
360
|
+
role: params.role,
|
|
361
|
+
team: params.team,
|
|
362
|
+
priority: params.priority || this.options.defaultPriority,
|
|
363
|
+
timeout: params.timeout || this.options.defaultTimeout,
|
|
364
|
+
metadata: params.metadata,
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
return this.waitForResponse<ReviewRequest<TContent>, ReviewResponse>(request)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Send a notification to a human
|
|
372
|
+
*
|
|
373
|
+
* @example
|
|
374
|
+
* ```ts
|
|
375
|
+
* await human.notify({
|
|
376
|
+
* type: 'info',
|
|
377
|
+
* title: 'Deployment complete',
|
|
378
|
+
* message: 'Version 2.0.0 deployed successfully',
|
|
379
|
+
* recipient: 'team@example.com',
|
|
380
|
+
* channels: ['slack', 'email'],
|
|
381
|
+
* })
|
|
382
|
+
* ```
|
|
383
|
+
*/
|
|
384
|
+
async notify(params: {
|
|
385
|
+
type: 'info' | 'warning' | 'error' | 'success'
|
|
386
|
+
title: string
|
|
387
|
+
message: string
|
|
388
|
+
recipient: string | string[]
|
|
389
|
+
channels?: ('slack' | 'email' | 'sms' | 'web')[]
|
|
390
|
+
priority?: Priority
|
|
391
|
+
data?: unknown
|
|
392
|
+
}): Promise<Notification> {
|
|
393
|
+
const notification: Notification = {
|
|
394
|
+
id: `notif_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
395
|
+
type: params.type,
|
|
396
|
+
title: params.title,
|
|
397
|
+
message: params.message,
|
|
398
|
+
recipient: params.recipient,
|
|
399
|
+
channels: params.channels,
|
|
400
|
+
priority: params.priority,
|
|
401
|
+
data: params.data,
|
|
402
|
+
createdAt: new Date(),
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// In a real implementation, this would:
|
|
406
|
+
// 1. Send the notification via specified channels
|
|
407
|
+
// 2. Track delivery status
|
|
408
|
+
// 3. Handle failures and retries
|
|
409
|
+
|
|
410
|
+
return notification
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Get a review queue
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* ```ts
|
|
418
|
+
* const queue = await human.getQueue({
|
|
419
|
+
* name: 'Pending Approvals',
|
|
420
|
+
* filters: {
|
|
421
|
+
* status: ['pending'],
|
|
422
|
+
* priority: ['high', 'critical'],
|
|
423
|
+
* },
|
|
424
|
+
* })
|
|
425
|
+
* ```
|
|
426
|
+
*/
|
|
427
|
+
async getQueue(params: {
|
|
428
|
+
name: string
|
|
429
|
+
description?: string
|
|
430
|
+
filters?: ReviewQueue['filters']
|
|
431
|
+
sortBy?: 'createdAt' | 'priority' | 'updatedAt'
|
|
432
|
+
sortDirection?: 'asc' | 'desc'
|
|
433
|
+
limit?: number
|
|
434
|
+
}): Promise<ReviewQueue> {
|
|
435
|
+
const items = await this.store.list(params.filters, params.limit)
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
id: `queue_${Date.now()}`,
|
|
439
|
+
name: params.name,
|
|
440
|
+
description: params.description,
|
|
441
|
+
items,
|
|
442
|
+
filters: params.filters,
|
|
443
|
+
sortBy: params.sortBy,
|
|
444
|
+
sortDirection: params.sortDirection,
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Get a request by ID
|
|
450
|
+
*/
|
|
451
|
+
async getRequest<T extends HumanRequest = HumanRequest>(id: string): Promise<T | null> {
|
|
452
|
+
return this.store.get<T>(id)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Complete a request with a response
|
|
457
|
+
*/
|
|
458
|
+
async completeRequest<T extends HumanRequest = HumanRequest>(
|
|
459
|
+
id: string,
|
|
460
|
+
response: T['response']
|
|
461
|
+
): Promise<T> {
|
|
462
|
+
return this.store.complete<T>(id, response)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Reject a request
|
|
467
|
+
*/
|
|
468
|
+
async rejectRequest(id: string, reason: string): Promise<HumanRequest> {
|
|
469
|
+
return this.store.reject(id, reason)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Escalate a request
|
|
474
|
+
*/
|
|
475
|
+
async escalateRequest(id: string, to: string): Promise<HumanRequest> {
|
|
476
|
+
return this.store.escalate(id, to)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Cancel a request
|
|
481
|
+
*/
|
|
482
|
+
async cancelRequest(id: string): Promise<HumanRequest> {
|
|
483
|
+
return this.store.cancel(id)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Define or update goals
|
|
488
|
+
*/
|
|
489
|
+
defineGoals(goals: Goals): Goals {
|
|
490
|
+
// In a real implementation, this would persist goals
|
|
491
|
+
return goals
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Track KPIs
|
|
496
|
+
*/
|
|
497
|
+
trackKPIs(kpis: KPIs): KPIs {
|
|
498
|
+
// In a real implementation, this would persist KPIs
|
|
499
|
+
return kpis
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Define or update OKRs
|
|
504
|
+
*/
|
|
505
|
+
defineOKRs(okrs: OKRs): OKRs {
|
|
506
|
+
// In a real implementation, this would persist OKRs
|
|
507
|
+
return okrs
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Create an approval workflow
|
|
512
|
+
*/
|
|
513
|
+
createWorkflow(workflow: ApprovalWorkflow): ApprovalWorkflow {
|
|
514
|
+
this.workflows.set(workflow.id, workflow)
|
|
515
|
+
return workflow
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Get a workflow by ID
|
|
520
|
+
*/
|
|
521
|
+
getWorkflow(id: string): ApprovalWorkflow | undefined {
|
|
522
|
+
return this.workflows.get(id)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Wait for a human response
|
|
527
|
+
*
|
|
528
|
+
* In a real implementation, this would:
|
|
529
|
+
* 1. Poll the store for updates
|
|
530
|
+
* 2. Listen for webhooks/events
|
|
531
|
+
* 3. Handle timeouts and escalations
|
|
532
|
+
* 4. Return the response when available
|
|
533
|
+
*
|
|
534
|
+
* For now, this throws an error to indicate manual completion is needed
|
|
535
|
+
*/
|
|
536
|
+
private async waitForResponse<TRequest extends HumanRequest, TResponse>(
|
|
537
|
+
request: TRequest
|
|
538
|
+
): Promise<TResponse> {
|
|
539
|
+
// Check if there's a timeout
|
|
540
|
+
if (request.timeout && request.timeout > 0) {
|
|
541
|
+
// Set up timeout handler
|
|
542
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
543
|
+
setTimeout(() => {
|
|
544
|
+
reject(new Error(`Request ${request.id} timed out after ${request.timeout}ms`))
|
|
545
|
+
}, request.timeout)
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
// Poll for completion
|
|
549
|
+
const pollPromise = this.pollForCompletion<TRequest, TResponse>(request.id)
|
|
550
|
+
|
|
551
|
+
// Race between timeout and completion
|
|
552
|
+
try {
|
|
553
|
+
return await Promise.race([pollPromise, timeoutPromise])
|
|
554
|
+
} catch (error) {
|
|
555
|
+
// On timeout, escalate if configured
|
|
556
|
+
if (this.options.autoEscalate && request.escalatesTo) {
|
|
557
|
+
const escalateTo = Array.isArray(request.escalatesTo)
|
|
558
|
+
? request.escalatesTo[0]
|
|
559
|
+
: request.escalatesTo
|
|
560
|
+
if (escalateTo) {
|
|
561
|
+
await this.store.escalate(request.id, escalateTo)
|
|
562
|
+
} else {
|
|
563
|
+
await this.store.update(request.id, { status: 'timeout' })
|
|
564
|
+
}
|
|
565
|
+
} else {
|
|
566
|
+
await this.store.update(request.id, { status: 'timeout' })
|
|
567
|
+
}
|
|
568
|
+
throw error
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// No timeout, just poll indefinitely
|
|
573
|
+
return this.pollForCompletion<TRequest, TResponse>(request.id)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Poll for request completion
|
|
578
|
+
*/
|
|
579
|
+
private async pollForCompletion<TRequest extends HumanRequest, TResponse>(
|
|
580
|
+
requestId: string
|
|
581
|
+
): Promise<TResponse> {
|
|
582
|
+
// In a real implementation, use webhooks, WebSockets, or event emitters
|
|
583
|
+
// This is a simplified polling implementation
|
|
584
|
+
const pollInterval = 1000 // 1 second
|
|
585
|
+
|
|
586
|
+
while (true) {
|
|
587
|
+
const request = await this.store.get<TRequest>(requestId)
|
|
588
|
+
|
|
589
|
+
if (!request) {
|
|
590
|
+
throw new Error(`Request ${requestId} not found`)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (request.status === 'completed' && request.response) {
|
|
594
|
+
return request.response as TResponse
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (request.status === 'rejected') {
|
|
598
|
+
throw new Error(`Request ${requestId} was rejected: ${request.rejectionReason}`)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (request.status === 'cancelled') {
|
|
602
|
+
throw new Error(`Request ${requestId} was cancelled`)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Wait before polling again
|
|
606
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval))
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Create a Human-in-the-loop manager instance
|
|
613
|
+
*
|
|
614
|
+
* @example
|
|
615
|
+
* ```ts
|
|
616
|
+
* import { Human } from 'human-in-the-loop'
|
|
617
|
+
*
|
|
618
|
+
* const human = Human({
|
|
619
|
+
* defaultTimeout: 3600000, // 1 hour
|
|
620
|
+
* autoEscalate: true,
|
|
621
|
+
* })
|
|
622
|
+
* ```
|
|
623
|
+
*/
|
|
624
|
+
export function Human(options?: HumanOptions): HumanManager {
|
|
625
|
+
return new HumanManager(options)
|
|
626
|
+
}
|