@vellumai/assistant 0.3.4 → 0.3.5

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 (122) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +37 -2
  3. package/package.json +1 -1
  4. package/scripts/ipc/generate-swift.ts +13 -0
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
  6. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  7. package/src/__tests__/approval-message-composer.test.ts +253 -0
  8. package/src/__tests__/call-domain.test.ts +12 -2
  9. package/src/__tests__/call-orchestrator.test.ts +70 -1
  10. package/src/__tests__/call-routes-http.test.ts +27 -2
  11. package/src/__tests__/channel-approval-routes.test.ts +21 -17
  12. package/src/__tests__/channel-approvals.test.ts +48 -1
  13. package/src/__tests__/channel-guardian.test.ts +74 -22
  14. package/src/__tests__/channel-readiness-service.test.ts +257 -0
  15. package/src/__tests__/config-schema.test.ts +2 -1
  16. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  17. package/src/__tests__/daemon-lifecycle.test.ts +13 -12
  18. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  19. package/src/__tests__/entity-search.test.ts +615 -0
  20. package/src/__tests__/handlers-twilio-config.test.ts +407 -0
  21. package/src/__tests__/ipc-snapshot.test.ts +63 -0
  22. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  23. package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
  24. package/src/__tests__/run-orchestrator.test.ts +22 -0
  25. package/src/__tests__/session-runtime-assembly.test.ts +85 -1
  26. package/src/__tests__/sms-messaging-provider.test.ts +125 -0
  27. package/src/__tests__/twilio-routes.test.ts +39 -3
  28. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  29. package/src/__tests__/web-search.test.ts +1 -1
  30. package/src/__tests__/work-item-output.test.ts +110 -0
  31. package/src/calls/call-domain.ts +8 -5
  32. package/src/calls/call-orchestrator.ts +22 -11
  33. package/src/calls/twilio-config.ts +17 -11
  34. package/src/calls/twilio-rest.ts +276 -0
  35. package/src/calls/twilio-routes.ts +39 -1
  36. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  37. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  38. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  39. package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
  40. package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
  41. package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
  42. package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
  43. package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
  44. package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
  45. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
  46. package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
  47. package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
  48. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
  49. package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
  50. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
  51. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  52. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  53. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
  54. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  55. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
  56. package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
  57. package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
  58. package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
  59. package/src/config/bundled-skills/messaging/SKILL.md +21 -6
  60. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  61. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  62. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  63. package/src/config/defaults.ts +2 -1
  64. package/src/config/schema.ts +9 -3
  65. package/src/config/system-prompt.ts +24 -0
  66. package/src/config/templates/IDENTITY.md +2 -2
  67. package/src/config/vellum-skills/catalog.json +6 -0
  68. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
  69. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
  70. package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
  71. package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
  72. package/src/daemon/handlers/config.ts +783 -9
  73. package/src/daemon/handlers/dictation.ts +182 -0
  74. package/src/daemon/handlers/identity.ts +14 -23
  75. package/src/daemon/handlers/index.ts +2 -0
  76. package/src/daemon/handlers/sessions.ts +2 -0
  77. package/src/daemon/handlers/shared.ts +3 -0
  78. package/src/daemon/handlers/work-items.ts +15 -7
  79. package/src/daemon/ipc-contract-inventory.json +10 -0
  80. package/src/daemon/ipc-contract.ts +108 -4
  81. package/src/daemon/lifecycle.ts +2 -0
  82. package/src/daemon/ride-shotgun-handler.ts +1 -1
  83. package/src/daemon/server.ts +6 -2
  84. package/src/daemon/session-agent-loop.ts +5 -1
  85. package/src/daemon/session-runtime-assembly.ts +55 -0
  86. package/src/daemon/session-tool-setup.ts +2 -0
  87. package/src/daemon/session.ts +11 -1
  88. package/src/inbound/public-ingress-urls.ts +3 -3
  89. package/src/memory/channel-guardian-store.ts +2 -1
  90. package/src/memory/db-init.ts +144 -0
  91. package/src/memory/job-handlers/media-processing.ts +100 -0
  92. package/src/memory/jobs-store.ts +2 -1
  93. package/src/memory/jobs-worker.ts +4 -0
  94. package/src/memory/media-store.ts +759 -0
  95. package/src/memory/retriever.ts +6 -1
  96. package/src/memory/schema.ts +98 -0
  97. package/src/memory/search/entity.ts +208 -25
  98. package/src/memory/search/ranking.ts +6 -1
  99. package/src/memory/search/types.ts +24 -0
  100. package/src/messaging/provider-types.ts +2 -0
  101. package/src/messaging/providers/sms/adapter.ts +204 -0
  102. package/src/messaging/providers/sms/client.ts +93 -0
  103. package/src/messaging/providers/sms/types.ts +7 -0
  104. package/src/permissions/checker.ts +16 -2
  105. package/src/runtime/approval-message-composer.ts +143 -0
  106. package/src/runtime/channel-approvals.ts +12 -4
  107. package/src/runtime/channel-guardian-service.ts +44 -18
  108. package/src/runtime/channel-readiness-service.ts +292 -0
  109. package/src/runtime/channel-readiness-types.ts +29 -0
  110. package/src/runtime/http-server.ts +53 -27
  111. package/src/runtime/http-types.ts +3 -0
  112. package/src/runtime/routes/call-routes.ts +2 -1
  113. package/src/runtime/routes/channel-routes.ts +67 -21
  114. package/src/runtime/run-orchestrator.ts +35 -2
  115. package/src/tools/assets/materialize.ts +2 -2
  116. package/src/tools/calls/call-start.ts +1 -0
  117. package/src/tools/credentials/vault.ts +1 -1
  118. package/src/tools/execution-target.ts +11 -1
  119. package/src/tools/network/web-search.ts +1 -1
  120. package/src/tools/types.ts +2 -0
  121. package/src/twitter/router.ts +1 -1
  122. package/src/util/platform.ts +35 -0
