@vellumai/assistant 0.11.3 → 0.11.4-staging.1

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 (324) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/docs/architecture/memory.md +11 -0
  3. package/docs/architecture/turn-actor.md +70 -0
  4. package/docs/flux-turn-detection-spike.md +243 -0
  5. package/docs/stt-provider-onboarding.md +3 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  8. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  10. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  11. package/openapi.yaml +140 -38
  12. package/package.json +1 -1
  13. package/scripts/voice-ttft-spike.ts +3 -3
  14. package/src/__tests__/app-compiler.test.ts +38 -3
  15. package/src/__tests__/attachments-store.test.ts +21 -12
  16. package/src/__tests__/byok-default-profile-ensure.test.ts +2 -0
  17. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  18. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  19. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  20. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  21. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  22. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  23. package/src/__tests__/config-schema.test.ts +25 -10
  24. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  26. package/src/__tests__/conversation-agent-loop.test.ts +28 -20
  27. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  28. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  29. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  30. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  31. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  32. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  33. package/src/__tests__/conversation-queue.test.ts +55 -4
  34. package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
  35. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  36. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  37. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  38. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  39. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  40. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  41. package/src/__tests__/edit-propagation.test.ts +0 -7
  42. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  43. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  44. package/src/__tests__/injector-chain.test.ts +63 -41
  45. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  46. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  47. package/src/__tests__/llm-schema.test.ts +5 -2
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  49. package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
  50. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  51. package/src/__tests__/mtime-cache.test.ts +1 -1
  52. package/src/__tests__/non-member-access-request.test.ts +0 -20
  53. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
  55. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  56. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  57. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  58. package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
  59. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  60. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  61. package/src/__tests__/skills.test.ts +32 -0
  62. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  63. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  64. package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
  65. package/src/__tests__/turn-events-store.test.ts +43 -0
  66. package/src/__tests__/user-plugin-loader.test.ts +1 -1
  67. package/src/__tests__/visible-app-context.test.ts +16 -9
  68. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  69. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  70. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  71. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  72. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  73. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  74. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  75. package/src/acp/__tests__/auth-required.test.ts +161 -0
  76. package/src/acp/acp-claude-oauth.ts +19 -2
  77. package/src/acp/agent-process.test.ts +100 -0
  78. package/src/acp/agent-process.ts +29 -26
  79. package/src/acp/auth-required.ts +102 -0
  80. package/src/acp/session-manager.test.ts +119 -0
  81. package/src/acp/session-manager.ts +68 -2
  82. package/src/api/events/acp-auth-required.ts +55 -0
  83. package/src/api/index.ts +7 -0
  84. package/src/apps/app-store.ts +3 -0
  85. package/src/bundler/package-resolver.ts +2 -30
  86. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  87. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  88. package/src/calls/call-controller.ts +9 -2
  89. package/src/calls/call-setup-flow.ts +0 -1
  90. package/src/calls/media-stream-stt-session.ts +15 -0
  91. package/src/calls/voice-session-bridge.ts +71 -16
  92. package/src/calls/voice-triage-escalate.ts +104 -2
  93. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  94. package/src/channels/config.ts +13 -0
  95. package/src/channels/plugin-channel-declarations.ts +108 -0
  96. package/src/channels/types.ts +30 -0
  97. package/src/cli/AGENTS.md +5 -2
  98. package/src/cli/commands/credentials.help.ts +2 -2
  99. package/src/cli/commands/inference-providers.ts +1 -1
  100. package/src/cli/commands/mcp.help.ts +13 -4
  101. package/src/cli/commands/mcp.ts +9 -0
  102. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  103. package/src/cli/commands/memory/index.help.ts +43 -1
  104. package/src/cli/commands/memory/memory-v3.ts +64 -0
  105. package/src/cli/commands/stt.help.ts +27 -2
  106. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  107. package/src/cli/lib/bundled-marketplace.json +13 -0
  108. package/src/cli/lib/upgrade-plugin.ts +42 -0
  109. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  110. package/src/config/__tests__/default-provider.test.ts +6 -1
  111. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  112. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  113. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  114. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  115. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  116. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  117. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  118. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  119. package/src/config/call-site-defaults.ts +4 -5
  120. package/src/config/default-profile-catalog.ts +82 -11
  121. package/src/config/default-profile-names.ts +4 -1
  122. package/src/config/default-provider-resolution.ts +4 -0
  123. package/src/config/llm-context-resolution.ts +11 -3
  124. package/src/config/llm-resolver.ts +28 -1
  125. package/src/config/profile-materialization.ts +70 -22
  126. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  127. package/src/config/schemas/call-site-catalog.ts +4 -4
  128. package/src/config/schemas/live-voice.ts +57 -23
  129. package/src/config/schemas/llm.ts +59 -32
  130. package/src/config/schemas/mcp.ts +23 -0
  131. package/src/config/schemas/plugin-updates.ts +6 -2
  132. package/src/config/schemas/stt.ts +1 -0
  133. package/src/context/outbound-sanitize.ts +96 -1
  134. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  135. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  136. package/src/daemon/conversation-agent-loop.ts +17 -6
  137. package/src/daemon/conversation-messaging.ts +5 -1
  138. package/src/daemon/conversation-notifiers.ts +9 -1
  139. package/src/daemon/conversation-process.ts +9 -6
  140. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  141. package/src/daemon/conversation-tool-setup.ts +1 -2
  142. package/src/daemon/conversation.ts +48 -0
  143. package/src/daemon/mcp-reload-service.ts +36 -6
  144. package/src/daemon/process-message.ts +13 -3
  145. package/src/daemon/providers-setup.ts +6 -3
  146. package/src/daemon/trust-context-types.ts +29 -0
  147. package/src/daemon/wake-conversation-ops.ts +3 -2
  148. package/src/documents/document-store.ts +138 -5
  149. package/src/hooks/hook-loader.ts +3 -3
  150. package/src/hooks/registry.ts +50 -6
  151. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  152. package/src/inbound/oauth-callback-url.ts +61 -0
  153. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  154. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  155. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  156. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  157. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  158. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  159. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  160. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  161. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  162. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  163. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  164. package/src/live-voice/live-voice-archive.ts +2 -0
  165. package/src/live-voice/live-voice-metrics.ts +57 -32
  166. package/src/live-voice/live-voice-photo.ts +1 -2
  167. package/src/live-voice/live-voice-session.ts +535 -314
  168. package/src/live-voice/progress-narration.ts +277 -0
  169. package/src/live-voice/protocol.ts +21 -1
  170. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  171. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  172. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  173. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  174. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  175. package/src/mcp/client.ts +16 -11
  176. package/src/mcp/effective-config.ts +113 -0
  177. package/src/mcp/manager.ts +11 -6
  178. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  179. package/src/mcp/mcp-oauth-provider.ts +205 -240
  180. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  181. package/src/monitoring/plugin-auto-update.ts +128 -24
  182. package/src/notifications/signal.ts +1 -0
  183. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  184. package/src/permissions/confirmation-guardian-request.ts +2 -2
  185. package/src/permissions/question-guardian-request.test.ts +14 -6
  186. package/src/permissions/question-guardian-request.ts +1 -2
  187. package/src/persistence/attachments-store.ts +8 -1
  188. package/src/persistence/bookmark-crud.ts +3 -7
  189. package/src/persistence/conversation-attention-store.ts +16 -45
  190. package/src/persistence/conversation-crud.ts +33 -4
  191. package/src/persistence/conversation-lineage.ts +9 -0
  192. package/src/persistence/conversation-queries.ts +108 -41
  193. package/src/persistence/delivery-crud.ts +38 -29
  194. package/src/persistence/external-conversation-store.ts +32 -4
  195. package/src/persistence/llm-request-log-store.ts +4 -10
  196. package/src/persistence/llm-usage-store.ts +8 -3
  197. package/src/persistence/message-reads.test.ts +197 -0
  198. package/src/persistence/message-reads.ts +211 -0
  199. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  200. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  201. package/src/persistence/real-user-turn-filter.ts +27 -3
  202. package/src/persistence/steps.ts +9 -0
  203. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  204. package/src/plugin-api/conversation-turn.ts +168 -5
  205. package/src/plugin-api/index.ts +21 -5
  206. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  207. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  208. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  209. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  210. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  211. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  212. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  213. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  214. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  215. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  216. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  217. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  218. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  219. package/src/plugins/defaults/memory/indexer.ts +3 -1
  220. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  221. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  222. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  223. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  224. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  225. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  226. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  227. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  228. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  229. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  230. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  231. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  232. package/src/plugins/defaults/memory/worker.ts +6 -3
  233. package/src/plugins/external-plugin-loader.ts +47 -0
  234. package/src/plugins/mcp-servers.ts +361 -0
  235. package/src/plugins/mtime-cache.ts +23 -49
  236. package/src/plugins/worker-plugin-surface.ts +33 -0
  237. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  238. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  239. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  240. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  241. package/src/providers/call-site-routing.ts +30 -3
  242. package/src/providers/connection-resolution.ts +194 -11
  243. package/src/providers/inference/auth.ts +6 -0
  244. package/src/providers/inference/connection-availability.ts +24 -2
  245. package/src/providers/inference/connections.ts +2 -0
  246. package/src/providers/model-intents.ts +26 -6
  247. package/src/providers/openai/codex-models.ts +2 -1
  248. package/src/providers/provider-send-message.ts +32 -3
  249. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  250. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  251. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  252. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  253. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  254. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  255. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  256. package/src/providers/speech-to-text/resolve.ts +25 -2
  257. package/src/routes/worker.ts +17 -5
  258. package/src/runtime/access-request-helper.ts +9 -12
  259. package/src/runtime/agent-wake.ts +3 -3
  260. package/src/runtime/pre-first-message-gate.ts +4 -0
  261. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  262. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  263. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  264. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  265. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  266. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  267. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  268. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  269. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  270. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  271. package/src/runtime/routes/channel-route-shared.ts +0 -6
  272. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  273. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  274. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  275. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  276. package/src/runtime/routes/default-provider-routes.ts +15 -0
  277. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  278. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  279. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  280. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  281. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  282. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  283. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  284. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  285. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  286. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  287. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  288. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  289. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  291. package/src/runtime/routes/log-export-routes.ts +3 -0
  292. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  293. package/src/runtime/routes/plugins-routes.ts +21 -3
  294. package/src/runtime/routes/stt-routes.ts +31 -25
  295. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  296. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  297. package/src/runtime/routes/user-route-import.ts +108 -0
  298. package/src/schedule/worker.ts +6 -0
  299. package/src/security/oauth2.ts +6 -22
  300. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  301. package/src/stt/__tests__/types.test.ts +94 -0
  302. package/src/stt/daemon-batch-transcriber.ts +10 -0
  303. package/src/stt/stt-stream-session.ts +8 -4
  304. package/src/stt/types.ts +103 -0
  305. package/src/subagent/manager.ts +1 -3
  306. package/src/subagent/types.ts +7 -6
  307. package/src/tools/acp/spawn.test.ts +97 -0
  308. package/src/tools/acp/spawn.ts +32 -0
  309. package/src/tools/document/document-tool.ts +12 -3
  310. package/src/tools/registry.ts +2 -1
  311. package/src/tools/workflows/run-workflow.ts +1 -2
  312. package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
  313. package/src/tts/reasoning-tag-filter.ts +110 -0
  314. package/src/workspace/byok-default-profile-ensure.ts +61 -18
  315. package/src/workspace/custom-profile-ensure.ts +4 -24
  316. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  317. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  318. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  319. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  320. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  321. package/src/workspace/migrations/registry.ts +8 -0
  322. package/src/workspace/provider-commit-message-generator.ts +7 -5
  323. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  324. package/src/live-voice/front-decision.ts +0 -476
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Pure protocol helpers for Deepgram's Flux conversational speech API
3
+ * (`wss://api.deepgram.com/v2/listen`).
4
+ *
5
+ * This module holds only pure functions: frame parsing and URL query
6
+ * construction. It owns no socket, no timers, and no I/O, so the risky part
7
+ * of the protocol (the wire shapes Deepgram controls) can be exercised
8
+ * exhaustively in tests. The streaming transcriber wires these into a
9
+ * WebSocket session.
10
+ *
11
+ * Flux differs from Deepgram's v1 live API in that its turn model, not the
12
+ * caller's endpointing heuristics, decides when a turn ends. Frames are
13
+ * mapped onto the daemon's {@link SttStreamServerEvent} contract so that a
14
+ * consumer which ignores the turn-detection events still commits transcripts
15
+ * from `partial` / `final` exactly as it does with any other provider.
16
+ */
17
+
18
+ import type {
19
+ SttErrorCategory,
20
+ SttStreamServerErrorEvent,
21
+ SttStreamServerEvent,
22
+ } from "../../stt/types.js";
23
+ import { parseJsonSafe } from "../../util/json.js";
24
+ import { getLogger } from "../../util/logger.js";
25
+ import { isPlainObject } from "../../util/object.js";
26
+
27
+ const log = getLogger("deepgram-flux-frames");
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Wire types
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Frame discriminators Deepgram emits on `/v2/listen`.
35
+ *
36
+ * `EagerEndOfTurn` and `TurnResumed` appear only when
37
+ * `eager_eot_threshold` is set on the dialed URL. `Error` is a fatal frame:
38
+ * Deepgram terminates the session after sending it.
39
+ */
40
+ export type FluxFrameKind =
41
+ | "Connected"
42
+ | "TurnInfo"
43
+ | "StartOfTurn"
44
+ | "Update"
45
+ | "EagerEndOfTurn"
46
+ | "TurnResumed"
47
+ | "EndOfTurn"
48
+ | "Error";
49
+
50
+ /**
51
+ * A turn-state frame.
52
+ *
53
+ * Deepgram nests the turn state under `event` on `TurnInfo` frames
54
+ * (`{ type: "TurnInfo", event: "EndOfTurn", ... }`) and also documents the
55
+ * states as frame types in their own right. Both shapes are accepted:
56
+ * `event` wins when present, otherwise `type` is the discriminator.
57
+ */
58
+ export interface FluxTurnFrame {
59
+ type: FluxFrameKind;
60
+ /** Turn state carried by a `TurnInfo` frame. */
61
+ event?: FluxFrameKind;
62
+ request_id?: string;
63
+ sequence_id?: number;
64
+ /** Zero-based index of the turn within the session. */
65
+ turn_index?: number;
66
+ /** Start of the audio window this frame describes, in seconds. */
67
+ audio_window_start?: number;
68
+ /** End of the audio window this frame describes, in seconds. */
69
+ audio_window_end?: number;
70
+ /** Transcript for the turn so far. */
71
+ transcript?: string;
72
+ /** Flux's end-of-turn confidence in [0, 1]. */
73
+ end_of_turn_confidence?: number;
74
+ }
75
+
76
+ /**
77
+ * Fatal error frame. Deepgram closes the session immediately after sending
78
+ * one, so it is the only diagnostic a caller ever gets for a provider-side
79
+ * failure.
80
+ */
81
+ export interface FluxErrorFrame {
82
+ type: "Error";
83
+ sequence_id?: number;
84
+ /** Machine-readable identifier, e.g. `"INTERNAL_SERVER_ERROR"`. */
85
+ code?: string;
86
+ /** Prose explanation of the failure. */
87
+ description?: string;
88
+ }
89
+
90
+ /**
91
+ * A payload that has been confirmed to be an object but whose frame kind is
92
+ * not yet known: the union of every field any documented frame can carry.
93
+ */
94
+ type InboundFluxFrame = Partial<FluxTurnFrame> &
95
+ Partial<Omit<FluxErrorFrame, "type">>;
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Frame parsing
99
+ // ---------------------------------------------------------------------------
100
+
101
+ /**
102
+ * Map one Flux frame onto zero or more daemon streaming events.
103
+ *
104
+ * Accepts either a JSON string (as delivered by the WebSocket) or an
105
+ * already-parsed value. Unknown frame kinds, malformed JSON, and non-object
106
+ * payloads yield an empty array and a debug log: Deepgram can add frame types
107
+ * without breaking the session, so this never throws and never rejects a
108
+ * stream over an unrecognized frame.
109
+ *
110
+ * Mapping:
111
+ * - `Connected` emits nothing (session bookkeeping).
112
+ * - `StartOfTurn` emits `turn-start`.
113
+ * - `Update` and bare `TurnInfo` emit `partial`: an interim transcript
114
+ * refresh, since the commit comes from `EndOfTurn`.
115
+ * - `EagerEndOfTurn` emits `eager-turn-end`, retracted by `TurnResumed` or
116
+ * confirmed by `EndOfTurn`.
117
+ * - `TurnResumed` emits `turn-resumed`.
118
+ * - `EndOfTurn` emits `final` followed by `turn-end`. The `final` comes first
119
+ * so a consumer that ignores turn events commits the transcript exactly as
120
+ * it does for a provider without turn detection.
121
+ * - `Error` emits `error`. This frame is fatal and is the only place the
122
+ * provider's own diagnostic appears, so it is surfaced rather than dropped.
123
+ *
124
+ * `turn-start` and `turn-end` carry the frame's `turn_index` when Deepgram
125
+ * sends one, which is what lets a consumer tell an end-of-turn for the turn
126
+ * still in progress from one Deepgram has already superseded.
127
+ */
128
+ export function parseFluxFrame(raw: unknown): SttStreamServerEvent[] {
129
+ const frame = coerceFrame(raw);
130
+ if (!frame) {
131
+ return [];
132
+ }
133
+
134
+ const kind = readFrameKind(frame);
135
+ const turnIndex = readNumber(frame.turn_index);
136
+ switch (kind) {
137
+ case "Connected":
138
+ return [];
139
+ case "StartOfTurn":
140
+ return [
141
+ {
142
+ type: "turn-start",
143
+ ...(turnIndex !== undefined ? { turnIndex } : {}),
144
+ },
145
+ ];
146
+ case "Update":
147
+ case "TurnInfo":
148
+ return [{ type: "partial", text: readTranscript(frame) }];
149
+ case "EagerEndOfTurn":
150
+ return [{ type: "eager-turn-end", text: readTranscript(frame) }];
151
+ case "TurnResumed":
152
+ return [{ type: "turn-resumed" }];
153
+ case "EndOfTurn": {
154
+ const text = readTranscript(frame);
155
+ const confidence = readNumber(frame.end_of_turn_confidence);
156
+ return [
157
+ { type: "final", text },
158
+ {
159
+ type: "turn-end",
160
+ text,
161
+ ...(confidence !== undefined ? { confidence } : {}),
162
+ ...(turnIndex !== undefined ? { turnIndex } : {}),
163
+ },
164
+ ];
165
+ }
166
+ case "Error":
167
+ return [readErrorEvent(frame)];
168
+ default:
169
+ log.debug({ kind }, "Ignoring unrecognized Deepgram Flux frame");
170
+ return [];
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Normalize an inbound payload into a frame object, or null when it is not
176
+ * one. JSON strings are parsed; anything that is not a plain object (null,
177
+ * arrays, primitives, unparseable text) is rejected.
178
+ */
179
+ function coerceFrame(raw: unknown): InboundFluxFrame | null {
180
+ const value = typeof raw === "string" ? parseJsonSafe(raw) : raw;
181
+ if (!isPlainObject(value)) {
182
+ log.debug("Dropped a Deepgram Flux payload that is not a frame object");
183
+ return null;
184
+ }
185
+ return value as InboundFluxFrame;
186
+ }
187
+
188
+ /** The turn state a frame describes: `event` when present, else `type`. */
189
+ function readFrameKind(frame: InboundFluxFrame): string | undefined {
190
+ if (typeof frame.event === "string") {
191
+ return frame.event;
192
+ }
193
+ return typeof frame.type === "string" ? frame.type : undefined;
194
+ }
195
+
196
+ /** Trimmed transcript text, empty when the frame carries none. */
197
+ function readTranscript(frame: InboundFluxFrame): string {
198
+ return typeof frame.transcript === "string" ? frame.transcript.trim() : "";
199
+ }
200
+
201
+ /**
202
+ * Patterns that map a Flux error onto a normalized {@link SttErrorCategory},
203
+ * tried in order. Anything unmatched stays `provider-error`.
204
+ *
205
+ * Deepgram documents only `INTERNAL_SERVER_ERROR` for Flux itself, so these
206
+ * match the account-wide error vocabulary
207
+ * (https://developers.deepgram.com/docs/errors) and mirror how
208
+ * `DeepgramRealtimeTranscriber` separates auth and rate-limit failures from
209
+ * everything else.
210
+ *
211
+ * The timeout pattern deliberately requires a `_TIMEOUT` suffix at a word
212
+ * boundary so a configuration complaint about `eot_timeout_ms` is not
213
+ * mistaken for a transport timeout.
214
+ */
215
+ const FLUX_ERROR_CATEGORY_PATTERNS: ReadonlyArray<
216
+ readonly [SttErrorCategory, RegExp]
217
+ > = [
218
+ [
219
+ "auth",
220
+ /INVALID_AUTH|INSUFFICIENT_PERMISSIONS|UNAUTHORIZED|FORBIDDEN|INVALID CREDENTIALS|API KEY/,
221
+ ],
222
+ ["rate-limit", /TOO_MANY_REQUESTS|RATE[_ ]LIMIT|QUOTA/],
223
+ ["timeout", /^TIMEOUT|_TIMEOUT\b|TIMED[_ ]OUT/],
224
+ [
225
+ "invalid-audio",
226
+ /UNPROCESSABLE|CORRUPT|UNSUPPORTED|INVALID[_ ]AUDIO|BAD[_ ]AUDIO/,
227
+ ],
228
+ ];
229
+
230
+ /**
231
+ * Turn a fatal `Error` frame into a stream `error` event, preserving the
232
+ * provider's own `code` and `description` so the diagnostic survives all the
233
+ * way to the caller. Deepgram closes the socket right after this frame, so
234
+ * dropping it would leave a bare close as the only signal.
235
+ */
236
+ function readErrorEvent(frame: InboundFluxFrame): SttStreamServerErrorEvent {
237
+ const code = typeof frame.code === "string" ? frame.code.trim() : "";
238
+ const description =
239
+ typeof frame.description === "string" ? frame.description.trim() : "";
240
+
241
+ const haystack = `${code} ${description}`.toUpperCase();
242
+ const category =
243
+ FLUX_ERROR_CATEGORY_PATTERNS.find(([, pattern]) =>
244
+ pattern.test(haystack),
245
+ )?.[0] ?? "provider-error";
246
+
247
+ const detail = description || "no description provided";
248
+ const message = code
249
+ ? `Deepgram Flux error (${code}): ${detail}`
250
+ : `Deepgram Flux error: ${detail}`;
251
+
252
+ log.warn({ code, category }, "Deepgram Flux sent a fatal error frame");
253
+ return { type: "error", category, message };
254
+ }
255
+
256
+ /** A finite number, or undefined for anything else. */
257
+ function readNumber(value: unknown): number | undefined {
258
+ return typeof value === "number" && Number.isFinite(value)
259
+ ? value
260
+ : undefined;
261
+ }
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // Query parameters
265
+ // ---------------------------------------------------------------------------
266
+
267
+ /**
268
+ * Raw audio encodings Flux accepts. Containerized audio (WAV, Ogg) is
269
+ * self-describing and needs neither `encoding` nor `sample_rate`.
270
+ */
271
+ export type FluxEncoding =
272
+ | "linear16"
273
+ | "linear32"
274
+ | "mulaw"
275
+ | "alaw"
276
+ | "opus"
277
+ | "ogg-opus";
278
+
279
+ /** Inclusive bounds Deepgram enforces on `eot_threshold`. */
280
+ const EOT_THRESHOLD_RANGE = { min: 0.5, max: 0.9 } as const;
281
+
282
+ /**
283
+ * Deepgram's server-side default for `eot_threshold`, applied when the
284
+ * parameter is omitted. It is the ceiling `eager_eot_threshold` must respect
285
+ * in that case, since the validation runs against the threshold actually in
286
+ * force rather than the one we sent.
287
+ */
288
+ const DEFAULT_EOT_THRESHOLD = 0.7;
289
+
290
+ /** Inclusive bounds Deepgram enforces on `eager_eot_threshold`. */
291
+ const EAGER_EOT_THRESHOLD_RANGE = { min: 0.3, max: 0.9 } as const;
292
+
293
+ /** Inclusive bounds Deepgram enforces on `eot_timeout_ms`. */
294
+ const EOT_TIMEOUT_MS_RANGE = { min: 500, max: 60_000 } as const;
295
+
296
+ export interface FluxQueryParamOptions {
297
+ /** Flux model to run, e.g. `"flux-general-en"`. Required by Deepgram. */
298
+ model: string;
299
+ /**
300
+ * Encoding of the raw audio being sent. Omit for containerized audio,
301
+ * which carries its own format header.
302
+ */
303
+ encoding?: FluxEncoding;
304
+ /**
305
+ * Sample rate (Hz) of the raw audio. Required whenever {@link encoding} is
306
+ * set. Flux accepts 8000, 16000, 24000, 44100, and 48000.
307
+ */
308
+ sampleRate?: number;
309
+ /** End-of-turn confidence Flux must reach before committing a turn. */
310
+ eotThreshold?: number;
311
+ /**
312
+ * Confidence at which Flux starts speculating a turn has ended. Leaving it
313
+ * undefined omits the parameter, which is what keeps Deepgram from emitting
314
+ * `EagerEndOfTurn` / `TurnResumed` frames at all.
315
+ */
316
+ eagerEotThreshold?: number;
317
+ /** Silence (ms) after which Flux force-ends a turn. */
318
+ eotTimeoutMs?: number;
319
+ }
320
+
321
+ /**
322
+ * Build the query string for Flux's `/v2/listen` endpoint (no leading `?`).
323
+ *
324
+ * Every optional parameter is omitted when undefined rather than sent with a
325
+ * default, because Deepgram's own defaults are the intended behavior and
326
+ * omitting `eager_eot_threshold` is what disables eager turn-end speculation.
327
+ * Thresholds are clamped to the ranges Deepgram accepts so an out-of-range
328
+ * value degrades to the nearest legal one instead of failing the handshake.
329
+ *
330
+ * Deepgram additionally rejects a request whose `eager_eot_threshold` exceeds
331
+ * its `eot_threshold`
332
+ * (https://developers.deepgram.com/docs/flux/configuration#validation-rules),
333
+ * so after each threshold is clamped to its own range the eager value is
334
+ * clamped down again to the EOT threshold in force (the one being sent, or
335
+ * Deepgram's {@link DEFAULT_EOT_THRESHOLD} when none is). Clamping keeps a
336
+ * misconfigured pair from failing the whole session; the adjustment is logged
337
+ * at debug when it fires.
338
+ */
339
+ export function buildFluxQueryParams(opts: FluxQueryParamOptions): string {
340
+ const params = new URLSearchParams();
341
+ params.set("model", opts.model);
342
+
343
+ if (opts.encoding !== undefined) {
344
+ params.set("encoding", opts.encoding);
345
+ }
346
+ const sampleRate = readNumber(opts.sampleRate);
347
+ if (sampleRate !== undefined) {
348
+ params.set("sample_rate", String(Math.round(sampleRate)));
349
+ }
350
+
351
+ const eotThreshold = clamp(opts.eotThreshold, EOT_THRESHOLD_RANGE);
352
+ if (eotThreshold !== undefined) {
353
+ params.set("eot_threshold", String(eotThreshold));
354
+ }
355
+
356
+ let eagerEotThreshold = clamp(
357
+ opts.eagerEotThreshold,
358
+ EAGER_EOT_THRESHOLD_RANGE,
359
+ );
360
+ const eagerCeiling = eotThreshold ?? DEFAULT_EOT_THRESHOLD;
361
+ if (eagerEotThreshold !== undefined && eagerEotThreshold > eagerCeiling) {
362
+ log.debug(
363
+ {
364
+ requested: opts.eagerEotThreshold,
365
+ clampedTo: eagerCeiling,
366
+ eotThreshold: eagerCeiling,
367
+ usingDefaultEotThreshold: eotThreshold === undefined,
368
+ },
369
+ "Clamped Deepgram Flux eager_eot_threshold down to eot_threshold",
370
+ );
371
+ eagerEotThreshold = eagerCeiling;
372
+ }
373
+ if (eagerEotThreshold !== undefined) {
374
+ params.set("eager_eot_threshold", String(eagerEotThreshold));
375
+ }
376
+
377
+ const eotTimeoutMs = clamp(opts.eotTimeoutMs, EOT_TIMEOUT_MS_RANGE);
378
+ if (eotTimeoutMs !== undefined) {
379
+ params.set("eot_timeout_ms", String(Math.round(eotTimeoutMs)));
380
+ }
381
+
382
+ return params.toString();
383
+ }
384
+
385
+ /** Clamp a finite number into an inclusive range; undefined passes through. */
386
+ function clamp(
387
+ value: number | undefined,
388
+ range: { min: number; max: number },
389
+ ): number | undefined {
390
+ const numeric = readNumber(value);
391
+ if (numeric === undefined) {
392
+ return undefined;
393
+ }
394
+ return Math.min(Math.max(numeric, range.min), range.max);
395
+ }