@vellumai/assistant 0.4.2 → 0.4.3

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.
Files changed (84) hide show
  1. package/ARCHITECTURE.md +84 -7
  2. package/docs/trusted-contact-access.md +20 -0
  3. package/package.json +1 -1
  4. package/src/__tests__/access-request-decision.test.ts +0 -1
  5. package/src/__tests__/assistant-id-boundary-guard.test.ts +290 -0
  6. package/src/__tests__/call-routes-http.test.ts +0 -25
  7. package/src/__tests__/channel-guardian.test.ts +6 -5
  8. package/src/__tests__/config-schema.test.ts +2 -0
  9. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
  10. package/src/__tests__/guardian-actions-endpoint.test.ts +21 -0
  11. package/src/__tests__/guardian-outbound-http.test.ts +0 -1
  12. package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
  13. package/src/__tests__/ingress-routes-http.test.ts +55 -0
  14. package/src/__tests__/non-member-access-request.test.ts +28 -1
  15. package/src/__tests__/notification-decision-strategy.test.ts +44 -0
  16. package/src/__tests__/relay-server.test.ts +644 -4
  17. package/src/__tests__/session-init.benchmark.test.ts +0 -1
  18. package/src/__tests__/session-runtime-assembly.test.ts +4 -1
  19. package/src/__tests__/session-surfaces-task-progress.test.ts +43 -0
  20. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
  21. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  22. package/src/__tests__/trusted-contact-verification.test.ts +0 -1
  23. package/src/__tests__/twilio-routes.test.ts +4 -3
  24. package/src/__tests__/update-bulletin.test.ts +0 -1
  25. package/src/approvals/guardian-decision-primitive.ts +2 -1
  26. package/src/approvals/guardian-request-resolvers.ts +42 -3
  27. package/src/calls/call-constants.ts +8 -0
  28. package/src/calls/call-controller.ts +2 -1
  29. package/src/calls/call-domain.ts +5 -4
  30. package/src/calls/relay-server.ts +513 -116
  31. package/src/calls/twilio-routes.ts +3 -5
  32. package/src/calls/types.ts +1 -1
  33. package/src/calls/voice-session-bridge.ts +4 -3
  34. package/src/cli/core-commands.ts +7 -4
  35. package/src/config/bundled-skills/app-builder/SKILL.md +164 -1
  36. package/src/config/bundled-skills/vercel-token-setup/SKILL.md +214 -0
  37. package/src/config/calls-schema.ts +12 -0
  38. package/src/config/feature-flag-registry.json +0 -8
  39. package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -2
  40. package/src/daemon/handlers/config-channels.ts +5 -7
  41. package/src/daemon/handlers/config-inbox.ts +2 -0
  42. package/src/daemon/handlers/index.ts +2 -1
  43. package/src/daemon/handlers/publish.ts +11 -46
  44. package/src/daemon/handlers/sessions.ts +11 -2
  45. package/src/daemon/ipc-contract/apps.ts +1 -0
  46. package/src/daemon/ipc-contract/inbox.ts +4 -0
  47. package/src/daemon/ipc-contract/integrations.ts +3 -1
  48. package/src/daemon/server.ts +2 -1
  49. package/src/daemon/session-agent-loop.ts +2 -1
  50. package/src/daemon/session-runtime-assembly.ts +3 -1
  51. package/src/daemon/session-surfaces.ts +29 -1
  52. package/src/memory/conversation-crud.ts +2 -1
  53. package/src/memory/conversation-title-service.ts +16 -2
  54. package/src/memory/db-init.ts +4 -0
  55. package/src/memory/delivery-crud.ts +2 -1
  56. package/src/memory/guardian-action-store.ts +2 -1
  57. package/src/memory/guardian-approvals.ts +3 -2
  58. package/src/memory/ingress-invite-store.ts +12 -2
  59. package/src/memory/ingress-member-store.ts +4 -3
  60. package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
  61. package/src/memory/migrations/index.ts +1 -0
  62. package/src/memory/schema.ts +10 -5
  63. package/src/notifications/copy-composer.ts +11 -1
  64. package/src/notifications/emit-signal.ts +2 -1
  65. package/src/runtime/access-request-helper.ts +11 -3
  66. package/src/runtime/actor-trust-resolver.ts +2 -2
  67. package/src/runtime/assistant-scope.ts +10 -0
  68. package/src/runtime/guardian-outbound-actions.ts +5 -4
  69. package/src/runtime/http-server.ts +11 -20
  70. package/src/runtime/ingress-service.ts +14 -0
  71. package/src/runtime/invite-redemption-service.ts +2 -1
  72. package/src/runtime/middleware/twilio-validation.ts +2 -4
  73. package/src/runtime/routes/call-routes.ts +2 -1
  74. package/src/runtime/routes/channel-route-shared.ts +3 -3
  75. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  76. package/src/runtime/routes/conversation-routes.ts +2 -1
  77. package/src/runtime/routes/events-routes.ts +2 -3
  78. package/src/runtime/routes/inbound-conversation.ts +4 -3
  79. package/src/runtime/routes/inbound-message-handler.ts +4 -3
  80. package/src/runtime/routes/ingress-routes.ts +2 -0
  81. package/src/tools/calls/call-start.ts +2 -1
  82. package/src/tools/terminal/parser.ts +12 -0
  83. package/src/tools/tool-approval-handler.ts +2 -1
  84. package/src/workspace/git-service.ts +19 -0
