human-in-the-loop 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/CHANGELOG.md +17 -0
- package/examples/basic-usage.js +325 -0
- package/package.json +3 -4
- package/src/helpers.js +274 -0
- package/src/human.js +475 -0
- package/src/human.test.js +333 -0
- package/src/index.js +50 -0
- package/src/store.js +161 -0
- package/src/types.js +4 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for human-in-the-loop primitives
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
5
|
+
import { Human } from './human.js';
|
|
6
|
+
import { InMemoryHumanStore } from './store.js';
|
|
7
|
+
describe('Human-in-the-loop', () => {
|
|
8
|
+
let human;
|
|
9
|
+
let store;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
store = new InMemoryHumanStore();
|
|
12
|
+
human = Human({ store });
|
|
13
|
+
});
|
|
14
|
+
describe('Role Management', () => {
|
|
15
|
+
it('should define and retrieve a role', () => {
|
|
16
|
+
const role = human.defineRole({
|
|
17
|
+
id: 'tech-lead',
|
|
18
|
+
name: 'Tech Lead',
|
|
19
|
+
description: 'Technical leadership',
|
|
20
|
+
capabilities: ['approve-prs', 'deploy-prod'],
|
|
21
|
+
});
|
|
22
|
+
expect(role.id).toBe('tech-lead');
|
|
23
|
+
expect(role.name).toBe('Tech Lead');
|
|
24
|
+
const retrieved = human.getRole('tech-lead');
|
|
25
|
+
expect(retrieved).toEqual(role);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('Team Management', () => {
|
|
29
|
+
it('should define and retrieve a team', () => {
|
|
30
|
+
const team = human.defineTeam({
|
|
31
|
+
id: 'engineering',
|
|
32
|
+
name: 'Engineering Team',
|
|
33
|
+
members: ['alice', 'bob'],
|
|
34
|
+
lead: 'alice',
|
|
35
|
+
});
|
|
36
|
+
expect(team.id).toBe('engineering');
|
|
37
|
+
expect(team.members).toHaveLength(2);
|
|
38
|
+
const retrieved = human.getTeam('engineering');
|
|
39
|
+
expect(retrieved).toEqual(team);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('Human Worker Management', () => {
|
|
43
|
+
it('should register and retrieve a human', () => {
|
|
44
|
+
const worker = human.registerHuman({
|
|
45
|
+
id: 'alice',
|
|
46
|
+
name: 'Alice Smith',
|
|
47
|
+
email: 'alice@example.com',
|
|
48
|
+
roles: ['tech-lead'],
|
|
49
|
+
teams: ['engineering'],
|
|
50
|
+
});
|
|
51
|
+
expect(worker.id).toBe('alice');
|
|
52
|
+
expect(worker.name).toBe('Alice Smith');
|
|
53
|
+
const retrieved = human.getHuman('alice');
|
|
54
|
+
expect(retrieved).toEqual(worker);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('Approval Requests', () => {
|
|
58
|
+
it('should create an approval request', async () => {
|
|
59
|
+
const requestPromise = human.approve({
|
|
60
|
+
title: 'Test Approval',
|
|
61
|
+
description: 'Test approval request',
|
|
62
|
+
subject: 'Test',
|
|
63
|
+
input: { data: 'test' },
|
|
64
|
+
assignee: 'alice@example.com',
|
|
65
|
+
priority: 'normal',
|
|
66
|
+
});
|
|
67
|
+
// The request should be pending in the store
|
|
68
|
+
const requests = await store.list({ status: ['pending'] });
|
|
69
|
+
expect(requests).toHaveLength(1);
|
|
70
|
+
expect(requests[0]?.type).toBe('approval');
|
|
71
|
+
expect(requests[0]?.title).toBe('Test Approval');
|
|
72
|
+
});
|
|
73
|
+
it('should complete an approval request', async () => {
|
|
74
|
+
// Create request
|
|
75
|
+
const request = await store.create({
|
|
76
|
+
type: 'approval',
|
|
77
|
+
status: 'pending',
|
|
78
|
+
title: 'Test Approval',
|
|
79
|
+
description: 'Test',
|
|
80
|
+
subject: 'Test',
|
|
81
|
+
input: { data: 'test' },
|
|
82
|
+
priority: 'normal',
|
|
83
|
+
});
|
|
84
|
+
// Complete it
|
|
85
|
+
const response = {
|
|
86
|
+
approved: true,
|
|
87
|
+
comments: 'Looks good!',
|
|
88
|
+
};
|
|
89
|
+
const completed = await human.completeRequest(request.id, response);
|
|
90
|
+
expect(completed.status).toBe('completed');
|
|
91
|
+
expect(completed.response).toEqual(response);
|
|
92
|
+
});
|
|
93
|
+
it('should reject an approval request', async () => {
|
|
94
|
+
const request = await store.create({
|
|
95
|
+
type: 'approval',
|
|
96
|
+
status: 'pending',
|
|
97
|
+
title: 'Test Approval',
|
|
98
|
+
description: 'Test',
|
|
99
|
+
subject: 'Test',
|
|
100
|
+
input: { data: 'test' },
|
|
101
|
+
priority: 'normal',
|
|
102
|
+
});
|
|
103
|
+
const rejected = await human.rejectRequest(request.id, 'Not ready yet');
|
|
104
|
+
expect(rejected.status).toBe('rejected');
|
|
105
|
+
expect(rejected.rejectionReason).toBe('Not ready yet');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('Question Requests', () => {
|
|
109
|
+
it('should create a question request', async () => {
|
|
110
|
+
const requestPromise = human.ask({
|
|
111
|
+
title: 'Test Question',
|
|
112
|
+
question: 'What is the answer?',
|
|
113
|
+
context: { topic: 'testing' },
|
|
114
|
+
assignee: 'alice@example.com',
|
|
115
|
+
});
|
|
116
|
+
const requests = await store.list({ status: ['pending'] });
|
|
117
|
+
expect(requests).toHaveLength(1);
|
|
118
|
+
expect(requests[0]?.type).toBe('question');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe('Decision Requests', () => {
|
|
122
|
+
it('should create a decision request', async () => {
|
|
123
|
+
const requestPromise = human.decide({
|
|
124
|
+
title: 'Test Decision',
|
|
125
|
+
options: ['option1', 'option2', 'option3'],
|
|
126
|
+
context: { info: 'test' },
|
|
127
|
+
assignee: 'alice@example.com',
|
|
128
|
+
});
|
|
129
|
+
const requests = await store.list({ status: ['pending'] });
|
|
130
|
+
expect(requests).toHaveLength(1);
|
|
131
|
+
expect(requests[0]?.type).toBe('decision');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe('Review Requests', () => {
|
|
135
|
+
it('should create a review request', async () => {
|
|
136
|
+
const requestPromise = human.review({
|
|
137
|
+
title: 'Test Review',
|
|
138
|
+
content: { code: 'console.log("test")' },
|
|
139
|
+
reviewType: 'code',
|
|
140
|
+
criteria: ['syntax', 'style', 'logic'],
|
|
141
|
+
assignee: 'alice@example.com',
|
|
142
|
+
});
|
|
143
|
+
const requests = await store.list({ status: ['pending'] });
|
|
144
|
+
expect(requests).toHaveLength(1);
|
|
145
|
+
expect(requests[0]?.type).toBe('review');
|
|
146
|
+
});
|
|
147
|
+
it('should complete a review request', async () => {
|
|
148
|
+
const request = await store.create({
|
|
149
|
+
type: 'review',
|
|
150
|
+
status: 'pending',
|
|
151
|
+
title: 'Code Review',
|
|
152
|
+
description: 'Review PR',
|
|
153
|
+
input: { code: 'test' },
|
|
154
|
+
content: { code: 'test' },
|
|
155
|
+
priority: 'normal',
|
|
156
|
+
});
|
|
157
|
+
const response = {
|
|
158
|
+
approved: true,
|
|
159
|
+
comments: 'Code looks good',
|
|
160
|
+
rating: 5,
|
|
161
|
+
};
|
|
162
|
+
const completed = await human.completeRequest(request.id, response);
|
|
163
|
+
expect(completed.status).toBe('completed');
|
|
164
|
+
expect(completed.response).toEqual(response);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe('Notifications', () => {
|
|
168
|
+
it('should send a notification', async () => {
|
|
169
|
+
const notification = await human.notify({
|
|
170
|
+
type: 'info',
|
|
171
|
+
title: 'Test Notification',
|
|
172
|
+
message: 'This is a test',
|
|
173
|
+
recipient: 'alice@example.com',
|
|
174
|
+
channels: ['email'],
|
|
175
|
+
priority: 'normal',
|
|
176
|
+
});
|
|
177
|
+
expect(notification.id).toBeDefined();
|
|
178
|
+
expect(notification.type).toBe('info');
|
|
179
|
+
expect(notification.title).toBe('Test Notification');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe('Review Queue', () => {
|
|
183
|
+
it('should create and filter a review queue', async () => {
|
|
184
|
+
// Create multiple requests
|
|
185
|
+
await store.create({
|
|
186
|
+
type: 'approval',
|
|
187
|
+
status: 'pending',
|
|
188
|
+
title: 'High Priority',
|
|
189
|
+
description: 'Test',
|
|
190
|
+
subject: 'Test',
|
|
191
|
+
input: {},
|
|
192
|
+
priority: 'high',
|
|
193
|
+
});
|
|
194
|
+
await store.create({
|
|
195
|
+
type: 'approval',
|
|
196
|
+
status: 'pending',
|
|
197
|
+
title: 'Normal Priority',
|
|
198
|
+
description: 'Test',
|
|
199
|
+
subject: 'Test',
|
|
200
|
+
input: {},
|
|
201
|
+
priority: 'normal',
|
|
202
|
+
});
|
|
203
|
+
await store.create({
|
|
204
|
+
type: 'approval',
|
|
205
|
+
status: 'completed',
|
|
206
|
+
title: 'Completed',
|
|
207
|
+
description: 'Test',
|
|
208
|
+
subject: 'Test',
|
|
209
|
+
input: {},
|
|
210
|
+
priority: 'normal',
|
|
211
|
+
});
|
|
212
|
+
// Get queue with filters
|
|
213
|
+
const queue = await human.getQueue({
|
|
214
|
+
name: 'High Priority Queue',
|
|
215
|
+
filters: {
|
|
216
|
+
status: ['pending'],
|
|
217
|
+
priority: ['high'],
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
expect(queue.items).toHaveLength(1);
|
|
221
|
+
expect(queue.items[0]?.priority).toBe('high');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe('Goals and OKRs', () => {
|
|
225
|
+
it('should define goals', () => {
|
|
226
|
+
const goals = human.defineGoals({
|
|
227
|
+
id: 'q1-2024',
|
|
228
|
+
objectives: ['Launch v2.0', 'Improve performance'],
|
|
229
|
+
targetDate: new Date('2024-03-31'),
|
|
230
|
+
});
|
|
231
|
+
expect(goals.id).toBe('q1-2024');
|
|
232
|
+
expect(goals.objectives).toHaveLength(2);
|
|
233
|
+
});
|
|
234
|
+
it('should track KPIs', () => {
|
|
235
|
+
const kpi = human.trackKPIs({
|
|
236
|
+
id: 'response-time',
|
|
237
|
+
name: 'API Response Time',
|
|
238
|
+
value: 120,
|
|
239
|
+
target: 100,
|
|
240
|
+
unit: 'ms',
|
|
241
|
+
trend: 'down',
|
|
242
|
+
});
|
|
243
|
+
expect(kpi.id).toBe('response-time');
|
|
244
|
+
expect(kpi.value).toBe(120);
|
|
245
|
+
});
|
|
246
|
+
it('should define OKRs', () => {
|
|
247
|
+
const okr = human.defineOKRs({
|
|
248
|
+
id: 'q1-okr',
|
|
249
|
+
objective: 'Improve performance',
|
|
250
|
+
keyResults: [
|
|
251
|
+
{
|
|
252
|
+
description: 'Reduce response time to <100ms',
|
|
253
|
+
progress: 0.75,
|
|
254
|
+
current: 120,
|
|
255
|
+
target: 100,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
period: 'Q1 2024',
|
|
259
|
+
});
|
|
260
|
+
expect(okr.id).toBe('q1-okr');
|
|
261
|
+
expect(okr.keyResults).toHaveLength(1);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
describe('Workflow Management', () => {
|
|
265
|
+
it('should create and retrieve a workflow', () => {
|
|
266
|
+
const workflow = human.createWorkflow({
|
|
267
|
+
id: 'approval-workflow',
|
|
268
|
+
name: 'Approval Workflow',
|
|
269
|
+
steps: [
|
|
270
|
+
{
|
|
271
|
+
name: 'Step 1',
|
|
272
|
+
approvers: ['alice'],
|
|
273
|
+
requireAll: true,
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: 'Step 2',
|
|
277
|
+
approvers: ['bob'],
|
|
278
|
+
requireAll: true,
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
currentStep: 0,
|
|
282
|
+
status: 'pending',
|
|
283
|
+
});
|
|
284
|
+
expect(workflow.id).toBe('approval-workflow');
|
|
285
|
+
expect(workflow.steps).toHaveLength(2);
|
|
286
|
+
const retrieved = human.getWorkflow('approval-workflow');
|
|
287
|
+
expect(retrieved).toEqual(workflow);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
describe('Request Operations', () => {
|
|
291
|
+
it('should get a request by ID', async () => {
|
|
292
|
+
const request = await store.create({
|
|
293
|
+
type: 'approval',
|
|
294
|
+
status: 'pending',
|
|
295
|
+
title: 'Test',
|
|
296
|
+
description: 'Test',
|
|
297
|
+
subject: 'Test',
|
|
298
|
+
input: {},
|
|
299
|
+
priority: 'normal',
|
|
300
|
+
});
|
|
301
|
+
const retrieved = await human.getRequest(request.id);
|
|
302
|
+
expect(retrieved?.id).toBe(request.id);
|
|
303
|
+
});
|
|
304
|
+
it('should escalate a request', async () => {
|
|
305
|
+
const request = await store.create({
|
|
306
|
+
type: 'approval',
|
|
307
|
+
status: 'pending',
|
|
308
|
+
title: 'Test',
|
|
309
|
+
description: 'Test',
|
|
310
|
+
subject: 'Test',
|
|
311
|
+
input: {},
|
|
312
|
+
priority: 'normal',
|
|
313
|
+
assignee: 'alice',
|
|
314
|
+
});
|
|
315
|
+
const escalated = await human.escalateRequest(request.id, 'bob');
|
|
316
|
+
expect(escalated.status).toBe('escalated');
|
|
317
|
+
expect(escalated.assignee).toBe('bob');
|
|
318
|
+
});
|
|
319
|
+
it('should cancel a request', async () => {
|
|
320
|
+
const request = await store.create({
|
|
321
|
+
type: 'approval',
|
|
322
|
+
status: 'pending',
|
|
323
|
+
title: 'Test',
|
|
324
|
+
description: 'Test',
|
|
325
|
+
subject: 'Test',
|
|
326
|
+
input: {},
|
|
327
|
+
priority: 'normal',
|
|
328
|
+
});
|
|
329
|
+
const cancelled = await human.cancelRequest(request.id);
|
|
330
|
+
expect(cancelled.status).toBe('cancelled');
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
});
|
package/src/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* human-in-the-loop - Primitives for integrating human oversight and intervention in AI workflows
|
|
3
|
+
*
|
|
4
|
+
* This package provides primitives for human oversight and intervention in AI workflows:
|
|
5
|
+
* - Approval gates and workflows
|
|
6
|
+
* - Review processes and queues
|
|
7
|
+
* - Escalation paths
|
|
8
|
+
* - Human intervention points
|
|
9
|
+
* - Role and team management
|
|
10
|
+
* - Goals, KPIs, and OKRs tracking
|
|
11
|
+
*
|
|
12
|
+
* Implements the digital-workers interface for humans operating within a company boundary.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { Human, approve, ask, notify } from 'human-in-the-loop'
|
|
18
|
+
*
|
|
19
|
+
* // Create a Human-in-the-loop manager
|
|
20
|
+
* const human = Human({
|
|
21
|
+
* defaultTimeout: 3600000, // 1 hour
|
|
22
|
+
* autoEscalate: true,
|
|
23
|
+
* })
|
|
24
|
+
*
|
|
25
|
+
* // Request approval
|
|
26
|
+
* const result = await approve({
|
|
27
|
+
* title: 'Deploy to production',
|
|
28
|
+
* description: 'Approve deployment of v2.0.0',
|
|
29
|
+
* subject: 'Production Deployment',
|
|
30
|
+
* assignee: 'tech-lead@example.com',
|
|
31
|
+
* priority: 'high',
|
|
32
|
+
* })
|
|
33
|
+
*
|
|
34
|
+
* if (result.approved) {
|
|
35
|
+
* await deploy()
|
|
36
|
+
* await notify({
|
|
37
|
+
* type: 'success',
|
|
38
|
+
* title: 'Deployment complete',
|
|
39
|
+
* message: 'v2.0.0 deployed to production',
|
|
40
|
+
* recipient: 'team@example.com',
|
|
41
|
+
* })
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
// Export main Human constructor and manager
|
|
46
|
+
export { Human, HumanManager } from './human.js';
|
|
47
|
+
// Export helper functions (convenience API)
|
|
48
|
+
export { Role, Team, Goals, approve, ask, do, decide, generate, is, notify, kpis, okrs, registerHuman, getDefaultHuman, } from './helpers.js';
|
|
49
|
+
// Export store implementations
|
|
50
|
+
export { InMemoryHumanStore } from './store.js';
|
package/src/store.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory store implementation for human requests
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Simple in-memory implementation of HumanStore
|
|
6
|
+
*
|
|
7
|
+
* For production use, implement a persistent store using:
|
|
8
|
+
* - Database (PostgreSQL, MongoDB, etc.)
|
|
9
|
+
* - Key-value store (Redis)
|
|
10
|
+
* - Message queue (RabbitMQ, AWS SQS)
|
|
11
|
+
*/
|
|
12
|
+
export class InMemoryHumanStore {
|
|
13
|
+
requests = new Map();
|
|
14
|
+
requestIdCounter = 0;
|
|
15
|
+
/**
|
|
16
|
+
* Generate a unique request ID
|
|
17
|
+
*/
|
|
18
|
+
generateId() {
|
|
19
|
+
return `req_${Date.now()}_${++this.requestIdCounter}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create a new request
|
|
23
|
+
*/
|
|
24
|
+
async create(request) {
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const fullRequest = {
|
|
27
|
+
...request,
|
|
28
|
+
id: this.generateId(),
|
|
29
|
+
createdAt: now,
|
|
30
|
+
updatedAt: now,
|
|
31
|
+
};
|
|
32
|
+
this.requests.set(fullRequest.id, fullRequest);
|
|
33
|
+
return fullRequest;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get a request by ID
|
|
37
|
+
*/
|
|
38
|
+
async get(id) {
|
|
39
|
+
const request = this.requests.get(id);
|
|
40
|
+
return request ? request : null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Update a request
|
|
44
|
+
*/
|
|
45
|
+
async update(id, updates) {
|
|
46
|
+
const request = this.requests.get(id);
|
|
47
|
+
if (!request) {
|
|
48
|
+
throw new Error(`Request not found: ${id}`);
|
|
49
|
+
}
|
|
50
|
+
const updated = {
|
|
51
|
+
...request,
|
|
52
|
+
...updates,
|
|
53
|
+
updatedAt: new Date(),
|
|
54
|
+
};
|
|
55
|
+
this.requests.set(id, updated);
|
|
56
|
+
return updated;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* List requests with filters
|
|
60
|
+
*/
|
|
61
|
+
async list(filters, limit) {
|
|
62
|
+
let requests = Array.from(this.requests.values());
|
|
63
|
+
// Apply filters
|
|
64
|
+
if (filters) {
|
|
65
|
+
if (filters.status) {
|
|
66
|
+
requests = requests.filter((r) => filters.status.includes(r.status));
|
|
67
|
+
}
|
|
68
|
+
if (filters.priority) {
|
|
69
|
+
requests = requests.filter((r) => filters.priority.includes(r.priority));
|
|
70
|
+
}
|
|
71
|
+
if (filters.assignee) {
|
|
72
|
+
requests = requests.filter((r) => {
|
|
73
|
+
if (!r.assignee)
|
|
74
|
+
return false;
|
|
75
|
+
const assignees = Array.isArray(r.assignee) ? r.assignee : [r.assignee];
|
|
76
|
+
return assignees.some((a) => filters.assignee.includes(a));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (filters.role) {
|
|
80
|
+
requests = requests.filter((r) => r.role && filters.role.includes(r.role));
|
|
81
|
+
}
|
|
82
|
+
if (filters.team) {
|
|
83
|
+
requests = requests.filter((r) => r.team && filters.team.includes(r.team));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Sort by creation date (newest first)
|
|
87
|
+
requests.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
88
|
+
// Apply limit
|
|
89
|
+
if (limit && limit > 0) {
|
|
90
|
+
requests = requests.slice(0, limit);
|
|
91
|
+
}
|
|
92
|
+
return requests;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Complete a request
|
|
96
|
+
*/
|
|
97
|
+
async complete(id, response) {
|
|
98
|
+
const request = await this.get(id);
|
|
99
|
+
if (!request) {
|
|
100
|
+
throw new Error(`Request not found: ${id}`);
|
|
101
|
+
}
|
|
102
|
+
return this.update(id, {
|
|
103
|
+
status: 'completed',
|
|
104
|
+
response,
|
|
105
|
+
completedAt: new Date(),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Reject a request
|
|
110
|
+
*/
|
|
111
|
+
async reject(id, reason) {
|
|
112
|
+
const request = await this.get(id);
|
|
113
|
+
if (!request) {
|
|
114
|
+
throw new Error(`Request not found: ${id}`);
|
|
115
|
+
}
|
|
116
|
+
return this.update(id, {
|
|
117
|
+
status: 'rejected',
|
|
118
|
+
rejectionReason: reason,
|
|
119
|
+
completedAt: new Date(),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Escalate a request
|
|
124
|
+
*/
|
|
125
|
+
async escalate(id, to) {
|
|
126
|
+
const request = await this.get(id);
|
|
127
|
+
if (!request) {
|
|
128
|
+
throw new Error(`Request not found: ${id}`);
|
|
129
|
+
}
|
|
130
|
+
return this.update(id, {
|
|
131
|
+
status: 'escalated',
|
|
132
|
+
assignee: to,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Cancel a request
|
|
137
|
+
*/
|
|
138
|
+
async cancel(id) {
|
|
139
|
+
const request = await this.get(id);
|
|
140
|
+
if (!request) {
|
|
141
|
+
throw new Error(`Request not found: ${id}`);
|
|
142
|
+
}
|
|
143
|
+
return this.update(id, {
|
|
144
|
+
status: 'cancelled',
|
|
145
|
+
completedAt: new Date(),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Clear all requests (for testing)
|
|
150
|
+
*/
|
|
151
|
+
clear() {
|
|
152
|
+
this.requests.clear();
|
|
153
|
+
this.requestIdCounter = 0;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get total count of requests
|
|
157
|
+
*/
|
|
158
|
+
count() {
|
|
159
|
+
return this.requests.size;
|
|
160
|
+
}
|
|
161
|
+
}
|
package/src/types.js
ADDED