@vellumai/assistant 0.3.19 → 0.3.21
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/ARCHITECTURE.md +151 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/bun.lock +139 -2
- package/docs/architecture/integrations.md +7 -11
- package/package.json +2 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
- package/src/__tests__/approval-primitive.test.ts +540 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
- package/src/__tests__/call-controller.test.ts +439 -108
- package/src/__tests__/channel-invite-transport.test.ts +264 -0
- package/src/__tests__/cli.test.ts +42 -1
- package/src/__tests__/config-schema.test.ts +11 -127
- package/src/__tests__/config-watcher.test.ts +0 -8
- package/src/__tests__/daemon-lifecycle.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +8 -2
- package/src/__tests__/diff.test.ts +22 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
- package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
- package/src/__tests__/guardian-dispatch.test.ts +124 -0
- package/src/__tests__/guardian-grant-minting.test.ts +6 -17
- package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
- package/src/__tests__/invite-redemption-service.test.ts +306 -0
- package/src/__tests__/ipc-snapshot.test.ts +57 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
- package/src/__tests__/sandbox-host-parity.test.ts +6 -13
- package/src/__tests__/scoped-approval-grants.test.ts +6 -6
- package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
- package/src/__tests__/session-load-history-repair.test.ts +169 -2
- package/src/__tests__/session-runtime-assembly.test.ts +33 -5
- package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
- package/src/__tests__/skill-feature-flags.test.ts +188 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
- package/src/__tests__/skill-mirror-parity.test.ts +1 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/terminal-sandbox.test.ts +142 -9
- package/src/__tests__/terminal-tools.test.ts +2 -93
- package/src/__tests__/thread-seed-composer.test.ts +18 -0
- package/src/__tests__/tool-approval-handler.test.ts +350 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
- package/src/agent/loop.ts +36 -1
- package/src/approvals/approval-primitive.ts +381 -0
- package/src/approvals/guardian-decision-primitive.ts +191 -0
- package/src/calls/call-controller.ts +252 -209
- package/src/calls/call-domain.ts +44 -6
- package/src/calls/guardian-dispatch.ts +48 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +46 -30
- package/src/cli/core-commands.ts +0 -4
- package/src/cli/mcp.ts +58 -0
- package/src/cli.ts +76 -34
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
- package/src/config/assistant-feature-flags.ts +162 -0
- package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
- package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/reminder/SKILL.md +49 -2
- package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env-registry.ts +10 -0
- package/src/config/feature-flag-registry.json +61 -0
- package/src/config/loader.ts +22 -1
- package/src/config/mcp-schema.ts +46 -0
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +18 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +0 -1
- package/src/config/skills.ts +9 -0
- package/src/config/system-prompt.ts +110 -46
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +19 -1
- package/src/config/vellum-skills/catalog.json +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -5
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -3
- package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/guardian-invite-intent.ts +124 -0
- package/src/daemon/handlers/avatar.ts +68 -0
- package/src/daemon/handlers/browser.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +120 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/sessions.ts +19 -0
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/install-cli-launchers.ts +58 -13
- package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -2
- package/src/daemon/ipc-contract/settings.ts +25 -2
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/providers-setup.ts +26 -1
- package/src/daemon/server.ts +1 -0
- package/src/daemon/session-lifecycle.ts +52 -7
- package/src/daemon/session-memory.ts +45 -0
- package/src/daemon/session-process.ts +258 -432
- package/src/daemon/session-runtime-assembly.ts +12 -0
- package/src/daemon/session-skill-tools.ts +14 -1
- package/src/daemon/session-tool-setup.ts +5 -0
- package/src/daemon/session.ts +11 -0
- package/src/daemon/shutdown-handlers.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +2 -2
- package/src/mcp/client.ts +152 -0
- package/src/mcp/manager.ts +139 -0
- package/src/memory/conversation-display-order-migration.ts +44 -0
- package/src/memory/conversation-queries.ts +2 -0
- package/src/memory/conversation-store.ts +91 -0
- package/src/memory/db-init.ts +5 -1
- package/src/memory/embedding-local.ts +13 -8
- package/src/memory/guardian-action-store.ts +125 -2
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +2 -1
- package/src/memory/schema.ts +5 -1
- package/src/memory/scoped-approval-grants.ts +14 -5
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/notifications/decision-engine.ts +49 -12
- package/src/notifications/emit-signal.ts +7 -0
- package/src/notifications/signal.ts +7 -0
- package/src/notifications/thread-seed-composer.ts +2 -1
- package/src/runtime/channel-approval-types.ts +16 -6
- package/src/runtime/channel-approvals.ts +19 -15
- package/src/runtime/channel-invite-transport.ts +85 -0
- package/src/runtime/channel-invite-transports/telegram.ts +105 -0
- package/src/runtime/guardian-action-grant-minter.ts +92 -35
- package/src/runtime/guardian-action-message-composer.ts +30 -0
- package/src/runtime/guardian-decision-types.ts +91 -0
- package/src/runtime/http-server.ts +23 -1
- package/src/runtime/ingress-service.ts +22 -0
- package/src/runtime/invite-redemption-service.ts +181 -0
- package/src/runtime/invite-redemption-templates.ts +39 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/guardian-action-routes.ts +206 -0
- package/src/runtime/routes/guardian-approval-interception.ts +66 -190
- package/src/runtime/routes/identity-routes.ts +73 -0
- package/src/runtime/routes/inbound-message-handler.ts +486 -394
- package/src/runtime/routes/pairing-routes.ts +4 -0
- package/src/security/encrypted-store.ts +31 -17
- package/src/security/keychain.ts +176 -2
- package/src/security/secure-keys.ts +97 -0
- package/src/security/tool-approval-digest.ts +1 -1
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/browser-manager.ts +46 -32
- package/src/tools/browser/browser-screencast.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -1
- package/src/tools/executor.ts +22 -17
- package/src/tools/mcp/mcp-tool-factory.ts +100 -0
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/registry.ts +64 -1
- package/src/tools/skills/load.ts +22 -8
- package/src/tools/system/avatar-generator.ts +119 -0
- package/src/tools/system/navigate-settings.ts +65 -0
- package/src/tools/system/open-system-settings.ts +75 -0
- package/src/tools/system/voice-config.ts +121 -32
- package/src/tools/terminal/backends/native.ts +40 -19
- package/src/tools/terminal/backends/types.ts +3 -3
- package/src/tools/terminal/parser.ts +1 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
- package/src/tools/terminal/sandbox.ts +1 -12
- package/src/tools/terminal/shell.ts +3 -31
- package/src/tools/tool-approval-handler.ts +141 -3
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +10 -2
- package/src/util/diff.ts +36 -13
- package/Dockerfile.sandbox +0 -5
- package/src/__tests__/doordash-client.test.ts +0 -187
- package/src/__tests__/doordash-session.test.ts +0 -154
- package/src/__tests__/signup-e2e.test.ts +0 -354
- package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
- package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
- package/src/cli/doordash.ts +0 -1057
- package/src/config/bundled-skills/doordash/SKILL.md +0 -163
- package/src/config/templates/LOOKS.md +0 -25
- package/src/doordash/cart-queries.ts +0 -787
- package/src/doordash/client.ts +0 -1016
- package/src/doordash/order-queries.ts +0 -85
- package/src/doordash/queries.ts +0 -13
- package/src/doordash/query-extractor.ts +0 -94
- package/src/doordash/search-queries.ts +0 -203
- package/src/doordash/session.ts +0 -84
- package/src/doordash/store-queries.ts +0 -246
- package/src/doordash/types.ts +0 -367
- package/src/tools/terminal/backends/docker.ts +0 -379
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { createAssistantMessage,createUserMessage } from '../agent/message-types.js';
|
|
10
10
|
import { answerCall } from '../calls/call-domain.js';
|
|
11
|
+
import { isTerminalState } from '../calls/call-state-machine.js';
|
|
12
|
+
import { getCallSession } from '../calls/call-store.js';
|
|
11
13
|
import type { TurnChannelContext, TurnInterfaceContext } from '../channels/types.js';
|
|
12
14
|
import { parseChannelId, parseInterfaceId } from '../channels/types.js';
|
|
13
15
|
import { getConfig } from '../config/loader.js';
|
|
@@ -15,10 +17,12 @@ import * as conversationStore from '../memory/conversation-store.js';
|
|
|
15
17
|
import { provenanceFromGuardianContext } from '../memory/conversation-store.js';
|
|
16
18
|
import {
|
|
17
19
|
finalizeFollowup,
|
|
20
|
+
getDeliveriesByRequestId,
|
|
18
21
|
getExpiredDeliveriesByConversation,
|
|
19
22
|
getFollowupDeliveriesByConversation,
|
|
20
23
|
getGuardianActionRequest,
|
|
21
24
|
getPendingDeliveriesByConversation,
|
|
25
|
+
getPendingRequestByCallSessionId,
|
|
22
26
|
progressFollowupState,
|
|
23
27
|
resolveGuardianActionRequest,
|
|
24
28
|
startFollowupFromExpiredRequest,
|
|
@@ -30,8 +34,9 @@ import { processGuardianFollowUpTurn } from '../runtime/guardian-action-conversa
|
|
|
30
34
|
import { executeFollowupAction } from '../runtime/guardian-action-followup-executor.js';
|
|
31
35
|
import { tryMintGuardianActionGrant } from '../runtime/guardian-action-grant-minter.js';
|
|
32
36
|
import { composeGuardianActionMessageGenerative } from '../runtime/guardian-action-message-composer.js';
|
|
33
|
-
import type { GuardianActionCopyGenerator, GuardianFollowUpConversationGenerator } from '../runtime/http-types.js';
|
|
37
|
+
import type { ApprovalConversationGenerator, GuardianActionCopyGenerator, GuardianFollowUpConversationGenerator } from '../runtime/http-types.js';
|
|
34
38
|
import { getLogger } from '../util/logger.js';
|
|
39
|
+
import { resolveGuardianInviteIntent } from './guardian-invite-intent.js';
|
|
35
40
|
import { resolveGuardianVerificationIntent } from './guardian-verification-intent.js';
|
|
36
41
|
import type { UsageStats } from './ipc-contract.js';
|
|
37
42
|
import type { ServerMessage, UserMessageAttachment } from './ipc-protocol.js';
|
|
@@ -51,6 +56,7 @@ const log = getLogger('session-process');
|
|
|
51
56
|
// generator through Session / DaemonServer constructors.
|
|
52
57
|
let _guardianFollowUpGenerator: GuardianFollowUpConversationGenerator | undefined;
|
|
53
58
|
let _guardianActionCopyGenerator: GuardianActionCopyGenerator | undefined;
|
|
59
|
+
let _approvalConversationGenerator: ApprovalConversationGenerator | undefined;
|
|
54
60
|
|
|
55
61
|
/** Inject the guardian follow-up conversation generator (called from lifecycle.ts). */
|
|
56
62
|
export function setGuardianFollowUpConversationGenerator(gen: GuardianFollowUpConversationGenerator): void {
|
|
@@ -62,6 +68,11 @@ export function setGuardianActionCopyGenerator(gen: GuardianActionCopyGenerator)
|
|
|
62
68
|
_guardianActionCopyGenerator = gen;
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
/** Inject the approval conversation generator (called from lifecycle.ts). */
|
|
72
|
+
export function setApprovalConversationGenerator(gen: ApprovalConversationGenerator): void {
|
|
73
|
+
_approvalConversationGenerator = gen;
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
/** Build a model_info event with fresh config data. */
|
|
66
77
|
function buildModelInfoEvent(): ServerMessage {
|
|
67
78
|
const config = getConfig();
|
|
@@ -105,6 +116,7 @@ export interface ProcessSessionContext {
|
|
|
105
116
|
/** Assistant identity — used for scoping notification preferences. */
|
|
106
117
|
readonly assistantId?: string;
|
|
107
118
|
guardianContext?: GuardianRuntimeContext;
|
|
119
|
+
ensureActorScopedHistory(): Promise<void>;
|
|
108
120
|
persistUserMessage(content: string, attachments: UserMessageAttachment[], requestId?: string, metadata?: Record<string, unknown>, displayContent?: string): Promise<string>;
|
|
109
121
|
runAgentLoop(
|
|
110
122
|
content: string,
|
|
@@ -294,6 +306,15 @@ export async function drainQueue(session: ProcessSessionContext, reason: QueueDr
|
|
|
294
306
|
log.info({ conversationId: session.conversationId, channelHint: guardianIntent.channelHint }, 'Guardian verification intent intercepted in queue — forcing skill flow');
|
|
295
307
|
agentLoopContent = guardianIntent.rewrittenContent;
|
|
296
308
|
session.preactivatedSkillIds = ['guardian-verify-setup'];
|
|
309
|
+
} else {
|
|
310
|
+
// Guardian invite intent interception — force invite management
|
|
311
|
+
// requests into the trusted-contacts skill flow.
|
|
312
|
+
const inviteIntent = resolveGuardianInviteIntent(resolvedContent);
|
|
313
|
+
if (inviteIntent.kind === 'invite_management') {
|
|
314
|
+
log.info({ conversationId: session.conversationId, action: inviteIntent.action }, 'Guardian invite intent intercepted in queue — forcing skill flow');
|
|
315
|
+
agentLoopContent = inviteIntent.rewrittenContent;
|
|
316
|
+
session.preactivatedSkillIds = ['trusted-contacts'];
|
|
317
|
+
}
|
|
297
318
|
}
|
|
298
319
|
}
|
|
299
320
|
|
|
@@ -380,481 +401,277 @@ export async function processMessage(
|
|
|
380
401
|
options?: { isInteractive?: boolean },
|
|
381
402
|
displayContent?: string,
|
|
382
403
|
): Promise<string> {
|
|
404
|
+
await session.ensureActorScopedHistory();
|
|
383
405
|
session.currentActiveSurfaceId = activeSurfaceId;
|
|
384
406
|
session.currentPage = currentPage;
|
|
385
407
|
|
|
386
|
-
// ──
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Multiple deliveries across any state: require request code prefix for disambiguation
|
|
416
|
-
if (!matchedDelivery && pendingDeliveries.length >= 1) {
|
|
417
|
-
for (const d of pendingDeliveries) {
|
|
418
|
-
const req = getGuardianActionRequest(d.requestId);
|
|
419
|
-
if (req && content.toUpperCase().startsWith(req.requestCode)) {
|
|
420
|
-
matchedDelivery = d;
|
|
421
|
-
answerText = content.slice(req.requestCode.length).trim();
|
|
422
|
-
break;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// If no pending delivery matched, check whether the code targets an
|
|
427
|
-
// expired or follow-up delivery. If so, skip the pending section entirely
|
|
428
|
-
// and let the message fall through to the expired/follow-up handlers below.
|
|
429
|
-
if (!matchedDelivery) {
|
|
430
|
-
let matchesOtherState = false;
|
|
431
|
-
for (const d of [...crossStateExpired, ...crossStateFollowup]) {
|
|
408
|
+
// ── Unified guardian action answer interception (mac channel) ──
|
|
409
|
+
// Deterministic priority matching: pending → follow-up → expired.
|
|
410
|
+
// When the guardian includes an explicit request code, match it across all
|
|
411
|
+
// states in priority order. When only one actionable request exists,
|
|
412
|
+
// auto-match without requiring a code prefix.
|
|
413
|
+
{
|
|
414
|
+
const allPending = getPendingDeliveriesByConversation(session.conversationId);
|
|
415
|
+
const allFollowup = getFollowupDeliveriesByConversation(session.conversationId);
|
|
416
|
+
const allExpired = getExpiredDeliveriesByConversation(session.conversationId);
|
|
417
|
+
const totalActionable = allPending.length + allFollowup.length + allExpired.length;
|
|
418
|
+
|
|
419
|
+
if (totalActionable > 0) {
|
|
420
|
+
const guardianIfCtx = session.getTurnInterfaceContext();
|
|
421
|
+
const guardianChannelMeta = { userMessageChannel: 'vellum' as const, assistantMessageChannel: 'vellum' as const, userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum', assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum', provenanceActorRole: 'guardian' as const };
|
|
422
|
+
|
|
423
|
+
// Try to parse an explicit request code from the message, in priority order
|
|
424
|
+
type CodeMatch = { delivery: typeof allPending[0]; request: NonNullable<ReturnType<typeof getGuardianActionRequest>>; state: 'pending' | 'followup' | 'expired'; answerText: string };
|
|
425
|
+
let codeMatch: CodeMatch | null = null;
|
|
426
|
+
const upperContent = content.toUpperCase();
|
|
427
|
+
const orderedSets: Array<{ deliveries: typeof allPending; state: 'pending' | 'followup' | 'expired' }> = [
|
|
428
|
+
{ deliveries: allPending, state: 'pending' },
|
|
429
|
+
{ deliveries: allFollowup, state: 'followup' },
|
|
430
|
+
{ deliveries: allExpired, state: 'expired' },
|
|
431
|
+
];
|
|
432
|
+
for (const { deliveries, state } of orderedSets) {
|
|
433
|
+
for (const d of deliveries) {
|
|
432
434
|
const req = getGuardianActionRequest(d.requestId);
|
|
433
|
-
if (req &&
|
|
434
|
-
|
|
435
|
+
if (req && upperContent.startsWith(req.requestCode)) {
|
|
436
|
+
codeMatch = { delivery: d, request: req, state, answerText: content.slice(req.requestCode.length).trim() };
|
|
435
437
|
break;
|
|
436
438
|
}
|
|
437
439
|
}
|
|
438
|
-
|
|
439
|
-
if (!matchesOtherState) {
|
|
440
|
-
const guardianIfCtx = session.getTurnInterfaceContext();
|
|
441
|
-
const guardianChannelMeta = { userMessageChannel: 'vellum' as const, assistantMessageChannel: 'vellum' as const, userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum', assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum', provenanceActorRole: 'guardian' as const };
|
|
442
|
-
const userMsg = createUserMessage(content, attachments);
|
|
443
|
-
const persisted = await conversationStore.addMessage(
|
|
444
|
-
session.conversationId,
|
|
445
|
-
'user',
|
|
446
|
-
JSON.stringify(userMsg.content),
|
|
447
|
-
guardianChannelMeta,
|
|
448
|
-
);
|
|
449
|
-
session.messages.push(userMsg);
|
|
450
|
-
|
|
451
|
-
// Include codes from all states so the guardian sees all options
|
|
452
|
-
const allDeliveries = [...pendingDeliveries, ...crossStateExpired, ...crossStateFollowup];
|
|
453
|
-
const codes = allDeliveries
|
|
454
|
-
.map((d) => { const req = getGuardianActionRequest(d.requestId); return req ? req.requestCode : null; })
|
|
455
|
-
.filter((code): code is string => typeof code === 'string' && code.length > 0);
|
|
456
|
-
const disambiguationText = `You have multiple pending guardian questions. Please prefix your reply with the reference code (${codes.join(', ')}) to indicate which question you are answering.`;
|
|
457
|
-
const disambiguationMsg = createAssistantMessage(disambiguationText);
|
|
458
|
-
await conversationStore.addMessage(
|
|
459
|
-
session.conversationId,
|
|
460
|
-
'assistant',
|
|
461
|
-
JSON.stringify(disambiguationMsg.content),
|
|
462
|
-
guardianChannelMeta,
|
|
463
|
-
);
|
|
464
|
-
session.messages.push(disambiguationMsg);
|
|
465
|
-
onEvent({ type: 'assistant_text_delta', text: disambiguationText });
|
|
466
|
-
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
467
|
-
return persisted.id;
|
|
468
|
-
}
|
|
469
|
-
// Code matched an expired/follow-up delivery — fall through to those handlers
|
|
440
|
+
if (codeMatch) break;
|
|
470
441
|
}
|
|
471
|
-
}
|
|
472
442
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
// another channel.
|
|
491
|
-
const answerResult = await answerCall({ callSessionId: guardianRequest.callSessionId, answer: answerText });
|
|
492
|
-
|
|
493
|
-
if ('ok' in answerResult && answerResult.ok) {
|
|
494
|
-
const resolved = resolveGuardianActionRequest(guardianRequest.id, answerText, 'vellum');
|
|
495
|
-
|
|
496
|
-
// Mint a scoped grant so the voice call can consume it
|
|
497
|
-
// for subsequent tool confirmations.
|
|
498
|
-
if (resolved) {
|
|
499
|
-
tryMintGuardianActionGrant({
|
|
500
|
-
resolvedRequest: resolved,
|
|
501
|
-
answerText,
|
|
502
|
-
decisionChannel: 'vellum',
|
|
503
|
-
});
|
|
443
|
+
// Explicit code targets a non-pending state: handle terminal superseded
|
|
444
|
+
if (codeMatch && codeMatch.state !== 'pending') {
|
|
445
|
+
const targetReq = codeMatch.request;
|
|
446
|
+
if (targetReq.status === 'expired' && targetReq.expiredReason === 'superseded') {
|
|
447
|
+
const callSession = getCallSession(targetReq.callSessionId);
|
|
448
|
+
const callStillActive = callSession && !isTerminalState(callSession.status);
|
|
449
|
+
if (!callStillActive) {
|
|
450
|
+
const userMsg = createUserMessage(content, attachments);
|
|
451
|
+
const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
|
|
452
|
+
session.messages.push(userMsg);
|
|
453
|
+
const staleText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_superseded' }, {}, _guardianActionCopyGenerator);
|
|
454
|
+
const staleMsg = createAssistantMessage(staleText);
|
|
455
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(staleMsg.content), guardianChannelMeta);
|
|
456
|
+
session.messages.push(staleMsg);
|
|
457
|
+
onEvent({ type: 'assistant_text_delta', text: staleText });
|
|
458
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
459
|
+
return persisted.id;
|
|
504
460
|
}
|
|
505
|
-
|
|
506
|
-
const replyText = resolved
|
|
507
|
-
? 'Your answer has been relayed to the call.'
|
|
508
|
-
: await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_answered' }, {}, _guardianActionCopyGenerator);
|
|
509
|
-
const replyMsg = createAssistantMessage(replyText);
|
|
510
|
-
await conversationStore.addMessage(
|
|
511
|
-
session.conversationId,
|
|
512
|
-
'assistant',
|
|
513
|
-
JSON.stringify(replyMsg.content),
|
|
514
|
-
guardianChannelMeta,
|
|
515
|
-
);
|
|
516
|
-
session.messages.push(replyMsg);
|
|
517
|
-
onEvent({ type: 'assistant_text_delta', text: replyText });
|
|
518
|
-
} else {
|
|
519
|
-
const errorDetail = 'error' in answerResult ? answerResult.error : 'Unknown error';
|
|
520
|
-
log.warn({ callSessionId: guardianRequest.callSessionId, error: errorDetail }, 'answerCall failed for mac guardian answer');
|
|
521
|
-
const failureText = await composeGuardianActionMessageGenerative(
|
|
522
|
-
{ scenario: 'guardian_answer_delivery_failed' },
|
|
523
|
-
{},
|
|
524
|
-
_guardianActionCopyGenerator,
|
|
525
|
-
);
|
|
526
|
-
const failMsg = createAssistantMessage(failureText);
|
|
527
|
-
await conversationStore.addMessage(
|
|
528
|
-
session.conversationId,
|
|
529
|
-
'assistant',
|
|
530
|
-
JSON.stringify(failMsg.content),
|
|
531
|
-
guardianChannelMeta,
|
|
532
|
-
);
|
|
533
|
-
session.messages.push(failMsg);
|
|
534
|
-
onEvent({ type: 'assistant_text_delta', text: failureText });
|
|
535
461
|
}
|
|
536
|
-
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
537
|
-
return persisted.id;
|
|
538
462
|
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
463
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const expCrossStateFollowup = getFollowupDeliveriesByConversation(session.conversationId);
|
|
554
|
-
const expTotalCrossStateCount = expiredDeliveries.length + expCrossStatePending.length + expCrossStateFollowup.length;
|
|
555
|
-
let matchedExpired = (expiredDeliveries.length === 1 && expTotalCrossStateCount === 1) ? expiredDeliveries[0] : null;
|
|
556
|
-
let expiredAnswerText = content;
|
|
557
|
-
|
|
558
|
-
// Strip the request code prefix from the answer text when the single
|
|
559
|
-
// expired delivery is auto-matched (content may include a code prefix
|
|
560
|
-
// if the pending section fell through via matchesOtherState).
|
|
561
|
-
if (matchedExpired) {
|
|
562
|
-
const req = getGuardianActionRequest(matchedExpired.requestId);
|
|
563
|
-
if (req && content.toUpperCase().startsWith(req.requestCode)) {
|
|
564
|
-
expiredAnswerText = content.slice(req.requestCode.length).trim();
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Multiple expired deliveries (or cross-state disambiguation needed):
|
|
569
|
-
// require request code prefix for disambiguation
|
|
570
|
-
if (!matchedExpired && expiredDeliveries.length >= 1) {
|
|
571
|
-
for (const d of expiredDeliveries) {
|
|
572
|
-
const req = getGuardianActionRequest(d.requestId);
|
|
573
|
-
if (req && content.toUpperCase().startsWith(req.requestCode)) {
|
|
574
|
-
matchedExpired = d;
|
|
575
|
-
expiredAnswerText = content.slice(req.requestCode.length).trim();
|
|
576
|
-
break;
|
|
464
|
+
// Auto-match: single actionable request across all states
|
|
465
|
+
if (!codeMatch && totalActionable === 1) {
|
|
466
|
+
const singleDelivery = allPending[0] ?? allFollowup[0] ?? allExpired[0];
|
|
467
|
+
const singleReq = getGuardianActionRequest(singleDelivery.requestId);
|
|
468
|
+
if (singleReq) {
|
|
469
|
+
const state: 'pending' | 'followup' | 'expired' = allPending.length === 1 ? 'pending' : allFollowup.length === 1 ? 'followup' : 'expired';
|
|
470
|
+
let text = content;
|
|
471
|
+
if (upperContent.startsWith(singleReq.requestCode)) {
|
|
472
|
+
text = content.slice(singleReq.requestCode.length).trim();
|
|
473
|
+
}
|
|
474
|
+
codeMatch = { delivery: singleDelivery, request: singleReq, state, answerText: text };
|
|
577
475
|
}
|
|
578
476
|
}
|
|
579
477
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
478
|
+
// Unknown code: message starts with a 6-char alphanumeric token that doesn't match
|
|
479
|
+
if (!codeMatch && totalActionable > 0) {
|
|
480
|
+
const possibleCodeMatch = content.match(/^([A-F0-9]{6})\s/i);
|
|
481
|
+
if (possibleCodeMatch) {
|
|
482
|
+
const candidateCode = possibleCodeMatch[1].toUpperCase();
|
|
483
|
+
const allDeliveries = [...allPending, ...allFollowup, ...allExpired];
|
|
484
|
+
const knownCodes = allDeliveries
|
|
485
|
+
.map((d) => { const req = getGuardianActionRequest(d.requestId); return req?.requestCode; })
|
|
486
|
+
.filter((code): code is string => typeof code === 'string');
|
|
487
|
+
if (!knownCodes.includes(candidateCode)) {
|
|
488
|
+
const userMsg = createUserMessage(content, attachments);
|
|
489
|
+
const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
|
|
490
|
+
session.messages.push(userMsg);
|
|
491
|
+
const unknownText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_unknown_code', unknownCode: candidateCode }, {}, _guardianActionCopyGenerator);
|
|
492
|
+
const unknownMsg = createAssistantMessage(unknownText);
|
|
493
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(unknownMsg.content), guardianChannelMeta);
|
|
494
|
+
session.messages.push(unknownMsg);
|
|
495
|
+
onEvent({ type: 'assistant_text_delta', text: unknownText });
|
|
496
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
497
|
+
return persisted.id;
|
|
589
498
|
}
|
|
590
499
|
}
|
|
591
|
-
|
|
592
|
-
if (!matchesFollowupState) {
|
|
593
|
-
const guardianIfCtx = session.getTurnInterfaceContext();
|
|
594
|
-
const guardianChannelMeta = { userMessageChannel: 'vellum' as const, assistantMessageChannel: 'vellum' as const, userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum', assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum', provenanceActorRole: 'guardian' as const };
|
|
595
|
-
const userMsg = createUserMessage(content, attachments);
|
|
596
|
-
const persisted = await conversationStore.addMessage(
|
|
597
|
-
session.conversationId,
|
|
598
|
-
'user',
|
|
599
|
-
JSON.stringify(userMsg.content),
|
|
600
|
-
guardianChannelMeta,
|
|
601
|
-
);
|
|
602
|
-
session.messages.push(userMsg);
|
|
603
|
-
|
|
604
|
-
// Include codes from all states so the guardian sees all options
|
|
605
|
-
const allExpiredDeliveries = [...expiredDeliveries, ...expCrossStatePending, ...expCrossStateFollowup];
|
|
606
|
-
const codes = allExpiredDeliveries
|
|
607
|
-
.map((d) => { const req = getGuardianActionRequest(d.requestId); return req ? req.requestCode : null; })
|
|
608
|
-
.filter((code): code is string => typeof code === 'string' && code.length > 0);
|
|
609
|
-
const disambiguationText = await composeGuardianActionMessageGenerative(
|
|
610
|
-
{ scenario: 'guardian_expired_disambiguation', requestCodes: codes },
|
|
611
|
-
{ requiredKeywords: codes },
|
|
612
|
-
_guardianActionCopyGenerator,
|
|
613
|
-
);
|
|
614
|
-
const disambiguationMsg = createAssistantMessage(disambiguationText);
|
|
615
|
-
await conversationStore.addMessage(
|
|
616
|
-
session.conversationId,
|
|
617
|
-
'assistant',
|
|
618
|
-
JSON.stringify(disambiguationMsg.content),
|
|
619
|
-
guardianChannelMeta,
|
|
620
|
-
);
|
|
621
|
-
session.messages.push(disambiguationMsg);
|
|
622
|
-
onEvent({ type: 'assistant_text_delta', text: disambiguationText });
|
|
623
|
-
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
624
|
-
return persisted.id;
|
|
625
|
-
}
|
|
626
|
-
// Code matched a follow-up or pending delivery — fall through to those handlers
|
|
627
500
|
}
|
|
628
|
-
}
|
|
629
501
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (expiredRequest && expiredRequest.status === 'expired' && expiredRequest.followupState === 'none') {
|
|
633
|
-
const guardianIfCtx = session.getTurnInterfaceContext();
|
|
634
|
-
const guardianChannelMeta = { userMessageChannel: 'vellum' as const, assistantMessageChannel: 'vellum' as const, userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum', assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum', provenanceActorRole: 'guardian' as const };
|
|
502
|
+
// No match and multiple actionable requests → disambiguation
|
|
503
|
+
if (!codeMatch && totalActionable > 1) {
|
|
635
504
|
const userMsg = createUserMessage(content, attachments);
|
|
636
|
-
const persisted = await conversationStore.addMessage(
|
|
637
|
-
session.conversationId,
|
|
638
|
-
'user',
|
|
639
|
-
JSON.stringify(userMsg.content),
|
|
640
|
-
guardianChannelMeta,
|
|
641
|
-
);
|
|
505
|
+
const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
|
|
642
506
|
session.messages.push(userMsg);
|
|
643
|
-
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
);
|
|
662
|
-
session.messages.push(replyMsg);
|
|
663
|
-
onEvent({ type: 'assistant_text_delta', text: followupText });
|
|
664
|
-
} else {
|
|
665
|
-
// Follow-up already started or conflict — send stale message
|
|
666
|
-
const staleText = await composeGuardianActionMessageGenerative(
|
|
667
|
-
{ scenario: 'guardian_stale_expired' },
|
|
668
|
-
{},
|
|
669
|
-
_guardianActionCopyGenerator,
|
|
670
|
-
);
|
|
671
|
-
const staleMsg = createAssistantMessage(staleText);
|
|
672
|
-
await conversationStore.addMessage(
|
|
673
|
-
session.conversationId,
|
|
674
|
-
'assistant',
|
|
675
|
-
JSON.stringify(staleMsg.content),
|
|
676
|
-
guardianChannelMeta,
|
|
677
|
-
);
|
|
678
|
-
session.messages.push(staleMsg);
|
|
679
|
-
onEvent({ type: 'assistant_text_delta', text: staleText });
|
|
680
|
-
}
|
|
507
|
+
const allDeliveries = [...allPending, ...allFollowup, ...allExpired];
|
|
508
|
+
const codes = allDeliveries
|
|
509
|
+
.map((d) => { const req = getGuardianActionRequest(d.requestId); return req ? req.requestCode : null; })
|
|
510
|
+
.filter((code): code is string => typeof code === 'string' && code.length > 0);
|
|
511
|
+
const disambiguationScenario = allPending.length > 0
|
|
512
|
+
? 'guardian_pending_disambiguation' as const
|
|
513
|
+
: allFollowup.length > 0
|
|
514
|
+
? 'guardian_followup_disambiguation' as const
|
|
515
|
+
: 'guardian_expired_disambiguation' as const;
|
|
516
|
+
const disambiguationText = await composeGuardianActionMessageGenerative(
|
|
517
|
+
{ scenario: disambiguationScenario, requestCodes: codes },
|
|
518
|
+
{ requiredKeywords: codes },
|
|
519
|
+
_guardianActionCopyGenerator,
|
|
520
|
+
);
|
|
521
|
+
const disambiguationMsg = createAssistantMessage(disambiguationText);
|
|
522
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(disambiguationMsg.content), guardianChannelMeta);
|
|
523
|
+
session.messages.push(disambiguationMsg);
|
|
524
|
+
onEvent({ type: 'assistant_text_delta', text: disambiguationText });
|
|
681
525
|
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
682
526
|
return persisted.id;
|
|
683
527
|
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
528
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
// is the reply — route it through the conversation engine. When multiple
|
|
691
|
-
// follow-up deliveries exist, require request-code prefix for disambiguation.
|
|
692
|
-
const followupDeliveries = getFollowupDeliveriesByConversation(session.conversationId);
|
|
693
|
-
if (followupDeliveries.length > 0) {
|
|
694
|
-
// Cross-state disambiguation: check total deliveries across all states
|
|
695
|
-
// (pending + expired + follow-up). When the total exceeds 1, require
|
|
696
|
-
// request-code disambiguation even for a lone follow-up delivery.
|
|
697
|
-
const fuCrossStatePending = getPendingDeliveriesByConversation(session.conversationId);
|
|
698
|
-
const fuCrossStateExpired = getExpiredDeliveriesByConversation(session.conversationId);
|
|
699
|
-
const fuTotalCrossStateCount = followupDeliveries.length + fuCrossStatePending.length + fuCrossStateExpired.length;
|
|
700
|
-
let matchedFollowup = (followupDeliveries.length === 1 && fuTotalCrossStateCount === 1) ? followupDeliveries[0] : null;
|
|
701
|
-
let followupReplyText = content;
|
|
702
|
-
|
|
703
|
-
// Strip the request code prefix from the reply text when the single
|
|
704
|
-
// follow-up delivery is auto-matched (content may include a code prefix
|
|
705
|
-
// if the pending section fell through via matchesOtherState).
|
|
706
|
-
if (matchedFollowup) {
|
|
707
|
-
const req = getGuardianActionRequest(matchedFollowup.requestId);
|
|
708
|
-
if (req && content.toUpperCase().startsWith(req.requestCode)) {
|
|
709
|
-
followupReplyText = content.slice(req.requestCode.length).trim();
|
|
710
|
-
}
|
|
711
|
-
}
|
|
529
|
+
// Dispatch matched delivery by state
|
|
530
|
+
if (codeMatch) {
|
|
531
|
+
const { request, state, answerText } = codeMatch;
|
|
712
532
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if (req && content.toUpperCase().startsWith(req.requestCode)) {
|
|
719
|
-
matchedFollowup = d;
|
|
720
|
-
followupReplyText = content.slice(req.requestCode.length).trim();
|
|
721
|
-
break;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
533
|
+
// PENDING state handler
|
|
534
|
+
if (state === 'pending' && request.status === 'pending') {
|
|
535
|
+
const userMsg = createUserMessage(content, attachments);
|
|
536
|
+
const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
|
|
537
|
+
session.messages.push(userMsg);
|
|
724
538
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
539
|
+
const answerResult = await answerCall({ callSessionId: request.callSessionId, answer: answerText, pendingQuestionId: request.pendingQuestionId });
|
|
540
|
+
|
|
541
|
+
if ('ok' in answerResult && answerResult.ok) {
|
|
542
|
+
const resolved = resolveGuardianActionRequest(request.id, answerText, 'vellum');
|
|
543
|
+
if (resolved) {
|
|
544
|
+
await tryMintGuardianActionGrant({ request, answerText, decisionChannel: 'vellum', approvalConversationGenerator: _approvalConversationGenerator });
|
|
545
|
+
}
|
|
546
|
+
const replyText = resolved
|
|
547
|
+
? 'Your answer has been relayed to the call.'
|
|
548
|
+
: await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_answered' }, {}, _guardianActionCopyGenerator);
|
|
549
|
+
const replyMsg = createAssistantMessage(replyText);
|
|
550
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(replyMsg.content), guardianChannelMeta);
|
|
551
|
+
session.messages.push(replyMsg);
|
|
552
|
+
onEvent({ type: 'assistant_text_delta', text: replyText });
|
|
553
|
+
} else {
|
|
554
|
+
const errorDetail = 'error' in answerResult ? answerResult.error : 'Unknown error';
|
|
555
|
+
log.warn({ callSessionId: request.callSessionId, error: errorDetail }, 'answerCall failed for mac guardian answer');
|
|
556
|
+
const failureText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_answer_delivery_failed' }, {}, _guardianActionCopyGenerator);
|
|
557
|
+
const failMsg = createAssistantMessage(failureText);
|
|
558
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(failMsg.content), guardianChannelMeta);
|
|
559
|
+
session.messages.push(failMsg);
|
|
560
|
+
onEvent({ type: 'assistant_text_delta', text: failureText });
|
|
735
561
|
}
|
|
562
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
563
|
+
return persisted.id;
|
|
736
564
|
}
|
|
737
565
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
const guardianChannelMeta = { userMessageChannel: 'vellum' as const, assistantMessageChannel: 'vellum' as const, userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum', assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum', provenanceActorRole: 'guardian' as const };
|
|
566
|
+
// FOLLOW-UP state handler
|
|
567
|
+
if (state === 'followup' && request.followupState === 'awaiting_guardian_choice') {
|
|
741
568
|
const userMsg = createUserMessage(content, attachments);
|
|
742
|
-
const persisted = await conversationStore.addMessage(
|
|
743
|
-
session.conversationId,
|
|
744
|
-
'user',
|
|
745
|
-
JSON.stringify(userMsg.content),
|
|
746
|
-
guardianChannelMeta,
|
|
747
|
-
);
|
|
569
|
+
const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
|
|
748
570
|
session.messages.push(userMsg);
|
|
749
571
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
.map((d) => { const req = getGuardianActionRequest(d.requestId); return req ? req.requestCode : null; })
|
|
754
|
-
.filter((code): code is string => typeof code === 'string' && code.length > 0);
|
|
755
|
-
const disambiguationText = await composeGuardianActionMessageGenerative(
|
|
756
|
-
{ scenario: 'guardian_followup_disambiguation', requestCodes: codes },
|
|
757
|
-
{ requiredKeywords: codes },
|
|
758
|
-
_guardianActionCopyGenerator,
|
|
759
|
-
);
|
|
760
|
-
const disambiguationMsg = createAssistantMessage(disambiguationText);
|
|
761
|
-
await conversationStore.addMessage(
|
|
762
|
-
session.conversationId,
|
|
763
|
-
'assistant',
|
|
764
|
-
JSON.stringify(disambiguationMsg.content),
|
|
765
|
-
guardianChannelMeta,
|
|
572
|
+
const turnResult = await processGuardianFollowUpTurn(
|
|
573
|
+
{ questionText: request.questionText, lateAnswerText: request.lateAnswerText ?? '', guardianReply: answerText },
|
|
574
|
+
_guardianFollowUpGenerator,
|
|
766
575
|
);
|
|
767
|
-
session.messages.push(disambiguationMsg);
|
|
768
|
-
onEvent({ type: 'assistant_text_delta', text: disambiguationText });
|
|
769
|
-
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
770
|
-
return persisted.id;
|
|
771
|
-
}
|
|
772
|
-
// Code matched an expired or pending delivery — fall through to agent loop
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
576
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const persisted = await conversationStore.addMessage(
|
|
783
|
-
session.conversationId,
|
|
784
|
-
'user',
|
|
785
|
-
JSON.stringify(userMsg.content),
|
|
786
|
-
guardianChannelMeta,
|
|
787
|
-
);
|
|
788
|
-
session.messages.push(userMsg);
|
|
577
|
+
let stateApplied = true;
|
|
578
|
+
if (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back') {
|
|
579
|
+
stateApplied = progressFollowupState(request.id, 'dispatching', turnResult.disposition) !== undefined;
|
|
580
|
+
} else if (turnResult.disposition === 'decline') {
|
|
581
|
+
stateApplied = finalizeFollowup(request.id, 'declined') !== undefined;
|
|
582
|
+
}
|
|
789
583
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
lateAnswerText: followupRequest.lateAnswerText ?? '',
|
|
794
|
-
guardianReply: followupReplyText,
|
|
795
|
-
},
|
|
796
|
-
_guardianFollowUpGenerator,
|
|
797
|
-
);
|
|
584
|
+
if (!stateApplied) {
|
|
585
|
+
log.warn({ requestId: request.id, disposition: turnResult.disposition }, 'Follow-up state transition failed (already resolved)');
|
|
586
|
+
}
|
|
798
587
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
} else if (turnResult.disposition === 'decline') {
|
|
808
|
-
stateApplied = finalizeFollowup(followupRequest.id, 'declined') !== undefined;
|
|
809
|
-
}
|
|
810
|
-
// keep_pending: no state change — guardian can reply again
|
|
588
|
+
const replyText = stateApplied
|
|
589
|
+
? turnResult.replyText
|
|
590
|
+
: await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_followup' }, {}, _guardianActionCopyGenerator);
|
|
591
|
+
const replyMsg = createAssistantMessage(replyText);
|
|
592
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(replyMsg.content), guardianChannelMeta);
|
|
593
|
+
session.messages.push(replyMsg);
|
|
594
|
+
onEvent({ type: 'assistant_text_delta', text: replyText });
|
|
595
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
811
596
|
|
|
812
|
-
|
|
813
|
-
|
|
597
|
+
if (stateApplied && (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back')) {
|
|
598
|
+
void (async () => {
|
|
599
|
+
try {
|
|
600
|
+
const execResult = await executeFollowupAction(request.id, turnResult.disposition as 'call_back' | 'message_back', _guardianActionCopyGenerator);
|
|
601
|
+
const completionMsg = createAssistantMessage(execResult.guardianReplyText);
|
|
602
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(completionMsg.content), guardianChannelMeta);
|
|
603
|
+
session.messages.push(completionMsg);
|
|
604
|
+
onEvent({ type: 'assistant_text_delta', text: execResult.guardianReplyText });
|
|
605
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
606
|
+
} catch (execErr) {
|
|
607
|
+
log.error({ err: execErr, requestId: request.id }, 'Follow-up action execution or completion message failed');
|
|
608
|
+
}
|
|
609
|
+
})();
|
|
610
|
+
}
|
|
611
|
+
return persisted.id;
|
|
814
612
|
}
|
|
815
613
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
session.conversationId,
|
|
822
|
-
'assistant',
|
|
823
|
-
JSON.stringify(replyMsg.content),
|
|
824
|
-
guardianChannelMeta,
|
|
825
|
-
);
|
|
826
|
-
session.messages.push(replyMsg);
|
|
827
|
-
onEvent({ type: 'assistant_text_delta', text: replyText });
|
|
828
|
-
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
614
|
+
// EXPIRED state handler
|
|
615
|
+
if (state === 'expired' && request.status === 'expired' && request.followupState === 'none') {
|
|
616
|
+
const userMsg = createUserMessage(content, attachments);
|
|
617
|
+
const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
|
|
618
|
+
session.messages.push(userMsg);
|
|
829
619
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
620
|
+
// Superseded remap
|
|
621
|
+
if (request.expiredReason === 'superseded') {
|
|
622
|
+
const callSession = getCallSession(request.callSessionId);
|
|
623
|
+
const callStillActive = callSession && !isTerminalState(callSession.status);
|
|
624
|
+
const currentPending = callStillActive ? getPendingRequestByCallSessionId(request.callSessionId) : null;
|
|
625
|
+
|
|
626
|
+
if (callStillActive && currentPending) {
|
|
627
|
+
const currentDeliveries = getDeliveriesByRequestId(currentPending.id);
|
|
628
|
+
const guardianExtUserId = session.guardianContext?.guardianExternalUserId;
|
|
629
|
+
// When guardianExternalUserId is present, verify the sender has a
|
|
630
|
+
// matching delivery on the current pending request. When it's absent
|
|
631
|
+
// (trusted Vellum/HTTP session), allow the remap without delivery check.
|
|
632
|
+
const senderHasDelivery = guardianExtUserId
|
|
633
|
+
? currentDeliveries.some((d) => d.destinationExternalUserId === guardianExtUserId)
|
|
634
|
+
: true;
|
|
635
|
+
if (!senderHasDelivery) {
|
|
636
|
+
log.info({ supersededRequestId: request.id, currentRequestId: currentPending.id, guardianExternalUserId: guardianExtUserId }, 'Superseded remap skipped: sender has no delivery on current pending request');
|
|
637
|
+
} else {
|
|
638
|
+
const remapResult = await answerCall({ callSessionId: currentPending.callSessionId, answer: answerText, pendingQuestionId: currentPending.pendingQuestionId });
|
|
639
|
+
if ('ok' in remapResult && remapResult.ok) {
|
|
640
|
+
const resolved = resolveGuardianActionRequest(currentPending.id, answerText, 'vellum');
|
|
641
|
+
if (resolved) {
|
|
642
|
+
await tryMintGuardianActionGrant({ request: currentPending, answerText, decisionChannel: 'vellum', approvalConversationGenerator: _approvalConversationGenerator });
|
|
643
|
+
}
|
|
644
|
+
const remapText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_superseded_remap', questionText: currentPending.questionText }, {}, _guardianActionCopyGenerator);
|
|
645
|
+
const remapMsg = createAssistantMessage(remapText);
|
|
646
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(remapMsg.content), guardianChannelMeta);
|
|
647
|
+
session.messages.push(remapMsg);
|
|
648
|
+
onEvent({ type: 'assistant_text_delta', text: remapText });
|
|
649
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
650
|
+
log.info({ supersededRequestId: request.id, remappedToRequestId: currentPending.id }, 'Late approval for superseded request remapped to current pending request');
|
|
651
|
+
return persisted.id;
|
|
652
|
+
}
|
|
653
|
+
log.warn({ callSessionId: currentPending.callSessionId, error: 'error' in remapResult ? remapResult.error : 'unknown' }, 'Superseded remap answerCall failed, falling through to follow-up');
|
|
654
|
+
}
|
|
853
655
|
}
|
|
854
|
-
}
|
|
855
|
-
}
|
|
656
|
+
}
|
|
856
657
|
|
|
857
|
-
|
|
658
|
+
const followupResult = startFollowupFromExpiredRequest(request.id, answerText);
|
|
659
|
+
if (followupResult) {
|
|
660
|
+
const followupText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_late_answer_followup', questionText: request.questionText, lateAnswerText: answerText }, {}, _guardianActionCopyGenerator);
|
|
661
|
+
const replyMsg = createAssistantMessage(followupText);
|
|
662
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(replyMsg.content), guardianChannelMeta);
|
|
663
|
+
session.messages.push(replyMsg);
|
|
664
|
+
onEvent({ type: 'assistant_text_delta', text: followupText });
|
|
665
|
+
} else {
|
|
666
|
+
const staleText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_expired' }, {}, _guardianActionCopyGenerator);
|
|
667
|
+
const staleMsg = createAssistantMessage(staleText);
|
|
668
|
+
await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(staleMsg.content), guardianChannelMeta);
|
|
669
|
+
session.messages.push(staleMsg);
|
|
670
|
+
onEvent({ type: 'assistant_text_delta', text: staleText });
|
|
671
|
+
}
|
|
672
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
673
|
+
return persisted.id;
|
|
674
|
+
}
|
|
858
675
|
}
|
|
859
676
|
}
|
|
860
677
|
}
|
|
@@ -942,6 +759,15 @@ export async function processMessage(
|
|
|
942
759
|
log.info({ conversationId: session.conversationId, channelHint: guardianIntent.channelHint }, 'Guardian verification intent intercepted — forcing skill flow');
|
|
943
760
|
agentLoopContent = guardianIntent.rewrittenContent;
|
|
944
761
|
session.preactivatedSkillIds = ['guardian-verify-setup'];
|
|
762
|
+
} else {
|
|
763
|
+
// Guardian invite intent interception — force invite management
|
|
764
|
+
// requests into the trusted-contacts skill flow.
|
|
765
|
+
const inviteIntent = resolveGuardianInviteIntent(resolvedContent);
|
|
766
|
+
if (inviteIntent.kind === 'invite_management') {
|
|
767
|
+
log.info({ conversationId: session.conversationId, action: inviteIntent.action }, 'Guardian invite intent intercepted — forcing skill flow');
|
|
768
|
+
agentLoopContent = inviteIntent.rewrittenContent;
|
|
769
|
+
session.preactivatedSkillIds = ['trusted-contacts'];
|
|
770
|
+
}
|
|
945
771
|
}
|
|
946
772
|
}
|
|
947
773
|
|