@vellumai/assistant 0.4.0 → 0.4.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/bun.lock +0 -83
- package/package.json +2 -3
- package/src/__tests__/channel-approval-routes.test.ts +55 -5
- package/src/__tests__/daemon-server-session-init.test.ts +54 -1
- package/src/__tests__/guardian-control-plane-policy.test.ts +6 -2
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +4 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +50 -9
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +161 -2
- package/src/__tests__/send-endpoint-busy.test.ts +413 -3
- package/src/approvals/guardian-decision-primitive.ts +22 -1
- package/src/daemon/handlers/sessions.ts +125 -11
- package/src/daemon/response-tier.ts +6 -5
- package/src/daemon/server.ts +17 -2
- package/src/daemon/session-agent-loop.ts +33 -22
- package/src/memory/app-store.ts +6 -0
- package/src/memory/embedding-local.ts +25 -13
- package/src/memory/embedding-runtime-manager.ts +24 -6
- package/src/runtime/guardian-context-resolver.ts +5 -1
- package/src/runtime/guardian-reply-router.ts +12 -0
- package/src/runtime/http-server.ts +1 -0
- package/src/runtime/routes/conversation-routes.ts +187 -2
- package/src/runtime/routes/inbound-message-handler.ts +12 -1
- package/src/tools/apps/executors.ts +15 -0
- package/src/tools/reminder/reminder-store.ts +10 -14
|
@@ -3,7 +3,7 @@ import * as net from 'node:net';
|
|
|
3
3
|
import { beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
4
4
|
|
|
5
5
|
import type { HandlerContext } from '../daemon/handlers.js';
|
|
6
|
-
import type { UserMessage } from '../daemon/ipc-contract.js';
|
|
6
|
+
import type { ConfirmationResponse, UserMessage } from '../daemon/ipc-contract.js';
|
|
7
7
|
import type { ServerMessage } from '../daemon/ipc-protocol.js';
|
|
8
8
|
import { DebouncerMap } from '../util/debounce.js';
|
|
9
9
|
|
|
@@ -12,8 +12,13 @@ const routeGuardianReplyMock = mock(async () => ({
|
|
|
12
12
|
decisionApplied: false,
|
|
13
13
|
type: 'not_consumed' as const,
|
|
14
14
|
})) as any;
|
|
15
|
+
const createCanonicalGuardianRequestMock = mock(() => ({
|
|
16
|
+
id: 'canonical-id',
|
|
17
|
+
}));
|
|
18
|
+
const generateCanonicalRequestCodeMock = mock(() => 'ABC123');
|
|
15
19
|
const listPendingByDestinationMock = mock(() => [] as Array<{ id: string; kind?: string }>);
|
|
16
20
|
const listCanonicalMock = mock(() => [] as Array<{ id: string }>);
|
|
21
|
+
const resolveCanonicalGuardianRequestMock = mock(() => null as { id: string } | null);
|
|
17
22
|
const getByConversationMock = mock(
|
|
18
23
|
() => [] as Array<{
|
|
19
24
|
requestId: string;
|
|
@@ -21,6 +26,7 @@ const getByConversationMock = mock(
|
|
|
21
26
|
session?: unknown;
|
|
22
27
|
}>,
|
|
23
28
|
);
|
|
29
|
+
const registerMock = mock(() => {});
|
|
24
30
|
const resolveMock = mock(() => undefined as unknown);
|
|
25
31
|
const addMessageMock = mock(async () => ({ id: 'persisted-message-id' }));
|
|
26
32
|
const getConfigMock = mock(() => ({
|
|
@@ -33,11 +39,15 @@ mock.module('../runtime/guardian-reply-router.js', () => ({
|
|
|
33
39
|
}));
|
|
34
40
|
|
|
35
41
|
mock.module('../memory/canonical-guardian-store.js', () => ({
|
|
42
|
+
createCanonicalGuardianRequest: createCanonicalGuardianRequestMock,
|
|
43
|
+
generateCanonicalRequestCode: generateCanonicalRequestCodeMock,
|
|
36
44
|
listPendingCanonicalGuardianRequestsByDestinationConversation: listPendingByDestinationMock,
|
|
37
45
|
listCanonicalGuardianRequests: listCanonicalMock,
|
|
46
|
+
resolveCanonicalGuardianRequest: resolveCanonicalGuardianRequestMock,
|
|
38
47
|
}));
|
|
39
48
|
|
|
40
49
|
mock.module('../runtime/pending-interactions.js', () => ({
|
|
50
|
+
register: registerMock,
|
|
41
51
|
getByConversation: getByConversationMock,
|
|
42
52
|
resolve: resolveMock,
|
|
43
53
|
}));
|
|
@@ -72,7 +82,7 @@ mock.module('../util/logger.js', () => ({
|
|
|
72
82
|
}),
|
|
73
83
|
}));
|
|
74
84
|
|
|
75
|
-
import { handleUserMessage } from '../daemon/handlers/sessions.js';
|
|
85
|
+
import { handleConfirmationResponse, handleUserMessage } from '../daemon/handlers/sessions.js';
|
|
76
86
|
|
|
77
87
|
interface TestSession {
|
|
78
88
|
messages: Array<{ role: string; content: unknown[] }>;
|
|
@@ -151,8 +161,12 @@ function makeSession(overrides: Partial<TestSession> = {}): TestSession {
|
|
|
151
161
|
describe('handleUserMessage pending-confirmation reply interception', () => {
|
|
152
162
|
beforeEach(() => {
|
|
153
163
|
routeGuardianReplyMock.mockClear();
|
|
164
|
+
createCanonicalGuardianRequestMock.mockClear();
|
|
165
|
+
generateCanonicalRequestCodeMock.mockClear();
|
|
154
166
|
listPendingByDestinationMock.mockClear();
|
|
155
167
|
listCanonicalMock.mockClear();
|
|
168
|
+
resolveCanonicalGuardianRequestMock.mockClear();
|
|
169
|
+
registerMock.mockClear();
|
|
156
170
|
getByConversationMock.mockClear();
|
|
157
171
|
resolveMock.mockClear();
|
|
158
172
|
addMessageMock.mockClear();
|
|
@@ -224,6 +238,28 @@ describe('handleUserMessage pending-confirmation reply interception', () => {
|
|
|
224
238
|
expect(requestComplete?.runStillActive).toBe(false);
|
|
225
239
|
});
|
|
226
240
|
|
|
241
|
+
test('consumes decision replies even when queue depth is non-zero', async () => {
|
|
242
|
+
listPendingByDestinationMock.mockReturnValue([{ id: 'req-1', kind: 'tool_approval' }]);
|
|
243
|
+
listCanonicalMock.mockReturnValue([{ id: 'req-1' }]);
|
|
244
|
+
routeGuardianReplyMock.mockResolvedValue({
|
|
245
|
+
consumed: true,
|
|
246
|
+
decisionApplied: true,
|
|
247
|
+
type: 'canonical_decision_applied',
|
|
248
|
+
requestId: 'req-1',
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const session = makeSession({
|
|
252
|
+
getQueueDepth: () => 2,
|
|
253
|
+
});
|
|
254
|
+
const { ctx } = createContext(session);
|
|
255
|
+
|
|
256
|
+
await handleUserMessage(makeMessage('approve'), {} as net.Socket, ctx);
|
|
257
|
+
|
|
258
|
+
expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
|
|
259
|
+
expect((session.denyAllPendingConfirmations as any).mock.calls.length).toBe(0);
|
|
260
|
+
expect((session.enqueueMessage as any).mock.calls.length).toBe(0);
|
|
261
|
+
});
|
|
262
|
+
|
|
227
263
|
test('does not mutate in-memory history while processing', async () => {
|
|
228
264
|
listPendingByDestinationMock.mockReturnValue([{ id: 'req-1', kind: 'tool_approval' }]);
|
|
229
265
|
listCanonicalMock.mockReturnValue([{ id: 'req-1' }]);
|
|
@@ -314,5 +350,128 @@ describe('handleUserMessage pending-confirmation reply interception', () => {
|
|
|
314
350
|
// session-scoped interaction should be resolved.
|
|
315
351
|
expect(resolveMock).toHaveBeenCalledTimes(1);
|
|
316
352
|
expect(resolveMock).toHaveBeenCalledWith('req-live');
|
|
353
|
+
expect(resolveCanonicalGuardianRequestMock).toHaveBeenCalledTimes(1);
|
|
354
|
+
expect(resolveCanonicalGuardianRequestMock).toHaveBeenCalledWith(
|
|
355
|
+
'req-live',
|
|
356
|
+
'pending',
|
|
357
|
+
{ status: 'denied' },
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('registers IPC confirmation events for NL approval routing', async () => {
|
|
362
|
+
const session = makeSession({
|
|
363
|
+
hasAnyPendingConfirmation: () => false,
|
|
364
|
+
enqueueMessage: mock(() => ({ queued: false, requestId: 'direct-id' })),
|
|
365
|
+
processMessage: async (_content, _attachments, onEvent) => {
|
|
366
|
+
(onEvent as (msg: ServerMessage) => void)({
|
|
367
|
+
type: 'confirmation_request',
|
|
368
|
+
requestId: 'req-confirm-1',
|
|
369
|
+
toolName: 'call_start',
|
|
370
|
+
input: { phone_number: '+18084436762' },
|
|
371
|
+
riskLevel: 'high',
|
|
372
|
+
executionTarget: 'host',
|
|
373
|
+
allowlistOptions: [],
|
|
374
|
+
scopeOptions: [],
|
|
375
|
+
persistentDecisionsAllowed: false,
|
|
376
|
+
} as ServerMessage);
|
|
377
|
+
return 'msg-id';
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
const { ctx, sent } = createContext(session);
|
|
381
|
+
|
|
382
|
+
await handleUserMessage(makeMessage('please call now'), {} as net.Socket, ctx);
|
|
383
|
+
|
|
384
|
+
expect(registerMock).toHaveBeenCalledTimes(1);
|
|
385
|
+
expect(registerMock).toHaveBeenCalledWith(
|
|
386
|
+
'req-confirm-1',
|
|
387
|
+
expect.objectContaining({
|
|
388
|
+
conversationId: 'conv-1',
|
|
389
|
+
kind: 'confirmation',
|
|
390
|
+
session,
|
|
391
|
+
confirmationDetails: expect.objectContaining({
|
|
392
|
+
toolName: 'call_start',
|
|
393
|
+
riskLevel: 'high',
|
|
394
|
+
executionTarget: 'host',
|
|
395
|
+
}),
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
398
|
+
expect(createCanonicalGuardianRequestMock).toHaveBeenCalledTimes(1);
|
|
399
|
+
expect(createCanonicalGuardianRequestMock).toHaveBeenCalledWith(
|
|
400
|
+
expect.objectContaining({
|
|
401
|
+
id: 'req-confirm-1',
|
|
402
|
+
kind: 'tool_approval',
|
|
403
|
+
sourceType: 'desktop',
|
|
404
|
+
sourceChannel: 'vellum',
|
|
405
|
+
conversationId: 'conv-1',
|
|
406
|
+
toolName: 'call_start',
|
|
407
|
+
status: 'pending',
|
|
408
|
+
requestCode: 'ABC123',
|
|
409
|
+
}),
|
|
410
|
+
);
|
|
411
|
+
expect(sent.some((event) => event.type === 'confirmation_request')).toBe(true);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('syncs canonical status to approved for IPC allow decisions', () => {
|
|
415
|
+
const session = {
|
|
416
|
+
hasPendingConfirmation: (requestId: string) => requestId === 'req-confirm-allow',
|
|
417
|
+
handleConfirmationResponse: mock(() => {}),
|
|
418
|
+
};
|
|
419
|
+
const { ctx } = createContext(makeSession());
|
|
420
|
+
ctx.sessions.set('conv-1', session as any);
|
|
421
|
+
|
|
422
|
+
const msg: ConfirmationResponse = {
|
|
423
|
+
type: 'confirmation_response',
|
|
424
|
+
requestId: 'req-confirm-allow',
|
|
425
|
+
decision: 'always_allow',
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
handleConfirmationResponse(msg, {} as net.Socket, ctx);
|
|
429
|
+
|
|
430
|
+
expect((session.handleConfirmationResponse as any).mock.calls.length).toBe(1);
|
|
431
|
+
expect((session.handleConfirmationResponse as any).mock.calls[0]).toEqual([
|
|
432
|
+
'req-confirm-allow',
|
|
433
|
+
'always_allow',
|
|
434
|
+
undefined,
|
|
435
|
+
undefined,
|
|
436
|
+
]);
|
|
437
|
+
expect(resolveCanonicalGuardianRequestMock).toHaveBeenCalledWith(
|
|
438
|
+
'req-confirm-allow',
|
|
439
|
+
'pending',
|
|
440
|
+
{ status: 'approved' },
|
|
441
|
+
);
|
|
442
|
+
expect(resolveMock).toHaveBeenCalledWith('req-confirm-allow');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test('syncs canonical status to denied for IPC deny decisions in CU sessions', () => {
|
|
446
|
+
const cuSession = {
|
|
447
|
+
hasPendingConfirmation: (requestId: string) => requestId === 'req-confirm-deny',
|
|
448
|
+
handleConfirmationResponse: mock(() => {}),
|
|
449
|
+
};
|
|
450
|
+
const { ctx } = createContext(makeSession({
|
|
451
|
+
hasPendingConfirmation: () => false,
|
|
452
|
+
}));
|
|
453
|
+
ctx.cuSessions.set('cu-1', cuSession as any);
|
|
454
|
+
|
|
455
|
+
const msg: ConfirmationResponse = {
|
|
456
|
+
type: 'confirmation_response',
|
|
457
|
+
requestId: 'req-confirm-deny',
|
|
458
|
+
decision: 'always_deny',
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
handleConfirmationResponse(msg, {} as net.Socket, ctx);
|
|
462
|
+
|
|
463
|
+
expect((cuSession.handleConfirmationResponse as any).mock.calls.length).toBe(1);
|
|
464
|
+
expect((cuSession.handleConfirmationResponse as any).mock.calls[0]).toEqual([
|
|
465
|
+
'req-confirm-deny',
|
|
466
|
+
'always_deny',
|
|
467
|
+
undefined,
|
|
468
|
+
undefined,
|
|
469
|
+
]);
|
|
470
|
+
expect(resolveCanonicalGuardianRequestMock).toHaveBeenCalledWith(
|
|
471
|
+
'req-confirm-deny',
|
|
472
|
+
'pending',
|
|
473
|
+
{ status: 'denied' },
|
|
474
|
+
);
|
|
475
|
+
expect(resolveMock).toHaveBeenCalledWith('req-confirm-deny');
|
|
317
476
|
});
|
|
318
477
|
});
|