@@ -0,0 +1,182 @@
1
+ import * as net from 'node:net';
2
+ import { getConfig } from '../../config/loader.js';
3
+ import { getFailoverProvider, listProviders } from '../../providers/registry.js';
4
+ import type { DictationRequest } from '../ipc-protocol.js';
5
+ import { log, defineHandlers, type HandlerContext } from './shared.js';
6
+
7
+ // Action verbs that signal the user wants a full agent session rather than inline text
8
+ const ACTION_VERBS = ['slack', 'email', 'send', 'create', 'open', 'search', 'find'];
9
+
10
+ const MAX_WINDOW_TITLE_LENGTH = 100;
11
+
12
+ /** Sanitize window title to mitigate prompt injection from attacker-controlled titles (e.g. browser tabs, Slack conversations). */
13
+ function sanitizeWindowTitle(title: string | undefined): string {
14
+ if (!title) return '';
15
+ return title
16
+ .replace(/[<>]/g, '') // strip angle brackets to prevent tag injection
17
+ .slice(0, MAX_WINDOW_TITLE_LENGTH);
18
+ }
19
+
20
+ /** Build a delimited app metadata block so the LLM treats it as contextual data, not instructions. */
21
+ function buildAppMetadataBlock(msg: DictationRequest): string {
22
+ const windowTitle = sanitizeWindowTitle(msg.context.windowTitle);
23
+ return [
24
+ '<app_metadata>',
25
+ `App: ${msg.context.appName} (${msg.context.bundleIdentifier})`,
26
+ `Window: ${windowTitle}`,
27
+ '</app_metadata>',
28
+ ].join('\n');
29
+ }
30
+
31
+ type DictationMode = 'dictation' | 'command' | 'action';
32
+
33
+ export function detectDictationMode(msg: DictationRequest): DictationMode {
34
+ // Command mode: selected text present — treat transcription as a transformation instruction
35
+ if (msg.context.selectedText && msg.context.selectedText.trim().length > 0) {
36
+ return 'command';
37
+ }
38
+
39
+ // Action mode: transcription starts with an action verb
40
+ const firstWord = msg.transcription.trim().split(/\s+/)[0]?.toLowerCase() ?? '';
41
+ if (ACTION_VERBS.includes(firstWord)) {
42
+ return 'action';
43
+ }
44
+
45
+ // Dictation mode: cursor is in a text field with no selection — clean up for typing
46
+ if (msg.context.cursorInTextField) {
47
+ return 'dictation';
48
+ }
49
+
50
+ // AX focus-role detection in browser editors (for example Gmail compose)
51
+ // is occasionally incomplete. If we default to action here, normal dictation
52
+ // gets misrouted into a new chat task. Treat ambiguous context as dictation.
53
+ return 'dictation';
54
+ }
55
+
56
+ function buildDictationPrompt(msg: DictationRequest): string {
57
+ return [
58
+ 'You are a dictation assistant. Clean up the following speech transcription for direct insertion into a text field.',
59
+ '',
60
+ '## Rules',
61
+ '- Fix grammar, punctuation, and capitalization',
62
+ '- Remove filler words (um, uh, like, you know)',
63
+ "- Maintain the speaker's intent and meaning",
64
+ '- Do NOT add explanations or commentary',
65
+ '- Return ONLY the cleaned text, nothing else',
66
+ '',
67
+ '## Tone Adaptation',
68
+ 'Adapt your output tone based on the active application:',
69
+ '- Email apps (Gmail, Mail): Professional but warm. Use proper greetings and sign-offs if appropriate.',
70
+ '- Slack: Casual and conversational. Match typical chat style.',
71
+ '- Code editors (VS Code, Xcode): Technical and concise. Code comments style.',
72
+ '- Terminal: Command-like, terse.',
73
+ '- Messages/iMessage: Very casual, texting style. Short sentences.',
74
+ '- Notes/Docs: Neutral, clear writing.',
75
+ '- Default: Match the user\'s natural voice.',
76
+ '',
77
+ '## Context Clues',
78
+ '- Window title may contain recipient name (Slack DMs, email compose)',
79
+ '- If you can identify a recipient, adapt formality to the apparent relationship',
80
+ '- Maintain the user\'s natural voice — don\'t over-formalize casual speech',
81
+ '- The user\'s writing patterns and preferences may be available from memory context — follow those when present',
82
+ '',
83
+ buildAppMetadataBlock(msg),
84
+ ].join('\n');
85
+ }
86
+
87
+ function buildCommandPrompt(msg: DictationRequest): string {
88
+ return [
89
+ 'You are a text transformation assistant. The user has selected text and given a voice command to transform it.',
90
+ '',
91
+ '## Rules',
92
+ '- Apply the instruction to the selected text',
93
+ '- Return ONLY the transformed text, nothing else',
94
+ '- Do NOT add explanations or commentary',
95
+ '',
96
+ '## Tone Adaptation',
97
+ 'Match the tone to the active application context:',
98
+ '- Email apps (Gmail, Mail): Professional but warm.',
99
+ '- Slack: Casual and conversational.',
100
+ '- Code editors (VS Code, Xcode): Technical and concise.',
101
+ '- Terminal: Command-like, terse.',
102
+ '- Messages/iMessage: Very casual, texting style.',
103
+ '- Notes/Docs: Neutral, clear writing.',
104
+ '- Default: Match the user\'s natural voice.',
105
+ '',
106
+ '## Context Clues',
107
+ '- Window title may contain recipient name (Slack DMs, email compose)',
108
+ '- If you can identify a recipient, adapt formality to the apparent relationship',
109
+ '- Maintain the user\'s natural voice — don\'t over-formalize casual speech',
110
+ '- The user\'s writing patterns and preferences may be available from memory context — follow those when present',
111
+ '',
112
+ buildAppMetadataBlock(msg),
113
+ '',
114
+ 'Selected text:',
115
+ msg.context.selectedText ?? '',
116
+ '',
117
+ `Instruction: ${msg.transcription}`,
118
+ ].join('\n');
119
+ }
120
+
121
+ export async function handleDictationRequest(
122
+ msg: DictationRequest,
123
+ socket: net.Socket,
124
+ ctx: HandlerContext,
125
+ ): Promise<void> {
126
+ const mode = detectDictationMode(msg);
127
+ log.info({ mode, transcriptionLength: msg.transcription.length }, 'Dictation request received');
128
+
129
+ // Action mode: return immediately — the client will route to a full agent session
130
+ if (mode === 'action') {
131
+ ctx.send(socket, {
132
+ type: 'dictation_response',
133
+ text: msg.transcription,
134
+ mode: 'action',
135
+ actionPlan: `User wants to: ${msg.transcription}`,
136
+ });
137
+ return;
138
+ }
139
+
140
+ // Dictation / command mode: make a single-turn LLM call for text cleanup or transformation
141
+ const systemPrompt = mode === 'dictation'
142
+ ? buildDictationPrompt(msg)
143
+ : buildCommandPrompt(msg);
144
+
145
+ const userText = mode === 'dictation'
146
+ ? msg.transcription
147
+ : msg.transcription; // command prompt already embeds the selected text and instruction
148
+
149
+ try {
150
+ const config = getConfig();
151
+ if (!listProviders().includes(config.provider)) {
152
+ log.warn({ provider: config.provider }, 'Dictation: no provider available, returning raw transcription');
153
+ const fallbackText = mode === 'command' ? (msg.context.selectedText ?? msg.transcription) : msg.transcription;
154
+ ctx.send(socket, { type: 'dictation_response', text: fallbackText, mode });
155
+ return;
156
+ }
157
+
158
+ const provider = getFailoverProvider(config.provider, config.providerOrder);
159
+ const response = await provider.sendMessage(
160
+ [{ role: 'user', content: [{ type: 'text', text: userText }] }],
161
+ [], // no tools
162
+ systemPrompt,
163
+ { config: { max_tokens: 1024 } },
164
+ );
165
+
166
+ const textBlock = response.content.find((b) => b.type === 'text');
167
+ const inlineFallback = mode === 'command' ? (msg.context.selectedText ?? msg.transcription) : msg.transcription;
168
+ const cleanedText = textBlock && 'text' in textBlock ? textBlock.text.trim() : inlineFallback;
169
+
170
+ ctx.send(socket, { type: 'dictation_response', text: cleanedText, mode });
171
+ } catch (err) {
172
+ const message = err instanceof Error ? err.message : String(err);
173
+ log.error({ err }, 'Dictation LLM call failed, returning raw transcription');
174
+ const fallbackText = mode === 'command' ? (msg.context.selectedText ?? msg.transcription) : msg.transcription;
175
+ ctx.send(socket, { type: 'dictation_response', text: fallbackText, mode });
176
+ ctx.send(socket, { type: 'error', message: `Dictation cleanup failed: ${message}` });
177
+ }
178
+ }
179
+
180
+ export const dictationHandlers = defineHandlers({
181
+ dictation_request: handleDictationRequest,
182
+ });
@@ -2,7 +2,7 @@ import * as net from 'node:net';
2
2
  import { existsSync, readFileSync, statSync } from 'node:fs';
