@vellumai/assistant 0.4.2 → 0.4.4
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/.env.example +3 -0
- package/ARCHITECTURE.md +124 -10
- package/README.md +43 -35
- package/docs/trusted-contact-access.md +20 -0
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +1 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
- package/src/__tests__/access-request-decision.test.ts +0 -1
- package/src/__tests__/actor-token-service.test.ts +1099 -0
- package/src/__tests__/agent-loop.test.ts +51 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
- package/src/__tests__/assistant-id-boundary-guard.test.ts +415 -0
- package/src/__tests__/call-controller.test.ts +49 -0
- package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
- package/src/__tests__/call-pointer-messages.test.ts +93 -3
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
- package/src/__tests__/call-routes-http.test.ts +0 -25
- package/src/__tests__/callback-handoff-copy.test.ts +186 -0
- package/src/__tests__/channel-approval-routes.test.ts +133 -12
- package/src/__tests__/channel-guardian.test.ts +0 -86
- package/src/__tests__/channel-readiness-service.test.ts +10 -16
- package/src/__tests__/checker.test.ts +33 -12
- package/src/__tests__/config-schema.test.ts +6 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
- package/src/__tests__/conversation-routes.test.ts +12 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/daemon-server-session-init.test.ts +4 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -5
- package/src/__tests__/guardian-question-mode.test.ts +200 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
- package/src/__tests__/guardian-routing-state.test.ts +525 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
- package/src/__tests__/handlers-telegram-config.test.ts +0 -83
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
- package/src/__tests__/headless-browser-navigate.test.ts +2 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
- package/src/__tests__/ingress-routes-http.test.ts +55 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +159 -9
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +106 -2
- package/src/__tests__/notification-guardian-path.test.ts +3 -0
- package/src/__tests__/recording-intent-handler.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +1475 -33
- package/src/__tests__/send-endpoint-busy.test.ts +5 -0
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-confirmation-signals.test.ts +523 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +4 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +44 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +21 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/__tests__/twilio-routes.test.ts +4 -3
- package/src/__tests__/update-bulletin.test.ts +0 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +12 -3
- package/src/approvals/guardian-request-resolvers.ts +169 -11
- package/src/calls/call-constants.ts +29 -0
- package/src/calls/call-controller.ts +11 -3
- package/src/calls/call-domain.ts +33 -11
- package/src/calls/call-pointer-message-composer.ts +154 -0
- package/src/calls/call-pointer-messages.ts +106 -27
- package/src/calls/guardian-dispatch.ts +4 -2
- package/src/calls/relay-server.ts +921 -112
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +4 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-session-bridge.ts +4 -3
- package/src/cli/core-commands.ts +7 -4
- package/src/cli.ts +5 -4
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +309 -10
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
- package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
- package/src/config/bundled-skills/messaging/SKILL.md +61 -12
- package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
- package/src/config/bundled-skills/twitter/SKILL.md +3 -3
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +215 -0
- package/src/config/calls-schema.ts +36 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schema.ts +2 -2
- package/src/config/skills.ts +11 -0
- package/src/config/system-prompt.ts +11 -1
- package/src/config/templates/SOUL.md +2 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
- package/src/daemon/call-pointer-generators.ts +59 -0
- package/src/daemon/computer-use-session.ts +2 -5
- package/src/daemon/handlers/apps.ts +76 -20
- package/src/daemon/handlers/config-channels.ts +9 -61
- package/src/daemon/handlers/config-inbox.ts +11 -3
- package/src/daemon/handlers/config-ingress.ts +28 -3
- package/src/daemon/handlers/config-telegram.ts +12 -0
- package/src/daemon/handlers/config.ts +2 -6
- package/src/daemon/handlers/index.ts +2 -1
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/publish.ts +11 -46
- package/src/daemon/handlers/sessions.ts +59 -5
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/apps.ts +1 -0
- package/src/daemon/ipc-contract/inbox.ts +4 -0
- package/src/daemon/ipc-contract/integrations.ts +1 -97
- package/src/daemon/ipc-contract/messages.ts +47 -1
- package/src/daemon/ipc-contract/notifications.ts +11 -0
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +17 -0
- package/src/daemon/server.ts +16 -2
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +24 -12
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +6 -1
- package/src/daemon/session-surfaces.ts +32 -3
- package/src/daemon/session.ts +88 -1
- package/src/daemon/tool-side-effects.ts +22 -0
- package/src/home-base/prebuilt/brain-graph.html +1483 -0
- package/src/home-base/prebuilt/index.html +40 -0
- package/src/inbound/platform-callback-registration.ts +157 -0
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/conversation-crud.ts +2 -1
- package/src/memory/conversation-title-service.ts +16 -2
- package/src/memory/db-init.ts +8 -0
- package/src/memory/delivery-crud.ts +2 -1
- package/src/memory/guardian-action-store.ts +2 -1
- package/src/memory/guardian-approvals.ts +3 -2
- package/src/memory/ingress-invite-store.ts +12 -2
- package/src/memory/ingress-member-store.ts +4 -3
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema.ts +26 -5
- package/src/messaging/provider-types.ts +24 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/adapter.ts +127 -0
- package/src/messaging/providers/sms/adapter.ts +40 -37
- package/src/notifications/adapters/macos.ts +45 -2
- package/src/notifications/broadcaster.ts +16 -0
- package/src/notifications/copy-composer.ts +50 -2
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +18 -9
- package/src/notifications/guardian-question-mode.ts +419 -0
- package/src/notifications/signal.ts +14 -3
- package/src/permissions/checker.ts +13 -1
- package/src/permissions/prompter.ts +14 -0
- package/src/providers/anthropic/client.ts +20 -0
- package/src/providers/provider-send-message.ts +15 -3
- package/src/runtime/access-request-helper.ts +82 -4
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/assistant-scope.ts +10 -0
- package/src/runtime/channel-approvals.ts +5 -3
- package/src/runtime/channel-readiness-service.ts +23 -64
- package/src/runtime/channel-readiness-types.ts +3 -4
- package/src/runtime/channel-retry-sweep.ts +4 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-context-resolver.ts +82 -0
- package/src/runtime/guardian-outbound-actions.ts +5 -7
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +75 -31
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/ingress-service.ts +14 -0
- package/src/runtime/invite-redemption-service.ts +10 -1
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/middleware/twilio-validation.ts +2 -4
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/channel-route-shared.ts +3 -3
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +142 -53
- package/src/runtime/routes/events-routes.ts +22 -8
- package/src/runtime/routes/guardian-action-routes.ts +45 -3
- package/src/runtime/routes/guardian-approval-interception.ts +29 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
- package/src/runtime/routes/inbound-conversation.ts +4 -3
- package/src/runtime/routes/inbound-message-handler.ts +147 -5
- package/src/runtime/routes/ingress-routes.ts +2 -0
- package/src/runtime/routes/integration-routes.ts +7 -15
- package/src/runtime/routes/pairing-routes.ts +163 -0
- package/src/runtime/routes/twilio-routes.ts +934 -0
- package/src/runtime/tool-grant-request-helper.ts +3 -1
- package/src/security/oauth2.ts +27 -2
- package/src/security/token-manager.ts +46 -10
- package/src/tools/browser/browser-execution.ts +4 -3
- package/src/tools/browser/browser-handoff.ts +10 -18
- package/src/tools/browser/browser-manager.ts +80 -25
- package/src/tools/browser/browser-screencast.ts +35 -119
- package/src/tools/calls/call-start.ts +2 -1
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/terminal/parser.ts +12 -0
- package/src/tools/tool-approval-handler.ts +244 -19
- package/src/workspace/git-service.ts +19 -0
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
package/src/calls/call-domain.ts
CHANGED
|
@@ -9,11 +9,13 @@ import { getTwilioUserPhoneNumber } from '../config/env.js';
|
|
|
9
9
|
import { loadConfig } from '../config/loader.js';
|
|
10
10
|
import { VALID_CALLER_IDENTITY_MODES } from '../config/schema.js';
|
|
11
11
|
import type { AssistantConfig } from '../config/types.js';
|
|
12
|
+
import { resolveCallbackUrl } from '../inbound/platform-callback-registration.js';
|
|
12
13
|
import { getTwilioStatusCallbackUrl,getTwilioVoiceWebhookUrl } from '../inbound/public-ingress-urls.js';
|
|
13
14
|
import { getOrCreateConversation } from '../memory/conversation-key-store.js';
|
|
14
15
|
import { queueGenerateConversationTitle } from '../memory/conversation-title-service.js';
|
|
15
16
|
import { upsertBinding } from '../memory/external-conversation-store.js';
|
|
16
17
|
import { revokeScopedApprovalGrantsForContext } from '../memory/scoped-approval-grants.js';
|
|
18
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
|
|
17
19
|
import { isGuardian } from '../runtime/channel-guardian-service.js';
|
|
18
20
|
import { getSecureKey } from '../security/secure-keys.js';
|
|
19
21
|
import { getLogger } from '../util/logger.js';
|
|
@@ -100,8 +102,7 @@ export type CallerIdentityResult =
|
|
|
100
102
|
* - Otherwise, always use `assistant_number` (implicit default).
|
|
101
103
|
*
|
|
102
104
|
* For `assistant_number`: uses the Twilio phone number from
|
|
103
|
-
* `getTwilioConfig(
|
|
104
|
-
* No eligibility check is performed — this is a fast path.
|
|
105
|
+
* `getTwilioConfig()`. No eligibility check is performed — this is a fast path.
|
|
105
106
|
* For `user_number`: uses `config.calls.callerIdentity.userNumber` or the
|
|
106
107
|
* secure key `credential:twilio:user_phone_number`, then validates that the
|
|
107
108
|
* number is usable as an outbound caller ID via the Twilio API.
|
|
@@ -133,7 +134,7 @@ export async function resolveCallerIdentity(
|
|
|
133
134
|
|
|
134
135
|
if (mode === 'assistant_number') {
|
|
135
136
|
const twilioConfig = getTwilioConfig(assistantId);
|
|
136
|
-
log.info({ mode, source, fromNumber: twilioConfig.phoneNumber
|
|
137
|
+
log.info({ mode, source, fromNumber: twilioConfig.phoneNumber }, 'Resolved caller identity');
|
|
137
138
|
return { ok: true, mode, fromNumber: twilioConfig.phoneNumber, source };
|
|
138
139
|
}
|
|
139
140
|
|
|
@@ -208,7 +209,7 @@ export type CreateInboundVoiceSessionResult = {
|
|
|
208
209
|
export function createInboundVoiceSession(
|
|
209
210
|
input: CreateInboundVoiceSessionInput,
|
|
210
211
|
): CreateInboundVoiceSessionResult {
|
|
211
|
-
const { callSid, fromNumber, toNumber, assistantId =
|
|
212
|
+
const { callSid, fromNumber, toNumber, assistantId = DAEMON_INTERNAL_ASSISTANT_ID } = input;
|
|
212
213
|
|
|
213
214
|
// Check if a session already exists for this CallSid (replay protection)
|
|
214
215
|
const existing = getCallSessionByCallSid(callSid);
|
|
@@ -219,7 +220,7 @@ export function createInboundVoiceSession(
|
|
|
219
220
|
|
|
220
221
|
// Create a dedicated voice conversation keyed by CallSid so inbound calls
|
|
221
222
|
// get their own conversation thread.
|
|
222
|
-
const voiceConvKey = assistantId && assistantId !==
|
|
223
|
+
const voiceConvKey = assistantId && assistantId !== DAEMON_INTERNAL_ASSISTANT_ID
|
|
223
224
|
? `asst:${assistantId}:voice:inbound:${callSid}`
|
|
224
225
|
: `voice:inbound:${callSid}`;
|
|
225
226
|
const { conversationId: voiceConversationId } = getOrCreateConversation(voiceConvKey);
|
|
@@ -272,7 +273,7 @@ export function createInboundVoiceSession(
|
|
|
272
273
|
* Initiate a new outbound call.
|
|
273
274
|
*/
|
|
274
275
|
export async function startCall(input: StartCallInput): Promise<StartCallResult | CallError> {
|
|
275
|
-
const { phoneNumber, task, context: callContext, conversationId, callerIdentityMode, assistantId =
|
|
276
|
+
const { phoneNumber, task, context: callContext, conversationId, callerIdentityMode, assistantId = DAEMON_INTERNAL_ASSISTANT_ID } = input;
|
|
276
277
|
|
|
277
278
|
if (!phoneNumber || typeof phoneNumber !== 'string') {
|
|
278
279
|
return { ok: false, error: 'phone_number is required and must be a string', status: 400 };
|
|
@@ -357,11 +358,23 @@ export async function startCall(input: StartCallInput): Promise<StartCallResult
|
|
|
357
358
|
|
|
358
359
|
log.info({ callSessionId: session.id, voiceConversationId, initiatedFrom: conversationId, to: phoneNumber, from: fromNumber, task }, 'Initiating outbound call');
|
|
359
360
|
|
|
361
|
+
const webhookUrl = await resolveCallbackUrl(
|
|
362
|
+
() => getTwilioVoiceWebhookUrl(ingressConfig, session.id),
|
|
363
|
+
'webhooks/twilio/voice',
|
|
364
|
+
'twilio_voice',
|
|
365
|
+
{ callSessionId: session.id },
|
|
366
|
+
);
|
|
367
|
+
const statusCallbackUrl = await resolveCallbackUrl(
|
|
368
|
+
() => getTwilioStatusCallbackUrl(ingressConfig),
|
|
369
|
+
'webhooks/twilio/status',
|
|
370
|
+
'twilio_status',
|
|
371
|
+
);
|
|
372
|
+
|
|
360
373
|
const { callSid } = await provider.initiateCall({
|
|
361
374
|
from: fromNumber,
|
|
362
375
|
to: phoneNumber,
|
|
363
|
-
webhookUrl
|
|
364
|
-
statusCallbackUrl
|
|
376
|
+
webhookUrl,
|
|
377
|
+
statusCallbackUrl,
|
|
365
378
|
});
|
|
366
379
|
|
|
367
380
|
updateCallSession(session.id, { providerCallSid: callSid });
|
|
@@ -644,7 +657,7 @@ export type StartGuardianVerificationCallResult =
|
|
|
644
657
|
export async function startGuardianVerificationCall(
|
|
645
658
|
input: StartGuardianVerificationCallInput,
|
|
646
659
|
): Promise<StartGuardianVerificationCallResult> {
|
|
647
|
-
const { phoneNumber, guardianVerificationSessionId, assistantId =
|
|
660
|
+
const { phoneNumber, guardianVerificationSessionId, assistantId = DAEMON_INTERNAL_ASSISTANT_ID, originConversationId } = input;
|
|
648
661
|
|
|
649
662
|
if (!phoneNumber || !E164_REGEX.test(phoneNumber)) {
|
|
650
663
|
return { ok: false, error: 'phone_number must be in E.164 format', status: 400 };
|
|
@@ -686,8 +699,17 @@ export async function startGuardianVerificationCall(
|
|
|
686
699
|
});
|
|
687
700
|
sessionId = session.id;
|
|
688
701
|
|
|
689
|
-
const webhookUrl =
|
|
690
|
-
|
|
702
|
+
const webhookUrl = await resolveCallbackUrl(
|
|
703
|
+
() => getTwilioVoiceWebhookUrl(config, session.id),
|
|
704
|
+
'webhooks/twilio/voice',
|
|
705
|
+
'twilio_voice',
|
|
706
|
+
{ callSessionId: session.id },
|
|
707
|
+
);
|
|
708
|
+
const statusCallbackUrl = await resolveCallbackUrl(
|
|
709
|
+
() => getTwilioStatusCallbackUrl(config),
|
|
710
|
+
'webhooks/twilio/status',
|
|
711
|
+
'twilio_status',
|
|
712
|
+
);
|
|
691
713
|
|
|
692
714
|
const { callSid } = await provider.initiateCall({
|
|
693
715
|
from: identityResult.fromNumber,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layered call pointer message composition system.
|
|
3
|
+
*
|
|
4
|
+
* Generates pointer/status copy through a priority chain:
|
|
5
|
+
* 1. Generator-produced text (when provided by daemon and audience is trusted)
|
|
6
|
+
* 2. Deterministic fallback templates (preserving existing semantics)
|
|
7
|
+
*
|
|
8
|
+
* Follows the same pattern as approval-message-composer.ts and
|
|
9
|
+
* guardian-action-message-composer.ts.
|
|
10
|
+
*/
|
|
11
|
+
import type { PointerCopyGenerator } from '../runtime/http-types.js';
|
|
12
|
+
import { getLogger } from '../util/logger.js';
|
|
13
|
+
|
|
14
|
+
const log = getLogger('call-pointer-message-composer');
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export type CallPointerMessageScenario =
|
|
21
|
+
| 'started'
|
|
22
|
+
| 'completed'
|
|
23
|
+
| 'failed'
|
|
24
|
+
| 'guardian_verification_succeeded'
|
|
25
|
+
| 'guardian_verification_failed';
|
|
26
|
+
|
|
27
|
+
export interface CallPointerMessageContext {
|
|
28
|
+
scenario: CallPointerMessageScenario;
|
|
29
|
+
phoneNumber: string;
|
|
30
|
+
duration?: string;
|
|
31
|
+
reason?: string;
|
|
32
|
+
verificationCode?: string;
|
|
33
|
+
channel?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ComposeCallPointerMessageOptions {
|
|
37
|
+
fallbackText?: string;
|
|
38
|
+
requiredFacts?: string[];
|
|
39
|
+
maxTokens?: number;
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Constants (exported for the daemon-injected generator implementation)
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export const POINTER_COPY_TIMEOUT_MS = 3_000;
|
|
48
|
+
export const POINTER_COPY_MAX_TOKENS = 120;
|
|
49
|
+
export const POINTER_COPY_SYSTEM_PROMPT =
|
|
50
|
+
'You are an assistant writing a brief status update about a phone call. '
|
|
51
|
+
+ 'Keep it concise (1-2 sentences), natural, and informative. '
|
|
52
|
+
+ 'Preserve all factual details exactly (phone numbers, durations, failure reasons, verification status). '
|
|
53
|
+
+ 'Do not mention internal systems or technical details. '
|
|
54
|
+
+ 'Return plain text only.';
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Public API
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Compose pointer copy using the daemon-injected generator when available,
|
|
62
|
+
* with deterministic fallback for reliability.
|
|
63
|
+
*
|
|
64
|
+
* The generator parameter is the daemon-provided function that knows about
|
|
65
|
+
* providers. When absent (or in test env), only the deterministic fallback
|
|
66
|
+
* is used.
|
|
67
|
+
*/
|
|
68
|
+
export async function composeCallPointerMessageGenerative(
|
|
69
|
+
context: CallPointerMessageContext,
|
|
70
|
+
options: ComposeCallPointerMessageOptions = {},
|
|
71
|
+
generator?: PointerCopyGenerator,
|
|
72
|
+
): Promise<string> {
|
|
73
|
+
const fallbackText = options.fallbackText?.trim() || getPointerFallbackMessage(context);
|
|
74
|
+
|
|
75
|
+
if (process.env.NODE_ENV === 'test') {
|
|
76
|
+
return fallbackText;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (generator) {
|
|
80
|
+
try {
|
|
81
|
+
const generated = await generator(context, options);
|
|
82
|
+
if (generated) return generated;
|
|
83
|
+
} catch (err) {
|
|
84
|
+
log.warn({ err, scenario: context.scenario }, 'Failed to generate pointer copy, using fallback');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return fallbackText;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @internal Exported for use by the daemon-injected generator implementation. */
|
|
92
|
+
export function buildPointerGenerationPrompt(
|
|
93
|
+
context: CallPointerMessageContext,
|
|
94
|
+
fallbackText: string,
|
|
95
|
+
requiredFacts: string[] | undefined,
|
|
96
|
+
): string {
|
|
97
|
+
const factClause = requiredFacts && requiredFacts.length > 0
|
|
98
|
+
? `Required facts to include: ${requiredFacts.join(', ')}.\n`
|
|
99
|
+
: '';
|
|
100
|
+
return [
|
|
101
|
+
'Rewrite the following call status message as a natural, conversational update.',
|
|
102
|
+
'Keep the same concrete facts (phone number, duration, failure reason, verification status).',
|
|
103
|
+
factClause,
|
|
104
|
+
`Context JSON: ${JSON.stringify(context)}`,
|
|
105
|
+
`Fallback message: ${fallbackText}`,
|
|
106
|
+
].filter(Boolean).join('\n\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @internal Exported for use by the daemon-injected generator implementation. */
|
|
110
|
+
export function includesRequiredFacts(text: string, requiredFacts: string[] | undefined): boolean {
|
|
111
|
+
if (!requiredFacts || requiredFacts.length === 0) return true;
|
|
112
|
+
return requiredFacts.every((fact) => text.includes(fact));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Deterministic fallback templates
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Return a scenario-specific deterministic fallback message.
|
|
121
|
+
*
|
|
122
|
+
* These preserve the exact semantics of the original hard-coded pointer
|
|
123
|
+
* templates from call-pointer-messages.ts.
|
|
124
|
+
*/
|
|
125
|
+
export function getPointerFallbackMessage(context: CallPointerMessageContext): string {
|
|
126
|
+
switch (context.scenario) {
|
|
127
|
+
case 'started':
|
|
128
|
+
return context.verificationCode
|
|
129
|
+
? `\u{1F4DE} Call to ${context.phoneNumber} started. Verification code: ${context.verificationCode}`
|
|
130
|
+
: `\u{1F4DE} Call to ${context.phoneNumber} started.`;
|
|
131
|
+
case 'completed':
|
|
132
|
+
return context.duration
|
|
133
|
+
? `\u{1F4DE} Call to ${context.phoneNumber} completed (${context.duration}).`
|
|
134
|
+
: `\u{1F4DE} Call to ${context.phoneNumber} completed.`;
|
|
135
|
+
case 'failed':
|
|
136
|
+
return context.reason
|
|
137
|
+
? `\u{1F4DE} Call to ${context.phoneNumber} failed: ${context.reason}.`
|
|
138
|
+
: `\u{1F4DE} Call to ${context.phoneNumber} failed.`;
|
|
139
|
+
case 'guardian_verification_succeeded': {
|
|
140
|
+
const ch = context.channel ?? 'voice';
|
|
141
|
+
return `\u{2705} Guardian verification (${ch}) for ${context.phoneNumber} succeeded.`;
|
|
142
|
+
}
|
|
143
|
+
case 'guardian_verification_failed': {
|
|
144
|
+
const ch = context.channel ?? 'voice';
|
|
145
|
+
return context.reason
|
|
146
|
+
? `\u{274C} Guardian verification (${ch}) for ${context.phoneNumber} failed: ${context.reason}.`
|
|
147
|
+
: `\u{274C} Guardian verification (${ch}) for ${context.phoneNumber} failed.`;
|
|
148
|
+
}
|
|
149
|
+
default: {
|
|
150
|
+
const _exhaustive: never = context.scenario;
|
|
151
|
+
return `Call status update. ${String(_exhaustive)}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -2,47 +2,126 @@
|
|
|
2
2
|
* Concise pointer/status messages posted to the initiating conversation
|
|
3
3
|
* so the user sees call lifecycle events without the full transcript
|
|
4
4
|
* (which lives in the dedicated voice conversation).
|
|
5
|
+
*
|
|
6
|
+
* Trust-aware: trusted audiences receive assistant-generated copy when a
|
|
7
|
+
* generator is available; untrusted/unknown audiences always receive
|
|
8
|
+
* deterministic fallback text.
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
import * as conversationStore from '../memory/conversation-store.js';
|
|
12
|
+
import type { PointerCopyGenerator } from '../runtime/http-types.js';
|
|
13
|
+
import { getLogger } from '../util/logger.js';
|
|
14
|
+
import {
|
|
15
|
+
type CallPointerMessageContext,
|
|
16
|
+
composeCallPointerMessageGenerative,
|
|
17
|
+
getPointerFallbackMessage,
|
|
18
|
+
} from './call-pointer-message-composer.js';
|
|
19
|
+
|
|
20
|
+
const log = getLogger('call-pointer-messages');
|
|
8
21
|
|
|
9
22
|
export type PointerEvent = 'started' | 'completed' | 'failed' | 'guardian_verification_succeeded' | 'guardian_verification_failed';
|
|
10
23
|
|
|
24
|
+
export type PointerAudienceMode = 'auto' | 'trusted' | 'untrusted';
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Module-level generator injection (set by daemon lifecycle at startup)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
let pointerCopyGenerator: PointerCopyGenerator | undefined;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Inject the daemon-provided pointer copy generator.
|
|
34
|
+
* Called from daemon/lifecycle.ts at startup, following the same pattern
|
|
35
|
+
* as setRelayBroadcast.
|
|
36
|
+
*/
|
|
37
|
+
export function setPointerCopyGenerator(generator: PointerCopyGenerator): void {
|
|
38
|
+
pointerCopyGenerator = generator;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @internal Reset for tests. */
|
|
42
|
+
export function resetPointerCopyGenerator(): void {
|
|
43
|
+
pointerCopyGenerator = undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Trust resolution
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve whether the audience for a pointer message is trusted.
|
|
52
|
+
*
|
|
53
|
+
* Trusted when:
|
|
54
|
+
* - conversation threadType is 'private' (local desktop-origin context)
|
|
55
|
+
* - conversation origin channel is 'vellum' (desktop app)
|
|
56
|
+
*
|
|
57
|
+
* Untrusted by default when insufficient evidence.
|
|
58
|
+
*/
|
|
59
|
+
function resolvePointerAudienceTrust(conversationId: string): boolean {
|
|
60
|
+
try {
|
|
61
|
+
const threadType = conversationStore.getConversationThreadType(conversationId);
|
|
62
|
+
if (threadType === 'private') return true;
|
|
63
|
+
|
|
64
|
+
const originChannel = conversationStore.getConversationOriginChannel(conversationId);
|
|
65
|
+
if (originChannel === 'vellum') return true;
|
|
66
|
+
} catch {
|
|
67
|
+
// Conversation may not exist or DB may be unavailable — default untrusted.
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Public API
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
11
77
|
export async function addPointerMessage(
|
|
12
78
|
conversationId: string,
|
|
13
79
|
event: PointerEvent,
|
|
14
80
|
phoneNumber: string,
|
|
15
81
|
extra?: { duration?: string; reason?: string; verificationCode?: string; channel?: string },
|
|
82
|
+
audienceMode: PointerAudienceMode = 'auto',
|
|
16
83
|
): Promise<void> {
|
|
84
|
+
const context: CallPointerMessageContext = {
|
|
85
|
+
scenario: event,
|
|
86
|
+
phoneNumber,
|
|
87
|
+
duration: extra?.duration,
|
|
88
|
+
reason: extra?.reason,
|
|
89
|
+
verificationCode: extra?.verificationCode,
|
|
90
|
+
channel: extra?.channel,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Build required-facts list so generated text cannot drop key details.
|
|
94
|
+
const requiredFacts: string[] = [phoneNumber];
|
|
95
|
+
if (extra?.duration) requiredFacts.push(extra.duration);
|
|
96
|
+
if (extra?.verificationCode) requiredFacts.push(extra.verificationCode);
|
|
97
|
+
if (extra?.reason) requiredFacts.push(extra.reason);
|
|
98
|
+
|
|
99
|
+
// Enforce lifecycle outcome keywords so the LLM cannot rewrite e.g. a
|
|
100
|
+
// "failed" event as a success — the generated text must contain the
|
|
101
|
+
// outcome word verbatim.
|
|
102
|
+
const eventOutcomeKeywords: Record<PointerEvent, string | undefined> = {
|
|
103
|
+
started: 'started',
|
|
104
|
+
completed: 'completed',
|
|
105
|
+
failed: 'failed',
|
|
106
|
+
guardian_verification_succeeded: 'succeeded',
|
|
107
|
+
guardian_verification_failed: 'failed',
|
|
108
|
+
};
|
|
109
|
+
const outcomeKeyword = eventOutcomeKeywords[event];
|
|
110
|
+
if (outcomeKeyword) requiredFacts.push(outcomeKeyword);
|
|
111
|
+
|
|
17
112
|
let text: string;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
break;
|
|
29
|
-
case 'failed':
|
|
30
|
-
text = extra?.reason
|
|
31
|
-
? `\u{1F4DE} Call to ${phoneNumber} failed: ${extra.reason}.`
|
|
32
|
-
: `\u{1F4DE} Call to ${phoneNumber} failed.`;
|
|
33
|
-
break;
|
|
34
|
-
case 'guardian_verification_succeeded': {
|
|
35
|
-
const ch = extra?.channel ?? 'voice';
|
|
36
|
-
text = `\u{2705} Guardian verification (${ch}) for ${phoneNumber} succeeded.`;
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
case 'guardian_verification_failed': {
|
|
40
|
-
const ch = extra?.channel ?? 'voice';
|
|
41
|
-
text = extra?.reason
|
|
42
|
-
? `\u{274C} Guardian verification (${ch}) for ${phoneNumber} failed: ${extra.reason}.`
|
|
43
|
-
: `\u{274C} Guardian verification (${ch}) for ${phoneNumber} failed.`;
|
|
44
|
-
break;
|
|
113
|
+
|
|
114
|
+
const isTrusted =
|
|
115
|
+
audienceMode === 'trusted' ||
|
|
116
|
+
(audienceMode === 'auto' && resolvePointerAudienceTrust(conversationId));
|
|
117
|
+
|
|
118
|
+
if (isTrusted && pointerCopyGenerator) {
|
|
119
|
+
text = await composeCallPointerMessageGenerative(context, { requiredFacts }, pointerCopyGenerator);
|
|
120
|
+
} else {
|
|
121
|
+
if (!isTrusted && pointerCopyGenerator) {
|
|
122
|
+
log.debug({ event, conversationId }, 'Untrusted audience — using deterministic pointer copy');
|
|
45
123
|
}
|
|
124
|
+
text = getPointerFallbackMessage(context);
|
|
46
125
|
}
|
|
47
126
|
|
|
48
127
|
// Pointer messages are assistant-generated status updates in the initiating
|
|
@@ -149,6 +149,7 @@ async function dispatchGuardianQuestionInner(params: GuardianDispatchParams): Pr
|
|
|
149
149
|
// Route through the canonical notification pipeline. The paired vellum
|
|
150
150
|
// conversation from this pipeline is the canonical guardian thread.
|
|
151
151
|
let vellumDeliveryId: string | null = null;
|
|
152
|
+
const requestCode = request.requestCode ?? request.id.slice(0, 6).toUpperCase();
|
|
152
153
|
const signalResult = await emitNotificationSignal({
|
|
153
154
|
sourceEventName: 'guardian.question',
|
|
154
155
|
sourceChannel: 'voice',
|
|
@@ -163,10 +164,11 @@ async function dispatchGuardianQuestionInner(params: GuardianDispatchParams): Pr
|
|
|
163
164
|
},
|
|
164
165
|
contextPayload: {
|
|
165
166
|
requestId: request.id,
|
|
166
|
-
|
|
167
|
+
requestKind: 'pending_question',
|
|
168
|
+
requestCode,
|
|
167
169
|
callSessionId,
|
|
170
|
+
toolName,
|
|
168
171
|
questionText: pendingQuestion.questionText,
|
|
169
|
-
pendingQuestionId: pendingQuestion.id,
|
|
170
172
|
activeGuardianRequestCount,
|
|
171
173
|
},
|
|
172
174
|
conversationAffinityHint,
|