@@ -147,10 +147,9 @@ function mapTwilioStatus(twilioStatus: string): CallStatus | null {
147
147
  * Supports two modes:
148
148
  * - **Outbound** (callSessionId present in query): uses the existing session
149
149
  * - **Inbound** (callSessionId absent): creates or reuses a session keyed
150
- * by the Twilio CallSid. The optional `forwardedAssistantId` is resolved
151
- * by the gateway from the "To" phone number.
150
+ * by the Twilio CallSid. Uses daemon internal scope for assistant identity.
152
151
  */
153
- export async function handleVoiceWebhook(req: Request, forwardedAssistantId?: string): Promise<Response> {
152
+ export async function handleVoiceWebhook(req: Request): Promise<Response> {
154
153
  const url = new URL(req.url);
155
154
  const callSessionId = url.searchParams.get('callSessionId');
156
155
 
@@ -167,13 +166,12 @@ export async function handleVoiceWebhook(req: Request, forwardedAssistantId?: st
167
166
  return new Response('Missing CallSid', { status: 400 });
168
167
  }
169
168
 
170
- log.info({ callSid, from: callerFrom, to: callerTo, assistantId: forwardedAssistantId }, 'Inbound voice webhook — creating/reusing session');
169
+ log.info({ callSid, from: callerFrom, to: callerTo }, 'Inbound voice webhook — creating/reusing session');
171
170
 
172
171
  const { session } = createInboundVoiceSession({
173
172
  callSid,
174
173
  fromNumber: callerFrom,
175
174
  toNumber: callerTo,
176
- assistantId: forwardedAssistantId,
177
175
  });
178
176
 
179
177
  return buildVoiceWebhookTwiml(session.id, session.assistantId ?? undefined, session.task, session.guardianVerificationSessionId);
@@ -1,5 +1,5 @@
1
1
  export type CallStatus = 'initiated' | 'ringing' | 'in_progress' | 'waiting_on_user' | 'completed' | 'failed' | 'cancelled';
2
- export type CallEventType = 'call_started' | 'call_connected' | 'caller_spoke' | 'assistant_spoke' | 'user_question_asked' | 'user_answered' | 'user_instruction_relayed' | 'call_ended' | 'call_failed' | 'callee_verification_started' | 'callee_verification_succeeded' | 'callee_verification_failed' | 'guardian_voice_verification_started' | 'guardian_voice_verification_succeeded' | 'guardian_voice_verification_failed' | 'outbound_guardian_voice_verification_started' | 'outbound_guardian_voice_verification_succeeded' | 'outbound_guardian_voice_verification_failed' | 'guardian_consultation_timed_out' | 'guardian_unavailable_skipped' | 'guardian_consult_deferred' | 'guardian_consult_coalesced' | 'inbound_acl_denied' | 'invite_redemption_started' | 'invite_redemption_succeeded' | 'invite_redemption_failed';
2
+ export type CallEventType = 'call_started' | 'call_connected' | 'caller_spoke' | 'assistant_spoke' | 'user_question_asked' | 'user_answered' | 'user_instruction_relayed' | 'call_ended' | 'call_failed' | 'callee_verification_started' | 'callee_verification_succeeded' | 'callee_verification_failed' | 'guardian_voice_verification_started' | 'guardian_voice_verification_succeeded' | 'guardian_voice_verification_failed' | 'outbound_guardian_voice_verification_started' | 'outbound_guardian_voice_verification_succeeded' | 'outbound_guardian_voice_verification_failed' | 'guardian_consultation_timed_out' | 'guardian_unavailable_skipped' | 'guardian_consult_deferred' | 'guardian_consult_coalesced' | 'inbound_acl_denied' | 'inbound_acl_name_capture_started' | 'inbound_acl_name_captured' | 'inbound_acl_name_capture_timeout' | 'inbound_acl_access_approved' | 'inbound_acl_access_denied' | 'inbound_acl_access_timeout' | 'invite_redemption_started' | 'invite_redemption_succeeded' | 'invite_redemption_failed';
3
3
  export type PendingQuestionStatus = 'pending' | 'answered' | 'expired' | 'cancelled';
4
4
 
5
5
  /**
@@ -19,6 +19,7 @@ import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.
19
19
  import { resolveChannelCapabilities } from '../daemon/session-runtime-assembly.js';
20
20
  import { buildAssistantEvent } from '../runtime/assistant-event.js';
21
21
  import { assistantEventHub } from '../runtime/assistant-event-hub.js';
22
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
22
23
  import { checkIngressForSecrets } from '../security/secret-ingress.js';
23
24
  import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
24
25
  import { IngressBlockedError } from '../util/errors.js';
@@ -306,7 +307,7 @@ export async function startVoiceTurn(opts: VoiceTurnOptions): Promise<VoiceTurnH
306
307
  ...session.memoryPolicy,
307
308
  strictSideEffects,
308
309
  };
309
- session.setAssistantId(opts.assistantId ?? 'self');
310
+ session.setAssistantId(opts.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID);
310
311
  session.callSessionId = opts.callSessionId;
311
312
  session.setGuardianContext(opts.guardianContext ?? null);
312
313
  session.setCommandIntent(null);
@@ -330,7 +331,7 @@ export async function startVoiceTurn(opts: VoiceTurnOptions): Promise<VoiceTurnH
330
331
  ? (msg as { sessionId: string }).sessionId
331
332
  : undefined;
332
333
  const resolvedSessionId = msgSessionId ?? opts.conversationId;
333
- const event = buildAssistantEvent('self', msg, resolvedSessionId);
334
+ const event = buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, msg, resolvedSessionId);
334
335
  hubChain = (async () => {
335
336
  await hubChain;
336
337
  try {
@@ -364,7 +365,7 @@ export async function startVoiceTurn(opts: VoiceTurnOptions): Promise<VoiceTurnH
364
365
  toolName: msg.toolName,
365
366
  inputDigest,
366
367
  consumingRequestId: msg.requestId,
367
- assistantId: opts.assistantId ?? 'self',
368
+ assistantId: opts.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
368
369
  executionChannel: 'voice',
369
370
  conversationId: opts.conversationId,
370
371
  callSessionId: opts.callSessionId,
@@ -107,8 +107,9 @@ export function registerDaemonCommand(program: Command): void {
107
107
  export function registerDevCommand(program: Command): void {
108
108
  program
109
109
  .command('dev')
110
- .description('Run the daemon in dev mode with auto-restart on file changes')
111
- .action(async () => {
110
+ .description('Run the daemon in dev mode')
111
+ .option('--watch', 'Auto-restart on source file changes (disruptive during Claude Code sessions)')
112
+ .action(async (opts: { watch?: boolean }) => {
112
113
  let status = await getDaemonStatus();
113
114
  if (status.running) {
114
115
  log.info('Stopping existing daemon...');
@@ -161,10 +162,12 @@ export function registerDevCommand(program: Command): void {
161
162
 
162
163
  const mainPath = `${import.meta.dirname}/../daemon/main.ts`;
163
164
 
164
- log.info('Starting daemon in dev mode (Ctrl+C to stop)');
165
+ const useWatch = opts.watch === true;
166
+ log.info(`Starting daemon in dev mode${useWatch ? ' with file watching' : ''} (Ctrl+C to stop)`);
165
167
 
166
168
  const repoRoot = join(import.meta.dirname, '..', '..', '..');
167
- const child = spawn('bun', ['--watch', 'run', mainPath], {
169
+ const args = useWatch ? ['--watch', 'run', mainPath] : ['run', mainPath];
170
+ const child = spawn('bun', args, {
168
171
  stdio: 'inherit',
169
172
  env: {
170
173
  ...process.env,
@@ -673,6 +673,28 @@ Wrap in `.v-metric-grid` for responsive 2-4 column layout. Always use a semantic
673
673
 
674
674
  `.v-pullquote` — Blockquote with gradient accent border. `.v-comparison` — Before/after cards (3-column grid with `.before`/`.after` modifiers). `.v-page` — Centered container (max-width 600px). Use `.v-animate-in` on children for staggered fade-in. Use `.v-gradient-text` for accent-colored gradient text.
675
675
 
676
+ `.v-slideshow` — Presentation slide deck with transitions and navigation. Init with `vellum.widgets.slideshow()`:
677
+ ```html
678
+ <div class="v-slideshow" id="deck">
679
+ <div class="v-slide">
680
+ <div class="v-slide-header">
681
+ <span class="v-slide-label">Overview</span>
682
+ </div>
683
+ <h1 class="v-slide-title">The city that never <span class="accent-word">sleeps</span></h1>
684
+ <p class="v-slide-body">Body text here...</p>
685
+ <div class="v-slide-stats">
686
+ <div class="v-slide-stat">
687
+ <span class="v-slide-stat-value">8.3M</span>
688
+ <span class="v-slide-stat-label">Residents</span>
689
+ </div>
690
+ </div>
691
+ </div>
692
+ <div class="v-slide"><!-- Slide 2 --></div>
693
+ <div class="v-slide"><!-- Slide 3 --></div>
694
+ </div>
695
+ ```
696
+ Slide content helpers: `.v-slide-label` (section label with colored dot), `.v-slide-title` (responsive heading), `.v-slide-body` (body text, max-width 540px), `.v-slide-stats` (auto-fit grid), `.v-slide-stat` / `.v-slide-stat-value` / `.v-slide-stat-label` (big-number cards), `.v-slide-quote` / `.v-slide-quote-attribution` (blockquote), `.v-slide-list` (styled list), `.v-slide-columns` / `.v-slide-column` (2-column comparison grid).
697
+
676
698
  #### Widget JavaScript utilities
677
699
 
678
700
  Interactive utilities at `window.vellum.widgets.*`:
@@ -724,6 +746,13 @@ vellum.widgets.toast('Connection lost', 'error', 0); // Manual dismiss
724
746
  vellum.widgets.countdown('timer-el', '2025-12-31T00:00:00Z', {
725
747
  onComplete: () => console.log('Done!')
726
748
  });
749
+
750
+ // Slideshow — presentation deck with transitions and navigation
751
+ vellum.widgets.slideshow('deck', {
752
+ transition: 'fade', showDots: true, showArrows: true,
753
+ showCounter: true, keyboard: true, loop: true
754
+ });
755
+ // Returns: { goTo(index), next(), prev() }
727
756
  ```
728
757
 
729
758
  #### Composition recipes
@@ -992,9 +1021,142 @@ async function handleBulk(action) {
992
1021
  }
993
1022
  ```
994
1023
 
1024
+ **Presentation slideshow** — multi-slide deck with 8 layout variants (title, stats, bullets, quote, comparison, visual, timeline, closing). Use the slideshow widget for presentations, pitch decks, and multi-slide educational content. The model provides slide content; the widget handles navigation, transitions, and keyboard support.
1025
+
1026
+ ```html
1027
+ <!DOCTYPE html>
1028
+ <html lang="en">
1029
+ <head>
1030
+ <meta charset="UTF-8">
1031
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1032
+ <style>
1033
+ :root { --v-accent: #8B5CF6; }
1034
+ body { margin: 0; padding: 0; background: linear-gradient(-45deg, #0f172a, #1e1b4b, #172554); min-height: 100vh; }
1035
+ .v-slideshow { border-radius: 0; min-height: 100vh; }
1036
+ .accent-word { color: var(--v-accent); }
1037
+ .trust-pill { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 999px; font-size: 13px; font-weight: 500; background: color-mix(in srgb, var(--v-surface) 60%, transparent); border: 1px solid var(--v-surface-border); color: var(--v-text-secondary); margin-top: var(--v-spacing-lg); }
1038
+ .trust-pill.accent { border-color: color-mix(in srgb, var(--v-accent) 30%, transparent); color: var(--v-accent); }
1039
+ .v-slide-list li { font-size: 15px; line-height: 1.7; }
1040
+ .v-slide-columns h3 { margin: 0 0 var(--v-spacing-sm); color: var(--v-text); font-size: var(--v-font-size-lg); }
1041
+ .slide-visual { position: relative; overflow: hidden; }
1042
+ .slide-visual-bg { position: absolute; inset: 0; background: radial-gradient(ellipse at 30% 40%, color-mix(in srgb, var(--v-accent) 15%, transparent), transparent 70%), radial-gradient(ellipse at 70% 60%, color-mix(in srgb, var(--v-forest-500, #22c55e) 10%, transparent), transparent 60%); }
1043
+ .slide-visual-overlay { position: relative; z-index: 1; background: color-mix(in srgb, var(--v-bg) 40%, transparent); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-radius: var(--v-radius-lg); padding: var(--v-spacing-xl); max-width: 500px; }
1044
+ </style>
1045
+ </head>
1046
+ <body>
1047
+ <div class="v-slideshow" id="deck">
1048
+
1049
+ <!-- 1. Title slide -->
1050
+ <div class="v-slide">
1051
+ <span class="v-slide-label">Introduction</span>
1052
+ <h1 class="v-slide-title">The city that never <span class="accent-word">sleeps</span></h1>
1053
+ <p class="v-slide-body">A brief subtitle or tagline here.</p>
1054
+ <span class="trust-pill accent">&#x1f5fd; 8.3 million residents</span>
1055
+ </div>
1056
+
1057
+ <!-- 2. Content + Stats slide -->
1058
+ <div class="v-slide">
1059
+ <span class="v-slide-label">By the Numbers</span>
1060
+ <h2 class="v-slide-title">Economy at a glance</h2>
1061
+ <p class="v-slide-body">New York generates more GDP than most countries...</p>
1062
+ <div class="v-slide-stats">
1063
+ <div class="v-slide-stat"><span class="v-slide-stat-value">$2.1T</span><span class="v-slide-stat-label">Metro GDP</span></div>
1064
+ <div class="v-slide-stat"><span class="v-slide-stat-value">4.7M</span><span class="v-slide-stat-label">Jobs</span></div>
1065
+ <div class="v-slide-stat"><span class="v-slide-stat-value">#1</span><span class="v-slide-stat-label">Financial Hub</span></div>
1066
+ </div>
1067
+ </div>
1068
+
1069
+ <!-- 3. Bullet points slide -->
1070
+ <div class="v-slide">
1071
+ <span class="v-slide-label">Culture</span>
1072
+ <h2 class="v-slide-title">What makes it <span class="accent-word">unique</span></h2>
1073
+ <ul class="v-slide-list">
1074
+ <li>&#x1f3ad; Broadway — 41 professional theaters in the Theater District</li>
1075
+ <li>&#x1f3db;&#xfe0f; 80+ world-class museums including the Met and MoMA</li>
1076
+ <li>&#x1f355; Over 27,000 restaurants spanning every cuisine on earth</li>
1077
+ <li>&#x1f333; 843 acres of Central Park in the heart of Manhattan</li>
1078
+ </ul>
1079
+ </div>
1080
+
1081
+ <!-- 4. Quote slide -->
1082
+ <div class="v-slide" style="justify-content: center; align-items: center; text-align: center;">
1083
+ <div class="v-slide-quote" style="border-left: none; padding-left: 0;">
1084
+ "There is no place like New York. It is the most exciting city in the world."
1085
+ </div>
1086
+ <div class="v-slide-quote-attribution">— John Updike</div>
1087
+ </div>
1088
+
1089
+ <!-- 5. Comparison / Two-column slide -->
1090
+ <div class="v-slide">
1091
+ <span class="v-slide-label">Comparison</span>
1092
+ <h2 class="v-slide-title">Manhattan vs Brooklyn</h2>
1093
+ <div class="v-slide-columns">
1094
+ <div class="v-slide-column">
1095
+ <h3>&#x1f3d9;&#xfe0f; Manhattan</h3>
1096
+ <p class="v-slide-body" style="margin-bottom: var(--v-spacing-sm);">Financial center, dense skyscrapers, high-energy nightlife, world-famous landmarks.</p>
1097
+ <span class="v-slide-stat-value">1.6M</span>
1098
+ <span class="v-slide-stat-label">Population</span>
1099
+ </div>
1100
+ <div class="v-slide-column">
1101
+ <h3>&#x1f309; Brooklyn</h3>
1102
+ <p class="v-slide-body" style="margin-bottom: var(--v-spacing-sm);">Creative hub, brownstone neighborhoods, artisan food scene, waterfront parks.</p>
1103
+ <span class="v-slide-stat-value">2.7M</span>
1104
+ <span class="v-slide-stat-label">Population</span>
1105
+ </div>
1106
+ </div>
1107
+ </div>
1108
+
1109
+ <!-- 6. Image/visual slide -->
1110
+ <div class="v-slide slide-visual">
1111
+ <div class="slide-visual-bg"></div>
1112
+ <div class="slide-visual-overlay">
1113
+ <span class="v-slide-label">Skyline</span>
1114
+ <h2 class="v-slide-title">An iconic <span class="accent-word">horizon</span></h2>
1115
+ <p class="v-slide-body">The Manhattan skyline is recognized worldwide...</p>
1116
+ </div>
1117
+ </div>
1118
+
1119
+ <!-- 7. Timeline slide -->
1120
+ <div class="v-slide">
1121
+ <span class="v-slide-label">History</span>
1122
+ <h2 class="v-slide-title">Key <span class="accent-word">milestones</span></h2>
1123
+ <div class="v-timeline" style="margin-top: var(--v-spacing-lg);">
1124
+ <div class="v-timeline-entry"><span class="v-timeline-time">1626</span><span class="v-timeline-title">Manhattan purchased</span></div>
1125
+ <div class="v-timeline-entry"><span class="v-timeline-time">1886</span><span class="v-timeline-title">Statue of Liberty dedicated</span></div>
1126
+ <div class="v-timeline-entry active"><span class="v-timeline-time">1931</span><span class="v-timeline-title">Empire State Building opens</span></div>
1127
+ </div>
1128
+ </div>
1129
+
1130
+ <!-- 8. Closing / CTA slide -->
1131
+ <div class="v-slide" style="text-align: center; align-items: center;">
1132
+ <h1 class="v-slide-title">The world's <span class="accent-word">capital</span></h1>
1133
+ <p class="v-slide-body" style="max-width: 400px;">New York isn't just a city — it's an idea that never stops evolving.</p>
1134
+ <div class="v-slide-stats" style="margin-top: var(--v-spacing-xxl);">
1135
+ <div class="v-slide-stat"><span class="v-slide-stat-value">800+</span><span class="v-slide-stat-label">Languages spoken</span></div>
1136
+ <div class="v-slide-stat"><span class="v-slide-stat-value">62M</span><span class="v-slide-stat-label">Annual visitors</span></div>
1137
+ </div>
1138
+ </div>
1139
+
1140
+ </div>
1141
+ <script>
1142
+ document.addEventListener('DOMContentLoaded', function() {
1143
+ vellum.widgets.slideshow('deck', {
1144
+ transition: 'fade',
1145
+ showDots: true,
1146
+ showArrows: true,
1147
+ showCounter: true,
1148
+ keyboard: true,
1149
+ loop: true
1150
+ });
1151
+ });
1152
+ </script>
1153
+ </body>
1154
+ </html>
1155
+ ```
1156
+
995
1157
  #### When to use widgets vs custom HTML
996
1158
 
997
- - **Use widgets** for standard patterns — tables, metrics, timelines, notifications
1159
+ - **Use widgets** for standard patterns — tables, metrics, timelines, notifications, presentations
998
1160
  - **Use custom HTML** for novel or creative UIs — games, art tools, unique dashboards
999
1161
  - **Mix freely** — widgets compose well together and with custom elements
1000
1162
  - Always prioritize the ideal user experience over using the widget library
@@ -1326,6 +1488,7 @@ Before delivering any app, mentally verify these 10 items — they cover the gap
1326
1488
  | `.v-pill-toggles` | Time range / filter toggle group | `.v-pill-toggle` (`.active`) — container with pill buttons |
1327
1489
  | `.v-chip-group` | Suggestion / filter chip row | `.v-chip` (`.active`) — wrapping row of clickable pills |
1328
1490
  | `.v-metric-card .v-metric-icon` | Emoji icon in metric cards | Place emoji `<span>` with `.v-metric-icon` inside `.v-metric-card` |
1491
+ | `.v-slideshow` | Presentation slide deck with transitions | `.v-slide` (`.active`), `.v-slide-label`, `.v-slide-title`, `.v-slide-body`, `.v-slide-stats`, `.v-slide-stat`, `.v-slide-quote` — init with `vellum.widgets.slideshow()` |
1329
1492
 
1330
1493
  Every app should include: search/filter, toast notifications for all CRUD operations, `window.vellum.confirm()` for destructive actions, staggered page-load animation, card hover effects, and skeleton loading states.
1331
1494
 
@@ -0,0 +1,214 @@
1
+ ---
2
+ name: "Vercel Token Setup"
3
+ description: "Set up a Vercel API token for publishing apps using browser automation"
4
+ includes: ["browser"]
5
+ metadata: {"vellum": {"emoji": "▲"}}
6
+ ---
7
+
8
+ You are helping your user set up a Vercel API token so they can publish apps to the web.
9
+
10
+ ## Client Check
11
+
12
+ Determine whether the user has browser automation available (macOS desktop app) or is on a non-interactive channel (Telegram, SMS, etc.).
13
+
14
+ - **macOS desktop app**: Follow the **Automated Setup** path below.
15
+ - **Telegram or other channel** (no browser automation): Follow the **Manual Setup for Channels** path below.
16
+
17
+ ---
18
+
19
+ # Path A: Manual Setup for Channels (Telegram, SMS, etc.)
20
+
21
+ When the user is on Telegram or any non-macOS client, walk them through a text-based setup. No browser automation is used — the user follows links and performs each action manually.
22
+
23
+ ### Channel Step 1: Confirm and Explain
24
+
25
+ Tell the user:
26
+
27
+ > **Setting up Vercel API Token**
28
+ >
29
+ > Since I can't automate the browser from here, I'll walk you through each step with direct links. You'll need:
30
+ > 1. A Vercel account (free tier works)
31
+ > 2. About 2 minutes
32
+ >
33
+ > Ready to start?
34
+
35
+ If the user declines, acknowledge and stop.
36
+
37
+ ### Channel Step 2: Create the Token
38
+
39
+ Tell the user:
40
+
41
+ > **Step 1: Create an API token**
42
+ >
43
+ > Open this link to go to your Vercel tokens page:
44
+ > https://vercel.com/account/tokens
45
+ >
46
+ > 1. Click **"Create"** (or **"Create Token"**)
47
+ > 2. Set the token name to **"Vellum Assistant"**
48
+ > 3. Select scope: **"Full Account"**
49
+ > 4. Set expiration to the longest option available (or **"No Expiration"** if offered)
50
+ > 5. Click **"Create Token"**
51
+ >
52
+ > A token value will appear — **copy it now**, as it's only shown once.
53
+
54
+ ### Channel Step 3: Store the Token
55
+
56
+ Tell the user:
57
+
58
+ > **Step 2: Send me the token**
59
+ >
60
+ > Please paste the token value into the secure prompt below.
61
+
62
+ Present the secure prompt:
63
+
64
+ ```
65
+ credential_store prompt:
66
+ service: "vercel"
67
+ field: "api_token"
68
+ label: "Vercel API Token"
69
+ description: "Paste the API token you just created on vercel.com"
70
+ placeholder: "Enter your Vercel API token"
71
+ ```
72
+
73
+ Wait for the user to complete the prompt. Once received, store it:
74
+
75
+ ```
76
+ credential_store store:
77
+ service: "vercel"
78
+ field: "api_token"
79
+ value: "<the token the user provided>"
80
+ allowedTools: ["publish_page", "unpublish_page"]
81
+ allowedDomains: ["api.vercel.com"]
82
+ ```
83
+
84
+ ### Channel Step 4: Done!
85
+
86
+ > **Vercel is connected!** You can now publish apps to the web. Try clicking Publish on any app you've built.
87
+
88
+ ---
89
+
90
+ # Path B: Automated Setup (macOS Desktop App)
91
+
92
+ You will automate Vercel token creation via the browser while the user watches. The user's only manual action is signing in to Vercel (if needed) and one copy-paste for the token value.
93
+
94
+ ## Browser Interaction Principles
95
+
96
+ Vercel's UI may change over time. Do NOT memorize or depend on specific element IDs, CSS selectors, or DOM structures. Instead:
97
+
98
+ 1. **Screenshot first, act second.** Before every interaction, take a `browser_screenshot` to see the current visual state. Use `browser_snapshot` to find interactive elements.
99
+ 2. **Adapt to what you see.** If a button's label or position differs from what you expect, use the screenshot to find the correct element.
100
+ 3. **Verify after every action.** After clicking, typing, or navigating, take a new screenshot to confirm the action succeeded.
101
+ 4. **Never assume DOM structure.** Use the snapshot to identify what's on the page and interact accordingly.
102
+ 5. **When stuck, screenshot and describe.** If you cannot find an expected element after 2 attempts, take a screenshot, describe what you see to the user, and ask for guidance.
103
+
104
+ ## Anti-Loop Guardrails
105
+
106
+ Each step has a **retry budget of 3 attempts**. An attempt is one try at the step's primary action (e.g., clicking a button, filling a form). If a step fails after 3 attempts:
107
+
108
+ 1. **Stop trying.** Do not continue retrying the same approach.
109
+ 2. **Fall back to manual.** Tell the user what you were trying to do and ask them to complete that step manually in the browser. Give them the direct URL and clear text instructions.
110
+ 3. **Resume automation** at the next step once the user confirms the manual step is done.
111
+
112
+ If **two or more steps** require manual fallback, abandon the automated flow entirely and switch to giving the user the remaining steps as clear text instructions with links.
113
+
114
+ ## Things That Do Not Work — Do Not Attempt
115
+
116
+ These actions are technically impossible in the browser automation environment:
117
+
118
+ - **Downloading files.** `browser_click` on a Download button does not save files to disk.
119
+ - **Reading the token value from a screenshot.** The token IS visible in the creation dialog, but you MUST NOT attempt to read it from a screenshot — it is too easy to misread characters, and the value must be exact. Always use the `credential_store prompt` approach to let the user copy-paste it accurately.
120
+ - **Clipboard operations.** You cannot copy/paste via browser automation.
121
+
122
+ ## Step 1: Single Upfront Confirmation
123
+
124
+ Tell the user:
125
+
126
+ > **Setting up your Vercel API token so we can publish your app...**
127
+ >
128
+ > Here's what will happen:
129
+ > 1. **A browser opens** to your Vercel account settings
130
+ > 2. **You sign in** (if not already signed in)
131
+ > 3. **I create the token** — you just watch
132
+ > 4. **One quick copy-paste** — I'll ask you to copy the token value into a secure prompt
133
+ >
134
+ > Takes about a minute. Ready?
135
+
136
+ If the user declines, acknowledge and stop. No further confirmations are needed after this point.
137
+
138
+ ## Step 2: Open Vercel and Sign In
139
+
140
+ **Goal:** The user is signed in and the Vercel tokens page is loaded.
141
+
142
+ Navigate to `https://vercel.com/account/tokens`.
143
+
144
+ Take a screenshot and snapshot to check the page state:
145
+ - **Sign-in page:** Tell the user: "Please sign in to your Vercel account in the browser." Then auto-detect sign-in completion by polling screenshots every 5-10 seconds. Check if the current URL has moved away from the login/sign-in page to the tokens page. Do NOT ask the user to "let me know when you're done" — detect it automatically. Once sign-in is detected, tell the user: "Signed in! Creating your API token now..."
146
+ - **Already signed in:** Tell the user: "Already signed in — creating your API token now..." and continue immediately.
147
+
148
+ **Verify:** URL contains `vercel.com/account/tokens` and no sign-in overlay is visible.
149
+
150
+ ## Step 3: Create Token
151
+
152
+ **Goal:** A new API token named "Vellum Assistant" is created.
153
+
154
+ Take a screenshot and snapshot. Find and click the button to create a new token (typically labeled "Create" or "Create Token").
155
+
156
+ On the creation form:
157
+ - Token name: **"Vellum Assistant"**
158
+ - Scope: Select **"Full Account"** (or the broadest scope available)
159
+ - Expiration: Select the longest option available, or **"No Expiration"** if offered
160
+ - Click create/submit
161
+
162
+ **Verify:** Take a screenshot. A dialog or section should now display the newly created token value.
163
+
164
+ ## Step 4: Capture Token via Secure Prompt
165
+
166
+ **Goal:** The token value is securely captured and stored.
167
+
168
+ ### CRITICAL — Token Capture Protocol
169
+
170
+ After token creation, Vercel shows the token value **once**. You MUST follow this exact sequence — **no improvisation**:
171
+
172
+ 1. Tell the user: "Your token has been created! Please copy the token value shown on screen and paste it into the secure prompt below."
173
+ 2. **IMMEDIATELY** present a `credential_store prompt` for the token. This is your ONLY next action.
174
+ 3. Wait for the user to paste the token.
175
+
176
+ **Absolute prohibitions during this step:**
177
+ - Do NOT try to read the token value from the screenshot. It must come from the user via secure prompt to ensure accuracy.
178
+ - Do NOT navigate away from the page until the user has pasted the token.
179
+ - Do NOT click any download or copy buttons.
180
+
181
+ Present the secure prompt:
182
+
183
+ ```
184
+ credential_store prompt:
185
+ service: "vercel"
186
+ field: "api_token"
187
+ label: "Vercel API Token"
188
+ description: "Copy the token value shown on the Vercel page and paste it here."
189
+ placeholder: "Enter your Vercel API token"
190
+ ```
191
+
192
+ Wait for the user to complete the prompt. Once received, store it:
193
+
194
+ ```
195
+ credential_store store:
196
+ service: "vercel"
197
+ field: "api_token"
198
+ value: "<the token the user provided>"
199
+ allowedTools: ["publish_page", "unpublish_page"]
200
+ allowedDomains: ["api.vercel.com"]
201
+ ```
202
+
203
+ **Verify:** `credential_store list` shows `api_token` for `vercel`.
204
+
205
+ ## Step 5: Done!
206
+
207
+ "**Vercel is connected!** Your API token is set up and ready to go. You can now publish apps to the web."
208
+
209
+ ## Error Handling
210
+
211
+ - **Page load failures:** Retry navigation once. If it still fails, tell the user and ask them to check their internet connection.
212
+ - **Element not found:** Take a fresh screenshot to re-assess. The Vercel UI may have changed. Describe what you see and try alternative approaches. If stuck after 2 attempts, ask the user for guidance.
213
+ - **Token already exists with same name:** This is fine — Vercel allows multiple tokens with the same name. Proceed with creation.
214
+ - **Any unexpected state:** Take a `browser_screenshot`, describe what you see, and ask the user for guidance.
@@ -125,6 +125,18 @@ export const CallsConfigSchema = z.object({
125
125
  .positive('calls.userConsultTimeoutSeconds must be a positive integer')
126
126
  .max(2_147_483, 'calls.userConsultTimeoutSeconds must be at most 2147483 (setTimeout-safe limit)')
127
127
  .default(120),
128
+ ttsPlaybackDelayMs: z
129
+ .number({ error: 'calls.ttsPlaybackDelayMs must be a number' })
130
+ .int('calls.ttsPlaybackDelayMs must be an integer')
131
+ .min(0, 'calls.ttsPlaybackDelayMs must be >= 0')
132
+ .max(10_000, 'calls.ttsPlaybackDelayMs must be at most 10000')
133
+ .default(3000),
134
+ accessRequestPollIntervalMs: z
135
+ .number({ error: 'calls.accessRequestPollIntervalMs must be a number' })
136
+ .int('calls.accessRequestPollIntervalMs must be an integer')
137
+ .min(50, 'calls.accessRequestPollIntervalMs must be >= 50')
138
+ .max(10_000, 'calls.accessRequestPollIntervalMs must be at most 10000')
139
+ .default(500),
128
140
  disclosure: CallsDisclosureConfigSchema.default(CallsDisclosureConfigSchema.parse({})),
129
141
  safety: CallsSafetyConfigSchema.default(CallsSafetyConfigSchema.parse({})),
130
142
  voice: CallsVoiceConfigSchema.default(CallsVoiceConfigSchema.parse({})),
@@ -49,14 +49,6 @@
49
49
  "description": "Send crash reports and error diagnostics to help improve the app",
50
50
  "defaultEnabled": true
51
51
  },
52
- {
53
- "id": "voice-invite-redemption",
54
- "scope": "assistant",
55
- "key": "feature_flags.voice-invite-redemption.enabled",
56
- "label": "Voice Invite Redemption",
57
- "description": "Enable voice invite code redemption for inbound callers with active voice invites",
58
- "defaultEnabled": false
59
- },
60
52
  {
61
53
  "id": "user-hosted-enabled",
62
54
  "scope": "macos",
@@ -254,6 +254,8 @@ INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites" \
254
254
  -d '{
255
255
  "sourceChannel": "voice",
256
256
  "expectedExternalUserId": "<phone_number_E164>",
257
+ "friendName": "<invitee_first_name>",
258
+ "guardianName": "<guardian_first_name>",
257
259
  "maxUses": 1,
258
260
  "note": "<optional note, e.g. the person it is for>"
259
261
  }')
@@ -263,6 +265,8 @@ printf '%s\n' "$INVITE_JSON"
263
265
  Required fields:
264
266
  - `sourceChannel` — must be `"voice"`
265
267
  - `expectedExternalUserId` — the invitee's phone number in E.164 format (e.g., `+15551234567`)
268
+ - `friendName` — the invitee's first name (used in the voice greeting)
269
+ - `guardianName` — the guardian's first name (used in the voice greeting)
266
270
 
267
271
  Optional fields:
268
272
  - `maxUses` — how many times the code can be used (default: 1)
@@ -348,6 +352,8 @@ Replace `<invite_id>` with the invite's `id` from the list response. The same re
348
352
  - `sourceChannel is required for create` — when creating an invite, always pass `"sourceChannel": "telegram"` for Telegram or `"sourceChannel": "voice"` for voice invites.
349
353
  - `expectedExternalUserId is required for voice invites` — voice invites must include the invitee's phone number.
350
354
  - `expectedExternalUserId must be in E.164 format` — the phone number must start with `+` followed by country code and number (e.g., `+15551234567`).
355
+ - `friendName is required for voice invites` — ask for the invitee's first name.
356
+ - `guardianName is required for voice invites` — ask for the guardian's (your user's) first name.
351
357
  - `Invite not found or already revoked` — the invite ID may be invalid or the invite is already revoked.
352
358
 
353
359
  ## Typical Workflows
@@ -368,9 +374,9 @@ Replace `<invite_id>` with the invite's `id` from the list response. The same re
368
374
 
369
375
  **"Revoke invite"** / **"Cancel invite link"** — List invites to identify the target, confirm, then revoke by ID.
370
376
 
371
- **"Create a voice invite for +15551234567"** — Create a voice invite with `sourceChannel: "voice"` and the given phone number as `expectedExternalUserId`. Present the invite code and instructions: the person must call from that number and enter the code.
377
+ **"Create a voice invite for +15551234567"** — Ask for the friend's first name and the guardian's first name (if not already known), then create a voice invite with `sourceChannel: "voice"`, the phone number as `expectedExternalUserId`, and both names. Present the invite code and instructions: the person must call from that number and enter the code.
372
378
 
373
- **"Let my mom call in"** / **"Invite someone by phone"** — Ask for the phone number in E.164 format, create a voice invite, and present the code + calling instructions.
379
+ **"Let my mom call in"** / **"Invite someone by phone"** — Ask for the phone number in E.164 format, the friend's first name, and the guardian's first name. Create a voice invite and present the code + calling instructions.
374
380
 
375
381
  **"Show my voice invites"** / **"List phone invites"** — List invites filtered by `sourceChannel=voice`, present active invites with bound phone number and expiration info.
376
382
 
@@ -3,6 +3,7 @@ import * as net from 'node:net';
3
3
  import type { ChannelId } from '../../channels/types.js';
4
4
  import * as externalConversationStore from '../../memory/external-conversation-store.js';
5
5
  import { findMember, revokeMember } from '../../memory/ingress-member-store.js';
6
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../../runtime/assistant-scope.js';
6
7
  import {
7
8
  createVerificationChallenge,
8
9
  findActiveSession,
@@ -17,7 +18,6 @@ import {
17
18
  resendOutbound,
18
19
  startOutbound,
19
20
  } from '../../runtime/guardian-outbound-actions.js';
20
- import { normalizeAssistantId } from '../../util/platform.js';
21
21
  import type {
22
22
  ChannelReadinessRequest,
23
23
  GuardianVerificationRequest,
@@ -64,7 +64,7 @@ export function createGuardianChallenge(
64
64
  rebind?: boolean,
65
65
  sessionId?: string,
66
66
  ): GuardianVerificationResult {
67
- const resolvedAssistantId = normalizeAssistantId(assistantId ?? 'self');
67
+ const resolvedAssistantId = DAEMON_INTERNAL_ASSISTANT_ID;
68
68
  const resolvedChannel = channel ?? 'telegram';
69
69
 
70
70
  const existingBinding = getGuardianBinding(resolvedAssistantId, resolvedChannel);
@@ -89,9 +89,9 @@ export function createGuardianChallenge(
89
89
 
90
90
  export function getGuardianStatus(
91
91
  channel?: ChannelId,
92
- assistantId?: string,
92
+ _assistantId?: string,
93
93
  ): GuardianVerificationResult {
94
- const resolvedAssistantId = normalizeAssistantId(assistantId ?? 'self');
94
+ const resolvedAssistantId = DAEMON_INTERNAL_ASSISTANT_ID;
95
95
  const resolvedChannel = channel ?? 'telegram';
96
96
 
97
97
  const binding = getGuardianBinding(resolvedAssistantId, resolvedChannel);
@@ -161,9 +161,7 @@ export function handleGuardianVerification(
161
161
  socket: net.Socket,
162
162
  ctx: HandlerContext,
163
163
  ): void {
164
- // Normalize the assistant ID so challenges are always stored under the
165
- // same key the inbound-call path will use for lookups (typically "self").
166
- const assistantId = normalizeAssistantId(msg.assistantId ?? 'self');
164
+ const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
167
165
  const channel = msg.channel ?? 'telegram';
168
166
 
169
167
  try {
@@ -40,6 +40,8 @@ export function handleIngressInvite(
40
40
  note: msg.note,
41
41
  maxUses: msg.maxUses,
42
42
  expiresInMs: msg.expiresInMs,
43
+ friendName: msg.friendName,
44
+ guardianName: msg.guardianName,
43
45
  });
44
46
  if (!result.ok) {
45
47
  ctx.send(socket, { type: 'ingress_invite_response', success: false, error: result.error });