3
3
  import { join, dirname } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { getWorkspacePromptPath } from '../../util/platform.js';
5
+ import { getWorkspacePromptPath, readLockfile } from '../../util/platform.js';
6
6
  import { log, defineHandlers, type HandlerContext } from './shared.js';
7
7
 
8
8
  function handleIdentityGet(socket: net.Socket, ctx: HandlerContext): void {
@@ -68,28 +68,19 @@ function handleIdentityGet(socket: net.Socket, ctx: HandlerContext): void {
68
68
  let cloud: string | undefined;
69
69
  let originSystem: string | undefined;
70
70
  try {
71
- const homedir = process.env.HOME ?? process.env.USERPROFILE ?? '';
72
- const lockfilePaths = [
73
- join(homedir, '.vellum.lock.json'),
74
- join(homedir, '.vellum.lockfile.json'),
75
- ];
76
- for (const lockPath of lockfilePaths) {
77
- if (!existsSync(lockPath)) continue;
78
- const lockData = JSON.parse(readFileSync(lockPath, 'utf-8'));
79
- const assistants = lockData.assistants as Array<Record<string, unknown>> | undefined;
80
- if (assistants && assistants.length > 0) {
81
- // Use the most recently hatched assistant
82
- const sorted = [...assistants].sort((a, b) => {
83
- const dateA = new Date(a.hatchedAt as string || 0).getTime();
84
- const dateB = new Date(b.hatchedAt as string || 0).getTime();
85
- return dateB - dateA;
86
- });
87
- const latest = sorted[0];
88
- assistantId = latest.assistantId as string | undefined;
89
- cloud = latest.cloud as string | undefined;
90
- originSystem = cloud === 'local' ? 'local' : cloud;
91
- }
92
- break;
71
+ const lockData = readLockfile();
72
+ const assistants = lockData?.assistants as Array<Record<string, unknown>> | undefined;
73
+ if (assistants && assistants.length > 0) {
74
+ // Use the most recently hatched assistant
75
+ const sorted = [...assistants].sort((a, b) => {
76
+ const dateA = new Date(a.hatchedAt as string || 0).getTime();
77
+ const dateB = new Date(b.hatchedAt as string || 0).getTime();
78
+ return dateB - dateA;
79
+ });
80
+ const latest = sorted[0];
81
+ assistantId = latest.assistantId as string | undefined;
82
+ cloud = latest.cloud as string | undefined;
83
+ originSystem = cloud === 'local' ? 'local' : cloud;
93
84
  }
94
85
  } catch {
95
86
  // ignore — lockfile may not exist
@@ -22,6 +22,7 @@ import { signingHandlers } from './signing.js';
22
22
  import { twitterAuthHandlers } from './twitter-auth.js';
23
23
  import { workspaceFileHandlers } from './workspace-files.js';
24
24
  import { identityHandlers } from './identity.js';
25
+ import { dictationHandlers } from './dictation.js';
25
26
 
26
27
  // Re-export types and utilities for backwards compatibility
27
28
  export type {
@@ -107,6 +108,7 @@ const handlers = {
107
108
  ...twitterAuthHandlers,
108
109
  ...workspaceFileHandlers,
109
110
  ...identityHandlers,
111
+ ...dictationHandlers,
110
112
  ...inlineHandlers,
111
113
  } satisfies DispatchMap;
112
114
 
@@ -114,6 +114,8 @@ export async function handleUserMessage(
114
114
  }
115
115
 
116
116
  rlog.info('Processing user message');
117
+ session.setAssistantId('self');
118
+ session.setGuardianContext(null);
117
119
  // Fire-and-forget: don't block the IPC handler so the connection can
118
120
  // continue receiving messages (e.g. cancel, confirmations, or
119
121
  // additional user_message that will be queued by the session).
@@ -9,6 +9,7 @@ import type { ClientMessage, CuSessionCreate, ServerMessage, SessionTransportMet
9
9
  import type { SecretPromptResult } from '../../permissions/secret-prompter.js';
10
10
  import { getConfig } from '../../config/loader.js';
11
11
  import type { DebouncerMap } from '../../util/debounce.js';
12
+ import type { GuardianRuntimeContext } from '../session-runtime-assembly.js';
12
13
 
13
14
  const log = getLogger('handlers');
14
15
 
@@ -99,6 +100,8 @@ export interface SessionCreateOptions {
99
100
  systemPromptOverride?: string;
100
101
  maxResponseTokens?: number;
101
102
  transport?: SessionTransportMetadata;
103
+ assistantId?: string;
104
+ guardianContext?: GuardianRuntimeContext;
102
105
  memoryScopeId?: string;
103
106
  isPrivateThread?: boolean;
104
107
  strictPrivateSideEffects?: boolean;
@@ -148,15 +148,23 @@ function broadcastWorkItemStatus(ctx: HandlerContext, id: string): void {
148
148
  }
149
149
  }
150
150
 
151
- /** Extract plain text from a message content string (handles JSON content block arrays). */
152
- function extractTextFromContent(content: string): string {
151
+ /**
152
+ * Extract only the latest assistant text block from stored content.
153
+ * Consolidation merges multiple assistant messages into one DB row; scanning
154
+ * from the end keeps task output focused on the final assistant response.
155
+ */
156
+ function extractLatestTextFromContent(content: string): string {
153
157
  try {
154
158
  const parsed = JSON.parse(content);
155
159
  if (Array.isArray(parsed)) {
156
- return parsed
157
- .filter((b: { type: string }) => b.type === 'text')
158
- .map((b: { text: string }) => b.text)
159
- .join('\n');
160
+ for (let i = parsed.length - 1; i >= 0; i--) {
161
+ const block = parsed[i] as { type?: unknown; text?: unknown };
162
+ if (block.type !== 'text') continue;
163
+ if (typeof block.text !== 'string') continue;
164
+ if (!block.text.trim()) continue;
165
+ return block.text;
166
+ }
167
+ return '';
160
168
  }
161
169
  } catch {
162
170
  // Plain text content — use as-is
@@ -299,7 +307,7 @@ export function handleWorkItemOutput(
299
307
  const m = msgs[i];
300
308
  if (m.role !== 'assistant') continue;
301
309
 
302
- const text = extractTextFromContent(m.content);
310
+ const text = extractLatestTextFromContent(m.content);
303
311
  if (!text.trim()) continue;
304
312
 
305
313
  summary = truncate(text, 2000, '');
@@ -19,12 +19,14 @@
19
19
  "BrowserUserScroll",
20
20
  "BundleAppRequest",
21
21
  "CancelRequest",
22
+ "ChannelReadinessRequest",
22
23
  "ConfirmationResponse",
23
24
  "CuObservation",
24
25
  "CuSessionAbort",
25
26
  "CuSessionCreate",
26
27
  "DeleteQueuedMessage",
27
28
  "DiagnosticsExportRequest",
29
+ "DictationRequest",
28
30
  "DocumentListRequest",
29
31
  "DocumentLoadRequest",
30
32
  "DocumentSaveRequest",
@@ -137,6 +139,7 @@
137
139
  "BrowserHandoffRequest",
138
140
  "BrowserInteractiveModeChanged",
139
141
  "BundleAppResponse",
142
+ "ChannelReadinessResponse",
140
143
  "ConfirmationRequest",
141
144
  "ContextCompacted",
142
145
  "CuAction",
@@ -144,6 +147,7 @@
144
147
  "CuError",
145
148
  "DaemonStatusMessage",
146
149
  "DiagnosticsExportResponse",
150
+ "DictationResponse",
147
151
  "DocumentEditorShow",
148
152
  "DocumentEditorUpdate",
149
153
  "DocumentListResponse",
@@ -179,6 +183,7 @@
179
183
  "PublishPageResponse",
180
184
  "ReminderFired",
181
185
  "RemindersListResponse",
186
+ "RideShotgunProgress",
182
187
  "RideShotgunResult",
183
188
  "ScheduleComplete",
184
189
  "SchedulesListResponse",
@@ -268,12 +273,14 @@
268
273
  "browser_user_scroll",
269
274
  "bundle_app",
270
275
  "cancel",
276
+ "channel_readiness",
271
277
  "confirmation_response",
272
278
  "cu_observation",
273
279
  "cu_session_abort",
274
280
  "cu_session_create",
275
281
  "delete_queued_message",
276
282
  "diagnostics_export_request",
283
+ "dictation_request",
277
284
  "document_list",
278
285
  "document_load",
279
286
  "document_save",
@@ -386,6 +393,7 @@
386
393
  "browser_handoff_request",
387
394
  "browser_interactive_mode_changed",
388
395
  "bundle_app_response",
396
+ "channel_readiness_response",
389
397
  "confirmation_request",
390
398
  "context_compacted",
391
399
  "cu_action",
@@ -393,6 +401,7 @@
393
401
  "cu_error",
394
402
  "daemon_status",
395
403
  "diagnostics_export_response",
404
+ "dictation_response",
396
405
  "document_editor_show",
397
406
  "document_editor_update",
398
407
  "document_list_response",
@@ -428,6 +437,7 @@
428
437
  "publish_page_response",
429
438
  "reminder_fired",
430
439
  "reminders_list_response",
440
+ "ride_shotgun_progress",
431
441
  "ride_shotgun_result",
432
442
  "schedule_complete",
433
443
  "schedules_list_response",
@@ -23,6 +23,14 @@ export interface UsageStats {
23
23
  estimatedCost: number;
24
24
  }
25
25
 
26
+ export interface DictationContext {
27
+ bundleIdentifier: string;
28
+ appName: string;
29
+ windowTitle: string;
30
+ selectedText?: string;
31
+ cursorInTextField: boolean;
32
+ }
33
+
26
34
  // === Client → Server messages ===
27
35
 
28
36
  export interface UserMessage {
@@ -555,13 +563,31 @@ export interface TelegramConfigResponse {
555
563
 
556
564
  export interface TwilioConfigRequest {
557
565
  type: 'twilio_config';
558
- action: 'get' | 'set_credentials' | 'clear_credentials' | 'provision_number' | 'assign_number' | 'list_numbers';
566
+ action: 'get' | 'set_credentials' | 'clear_credentials' | 'provision_number' | 'assign_number' | 'list_numbers'
567
+ | 'sms_compliance_status' | 'sms_submit_tollfree_verification' | 'sms_update_tollfree_verification'
568
+ | 'sms_delete_tollfree_verification' | 'release_number' | 'sms_send_test' | 'sms_doctor';
559
569
  accountSid?: string; // Only for action: 'set_credentials'
560
570
  authToken?: string; // Only for action: 'set_credentials'
561
- phoneNumber?: string; // Only for action: 'assign_number'
571
+ phoneNumber?: string; // Only for action: 'assign_number' or 'sms_send_test'
562
572
  areaCode?: string; // Only for action: 'provision_number'
563
573
  country?: string; // Only for action: 'provision_number' (ISO 3166-1 alpha-2, default 'US')
564
574
  assistantId?: string; // Scope number assignment/lookup to a specific assistant
575
+ verificationSid?: string; // Only for update/delete verification actions
576
+ verificationParams?: {
577
+ tollfreePhoneNumberSid?: string;
578
+ businessName?: string;
579
+ businessWebsite?: string;
580
+ notificationEmail?: string;
581
+ useCaseCategories?: string[];
582
+ useCaseSummary?: string;
583
+ productionMessageSample?: string;
584
+ optInImageUrls?: string[];
585
+ optInType?: string;
586
+ messageVolume?: string;
587
+ businessType?: string;
588
+ customerProfileSid?: string;
589
+ };
590
+ text?: string; // Only for action: 'sms_send_test' (default: "Test SMS from your Vellum assistant")
565
591
  }
566
592
 
567
593
  export interface TwilioConfigResponse {
@@ -573,6 +599,56 @@ export interface TwilioConfigResponse {
573
599
  error?: string;
574
600
  /** Non-fatal warning message (e.g. webhook sync failure that did not prevent the primary operation). */
575
601
  warning?: string;
602
+ compliance?: {
603
+ numberType?: string;
604
+ verificationSid?: string;
605
+ verificationStatus?: string;
606
+ rejectionReason?: string;
607
+ rejectionReasons?: string[];
608
+ errorCode?: string;
609
+ editAllowed?: boolean;
610
+ editExpiration?: string;
611
+ };
612
+ /** Present when action is 'sms_send_test'. */
613
+ testResult?: {
614
+ messageSid: string;
615
+ to: string;
616
+ initialStatus: string;
617
+ finalStatus: string;
618
+ errorCode?: string;
619
+ errorMessage?: string;
620
+ };
621
+ /** Present when action is 'sms_doctor'. */
622
+ diagnostics?: {
623
+ readiness: { ready: boolean; issues: string[] };
624
+ compliance: { status: string; detail?: string; remediation?: string };
625
+ lastSend?: { status: string; errorCode?: string; remediation?: string };
626
+ overallStatus: 'healthy' | 'degraded' | 'broken';
627
+ actionItems: string[];
628
+ };
629
+ }
630
+
631
+ export interface ChannelReadinessRequest {
632
+ type: 'channel_readiness';
633
+ action: 'get' | 'refresh';
634
+ channel?: string;
635
+ assistantId?: string;
636
+ includeRemote?: boolean;
637
+ }
638
+
639
+ export interface ChannelReadinessResponse {
640
+ type: 'channel_readiness_response';
641
+ success: boolean;
642
+ snapshots?: Array<{
643
+ channel: string;
644
+ ready: boolean;
645
+ checkedAt: number;
646
+ stale: boolean;
647
+ reasons: Array<{ code: string; text: string }>;
648
+ localChecks: Array<{ name: string; passed: boolean; message: string }>;
649
+ remoteChecks?: Array<{ name: string; passed: boolean; message: string }>;
650
+ }>;
651
+ error?: string;
576
652
  }
577
653
 
578
654
  export interface GuardianVerificationRequest {
@@ -597,6 +673,10 @@ export interface GuardianVerificationResponse {
597
673
  assistantId?: string;
598
674
  /** The delivery chat ID for the guardian (e.g. Telegram chat ID). Present when action is 'status' and bound is true. */
599
675
  guardianDeliveryChatId?: string;
676
+ /** Optional channel username/handle for the bound guardian (for UI display). */
677
+ guardianUsername?: string;
678
+ /** Optional display name for the bound guardian (for UI display). */
679
+ guardianDisplayName?: string;
600
680
  error?: string;
601
681
  }
602
682
 
@@ -659,6 +739,12 @@ export interface IpcBlobProbe {
659
739
  nonceSha256: string;
660
740
  }
661
741
 
742
+ export interface DictationRequest {
743
+ type: 'dictation_request';
744
+ transcription: string;
745
+ context: DictationContext;
746
+ }
747
+
662
748
  // === Surface types ===
663
749
 
664
750
  export type SurfaceType = 'card' | 'form' | 'list' | 'table' | 'confirmation' | 'dynamic_page' | 'file_upload' | 'browser_view' | 'document_preview';
@@ -1084,6 +1170,7 @@ export type ClientMessage =
1084
1170
  | TwitterIntegrationConfigRequest
1085
1171
  | TelegramConfigRequest
1086
1172
  | TwilioConfigRequest
1173
+ | ChannelReadinessRequest
1087
1174
  | GuardianVerificationRequest
1088
1175
  | TwitterAuthStartRequest
1089
1176
  | TwitterAuthStatusRequest
@@ -1129,7 +1216,8 @@ export type ClientMessage =
1129
1216
  | WorkspaceFileReadRequest
1130
1217
  | IdentityGetRequest
1131
1218
  | ToolPermissionSimulateRequest
1132
- | ToolNamesListRequest;
1219
+ | ToolNamesListRequest
1220
+ | DictationRequest;
1133
1221
 
1134
1222
  export interface IntegrationListRequest {
1135
1223
  type: 'integration_list';
@@ -1528,6 +1616,12 @@ export interface TaskRouted {
1528
1616
  escalatedFrom?: string;
1529
1617
  }
1530
1618
 
1619
+ export interface RideShotgunProgress {
1620
+ type: 'ride_shotgun_progress';
1621
+ watchId: string;
1622
+ message: string;
1623
+ }
1624
+
1531
1625
  export interface RideShotgunResult {
1532
1626
  type: 'ride_shotgun_result';
1533
1627
  sessionId: string;
@@ -2366,6 +2460,13 @@ export interface ToolNamesListResponse {
2366
2460
  schemas?: Record<string, ToolInputSchema>;
2367
2461
  }
2368
2462
 
2463
+ export interface DictationResponse {
2464
+ type: 'dictation_response';
2465
+ text: string;
2466
+ mode: 'dictation' | 'command' | 'action';
2467
+ actionPlan?: string;
2468
+ }
2469
+
2369
2470
  export type ServerMessage =
2370
2471
  | AuthResult
2371
2472
  | UserMessageEcho
@@ -2400,6 +2501,7 @@ export type ServerMessage =
2400
2501
  | CuError
2401
2502
  | SessionErrorMessage
2402
2503
  | TaskRouted
2504
+ | RideShotgunProgress
2403
2505
  | RideShotgunResult
2404
2506
  | UiSurfaceShow
2405
2507
  | UiSurfaceUpdate
@@ -2452,6 +2554,7 @@ export type ServerMessage =
2452
2554
  | TwitterIntegrationConfigResponse
2453
2555
  | TelegramConfigResponse
2454
2556
  | TwilioConfigResponse
2557
+ | ChannelReadinessResponse
2455
2558
  | GuardianVerificationResponse
2456
2559
  | TwitterAuthResult
2457
2560
  | TwitterAuthStatusResponse
@@ -2495,7 +2598,8 @@ export type ServerMessage =
2495
2598
  | WorkspaceFileReadResponse
2496
2599
  | IdentityGetResponse
2497
2600
  | ToolPermissionSimulateResponse
2498
- | ToolNamesListResponse;
2601
+ | ToolNamesListResponse
2602
+ | DictationResponse;
2499
2603
 
2500
2604
  // === Subagent IPC ─────────────────────────────────────────────────────
2501
2605
 
@@ -42,6 +42,7 @@ import { registerMessagingProvider } from '../messaging/registry.js';
42
42
  import { slackProvider as slackMessagingProvider } from '../messaging/providers/slack/adapter.js';
43
43
  import { gmailMessagingProvider } from '../messaging/providers/gmail/adapter.js';
44
44
  import { telegramBotMessagingProvider } from '../messaging/providers/telegram-bot/adapter.js';
45
+ import { smsMessagingProvider } from '../messaging/providers/sms/adapter.js';
45
46
  import { browserManager } from '../tools/browser/browser-manager.js';
46
47
  import { RuntimeHttpServer } from '../runtime/http-server.js';
47
48
  import { getHookManager } from '../hooks/manager.js';
@@ -386,6 +387,7 @@ export async function runDaemon(): Promise<void> {
386
387
  registerMessagingProvider(slackMessagingProvider);
387
388
  registerMessagingProvider(gmailMessagingProvider);
388
389
  registerMessagingProvider(telegramBotMessagingProvider);
390
+ registerMessagingProvider(smsMessagingProvider);
389
391
 
390
392
  const scheduler = startScheduler(
391
393
  async (conversationId, message) => {
@@ -177,7 +177,7 @@ export async function handleRideShotgunStart(
177
177
  type: 'ride_shotgun_progress',
178
178
  watchId,
179
179
  message: `[${progress.pageNumber || '?'}] ${shortUrl}`,
180
- } as any);
180
+ });
181
181
  }
182
182
  }).then(visited => {
183
183
  clearInterval(checkInterval);
@@ -666,6 +666,8 @@ export class DaemonServer {
666
666
  throw new Error('Session is already processing a message');
667
667
  }
668
668
 
669
+ session.setAssistantId(options?.assistantId ?? 'self');
670
+ session.setGuardianContext(options?.guardianContext ?? null);
669
671
  session.setChannelCapabilities(resolveChannelCapabilities(sourceChannel));
670
672
 
671
673
  const attachments = attachmentIds
@@ -720,6 +722,8 @@ export class DaemonServer {
720
722
  throw new Error('Session is already processing a message');
721
723
  }
722
724
 
725
+ session.setAssistantId(options?.assistantId ?? 'self');
726
+ session.setGuardianContext(options?.guardianContext ?? null);
723
727
  session.setChannelCapabilities(resolveChannelCapabilities(sourceChannel));
724
728
 
725
729
  const attachments = attachmentIds
@@ -790,8 +794,8 @@ export class DaemonServer {
790
794
 
791
795
  createRunOrchestrator(): RunOrchestrator {
792
796
  return new RunOrchestrator({
793
- getOrCreateSession: (conversationId) =>
794
- this.getOrCreateSession(conversationId),
797
+ getOrCreateSession: (conversationId, transport) =>
798
+ this.getOrCreateSession(conversationId, undefined, true, transport ? { transport } : undefined),
795
799
  resolveAttachments: (attachmentIds) =>
796
800
  attachmentsStore.getAttachmentsByIds(attachmentIds).map((a) => ({
797
801
  id: a.id,