@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
@@ -14,11 +14,6 @@ import type {
14
14
  StreamingTranscriber,
15
15
  SttStreamServerEvent,
16
16
  } from "../../stt/types.js";
17
- import type {
18
- VoiceAckTextInput,
19
- VoiceFrontDecider,
20
- VoiceProgressTextInput,
21
- } from "../front-decision.js";
22
17
  import {
23
18
  LiveVoiceSession,
24
19
  type LiveVoiceTtsStreamer,
@@ -26,6 +21,10 @@ import {
26
21
  } from "../live-voice-session.js";
27
22
  import type { LiveVoiceSessionFactoryContext } from "../live-voice-session-manager.js";
28
23
  import type { LiveVoiceTtsOptions } from "../live-voice-tts.js";
24
+ import type {
25
+ VoiceProgressNarrator,
26
+ VoiceProgressTextInput,
27
+ } from "../progress-narration.js";
29
28
  import {
30
29
  pickProgressPhrase,
31
30
  PROGRESS_FALLBACK_PHRASES,
@@ -47,16 +46,7 @@ const START_FRAME = {
47
46
  },
48
47
  } as const satisfies LiveVoiceClientStartFrame;
49
48
 
50
- // Generous enough that the slow-first-delta ack never fires during a test;
51
- // the immediate tool-start ack still speaks and is asserted where relevant.
52
- const GENEROUS_ACK_TIMEOUT_MS = 60_000;
53
-
54
49
  const GENERATED_NARRATION = "Progress text.";
55
- // Acks are front-model-phrased; narrations fall back to a static phrase.
56
- // Both pass through the same TTS sanitizer, and each fresh session's
57
- // narration phrase counter starts at 0.
58
- const GENERATED_TOOL_ACK = "Let me look that up.";
59
- const EXPECTED_TOOL_ACK = sanitizeForTts(GENERATED_TOOL_ACK).trim();
60
50
  const EXPECTED_PROGRESS_FALLBACK = sanitizeForTts(pickProgressPhrase(0)).trim();
61
51
 
62
52
  class MockStreamingTranscriber implements StreamingTranscriber {
@@ -143,11 +133,10 @@ function createRecordingTtsStreamer(
143
133
  return { streamTtsAudio, ttsTexts, ttsCalls };
144
134
  }
145
135
 
146
- function makeProgressDecider(
147
- generateProgressText: VoiceFrontDecider["generateProgressText"],
148
- ): VoiceFrontDecider {
136
+ function makeProgressNarrator(
137
+ generateProgressText: VoiceProgressNarrator["generateProgressText"],
138
+ ): VoiceProgressNarrator {
149
139
  return {
150
- generateAckText: async () => GENERATED_TOOL_ACK,
151
140
  generateProgressText,
152
141
  };
153
142
  }
@@ -157,7 +146,6 @@ function progressConfig(
157
146
  ): Partial<LiveVoiceFrontModelConfig> {
158
147
  const idleIntervalMs = overrides.idleIntervalMs ?? 60_000;
159
148
  return {
160
- ackFirstDeltaTimeoutMs: GENEROUS_ACK_TIMEOUT_MS,
161
149
  progress: {
162
150
  enabled: true,
163
151
  opsThreshold: 3,
@@ -176,7 +164,7 @@ function progressConfig(
176
164
 
177
165
  function createProgressHarness(options: {
178
166
  frontModelConfig: Partial<LiveVoiceFrontModelConfig>;
179
- frontDecider: VoiceFrontDecider;
167
+ progressNarrator: VoiceProgressNarrator;
180
168
  emitMetrics?: boolean;
181
169
  gateTtsText?: (text: string) => Promise<void> | null;
182
170
  // Events the transcriber flushes at utterance release; defaults to a plain
@@ -195,7 +183,7 @@ function createProgressHarness(options: {
195
183
  startVoiceTurn,
196
184
  streamTtsAudio,
197
185
  frontModelConfig: options.frontModelConfig,
198
- frontDecider: options.frontDecider,
186
+ progressNarrator: options.progressNarrator,
199
187
  createTurnId: () => "live-turn-1",
200
188
  emitMetrics: options.emitMetrics ?? false,
201
189
  });
@@ -282,7 +270,7 @@ describe("LiveVoiceSession progress narration", () => {
282
270
  );
283
271
  const { session, getCallbacks, ttsTexts } = createProgressHarness({
284
272
  frontModelConfig: progressConfig({ idleIntervalMs: 40 }),
285
- frontDecider: makeProgressDecider(generateProgressText),
273
+ progressNarrator: makeProgressNarrator(generateProgressText),
286
274
  });
287
275
  await startReleasedTurn(session, getCallbacks);
288
276
 
@@ -308,22 +296,18 @@ describe("LiveVoiceSession progress narration", () => {
308
296
  });
309
297
  const { session, getCallbacks, ttsTexts } = createProgressHarness({
310
298
  frontModelConfig: progressConfig({ opsThreshold: 3, minGapMs: 10 }),
311
- frontDecider: makeProgressDecider(generateProgressText),
299
+ progressNarrator: makeProgressNarrator(generateProgressText),
312
300
  });
313
301
 
314
302
  await startReleasedTurn(session, getCallbacks);
315
303
  emitToolStart(getCallbacks, "web_search", "tool-1");
316
- await waitFor(() => ttsTexts.length === 1);
317
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK]);
318
304
 
319
305
  emitToolResult(getCallbacks, "web_search", "tool-1", "found 3 results");
320
306
  emitToolStart(getCallbacks, "file_read", "tool-2");
321
307
  emitToolResult(getCallbacks, "file_read", "tool-2", "file contents");
322
- // Let the ack's minGapMs spacing elapse before the threshold-crossing op.
323
- await sleep(30);
324
308
  emitToolStart(getCallbacks, "web_search", "tool-3");
325
- await waitFor(() => ttsTexts.length === 2);
326
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK, GENERATED_NARRATION]);
309
+ await waitFor(() => ttsTexts.length === 1);
310
+ expect(ttsTexts).toEqual([GENERATED_NARRATION]);
327
311
  expect(generateProgressText).toHaveBeenCalledTimes(1);
328
312
  expect(inputs[0]).toMatchObject({
329
313
  transcriptSoFar: "hello",
@@ -339,7 +323,7 @@ describe("LiveVoiceSession progress narration", () => {
339
323
  // below the threshold: still exactly one narration.
340
324
  emitToolResult(getCallbacks, "web_search", "tool-3");
341
325
  await sleep(30);
342
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK, GENERATED_NARRATION]);
326
+ expect(ttsTexts).toEqual([GENERATED_NARRATION]);
343
327
  expect(generateProgressText).toHaveBeenCalledTimes(1);
344
328
 
345
329
  emitMessageComplete(getCallbacks);
@@ -355,12 +339,11 @@ describe("LiveVoiceSession progress narration", () => {
355
339
  });
356
340
  const { session, getCallbacks, ttsTexts } = createProgressHarness({
357
341
  frontModelConfig: progressConfig({ opsThreshold: 3, minGapMs: 10 }),
358
- frontDecider: makeProgressDecider(generateProgressText),
342
+ progressNarrator: makeProgressNarrator(generateProgressText),
359
343
  });
360
344
 
361
345
  await startReleasedTurn(session, getCallbacks);
362
346
  emitToolStart(getCallbacks, "web_search", "tool-1");
363
- await waitFor(() => ttsTexts.length === 1);
364
347
 
365
348
  // Name-only result: no toolUseId to correlate by.
366
349
  getCallbacks()?.tool_result?.({
@@ -368,11 +351,9 @@ describe("LiveVoiceSession progress narration", () => {
368
351
  resultPreview: "found it",
369
352
  });
370
353
  emitToolStart(getCallbacks, "file_read", "tool-2");
371
- // Let the ack's minGapMs spacing elapse before the threshold-crossing op.
372
- await sleep(30);
373
354
  emitToolStart(getCallbacks, "bash", "tool-3");
374
- await waitFor(() => ttsTexts.length === 2);
375
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK, GENERATED_NARRATION]);
355
+ await waitFor(() => ttsTexts.length === 1);
356
+ expect(ttsTexts).toEqual([GENERATED_NARRATION]);
376
357
 
377
358
  // The name fallback completed the web_search op: it reports as completed
378
359
  // (with its preview) rather than lingering incomplete.
@@ -384,7 +365,7 @@ describe("LiveVoiceSession progress narration", () => {
384
365
  emitMessageComplete(getCallbacks);
385
366
  });
386
367
 
387
- test("completedOps reach the decider in completion order, not start order", async () => {
368
+ test("completedOps reach the narrator in completion order, not start order", async () => {
388
369
  const inputs: VoiceProgressTextInput[] = [];
389
370
  const generateProgressText = mock(async (input: VoiceProgressTextInput) => {
390
371
  inputs.push(input);
@@ -392,21 +373,20 @@ describe("LiveVoiceSession progress narration", () => {
392
373
  });
393
374
  const { session, getCallbacks, ttsTexts } = createProgressHarness({
394
375
  frontModelConfig: progressConfig({ opsThreshold: 3, minGapMs: 10 }),
395
- frontDecider: makeProgressDecider(generateProgressText),
376
+ progressNarrator: makeProgressNarrator(generateProgressText),
396
377
  });
397
378
 
398
379
  await startReleasedTurn(session, getCallbacks);
399
380
  // Two parallel tools start in one order…
400
381
  emitToolStart(getCallbacks, "web_search", "tool-1");
401
- await waitFor(() => ttsTexts.length === 1);
402
382
  emitToolStart(getCallbacks, "file_read", "tool-2");
403
- // …and complete in the other. The sleep separates the completion
404
- // timestamps (millisecond clock) and lets the ack's minGapMs elapse.
383
+ // They complete in the other order. The sleep separates their completion
384
+ // timestamps on the millisecond clock.
405
385
  emitToolResult(getCallbacks, "file_read", "tool-2", "file contents");
406
386
  await sleep(30);
407
387
  emitToolResult(getCallbacks, "web_search", "tool-1", "found 3 results");
408
388
  emitToolStart(getCallbacks, "bash", "tool-3");
409
- await waitFor(() => ttsTexts.length === 2);
389
+ await waitFor(() => ttsTexts.length === 1);
410
390
 
411
391
  expect(inputs[0]?.completedOps).toEqual([
412
392
  { toolName: "file_read", resultPreview: "file contents" },
@@ -416,7 +396,7 @@ describe("LiveVoiceSession progress narration", () => {
416
396
  emitMessageComplete(getCallbacks);
417
397
  });
418
398
 
419
- test("idle trigger: dead air narrates with the decider text, audio-only", async () => {
399
+ test("idle trigger: dead air narrates with generated text, audio-only", async () => {
420
400
  const inputs: VoiceProgressTextInput[] = [];
421
401
  const generateProgressText = mock(async (input: VoiceProgressTextInput) => {
422
402
  inputs.push(input);
@@ -427,7 +407,7 @@ describe("LiveVoiceSession progress narration", () => {
427
407
  idleIntervalMs: 40,
428
408
  minGapMs: 60_000,
429
409
  }),
430
- frontDecider: makeProgressDecider(generateProgressText),
410
+ progressNarrator: makeProgressNarrator(generateProgressText),
431
411
  });
432
412
 
433
413
  await startReleasedTurn(session, getCallbacks);
@@ -448,14 +428,14 @@ describe("LiveVoiceSession progress narration", () => {
448
428
  emitMessageComplete(getCallbacks);
449
429
  });
450
430
 
451
- test("idle trigger with a null decider result speaks the static fallback", async () => {
431
+ test("idle trigger with a null narrator result speaks the static fallback", async () => {
452
432
  const generateProgressText = mock(async () => null);
453
433
  const { session, getCallbacks, ttsTexts } = createProgressHarness({
454
434
  frontModelConfig: progressConfig({
455
435
  idleIntervalMs: 40,
456
436
  minGapMs: 60_000,
457
437
  }),
458
- frontDecider: makeProgressDecider(generateProgressText),
438
+ progressNarrator: makeProgressNarrator(generateProgressText),
459
439
  });
460
440
 
461
441
  await startReleasedTurn(session, getCallbacks);
@@ -488,7 +468,7 @@ describe("LiveVoiceSession progress narration", () => {
488
468
  idleIntervalMs: 40,
489
469
  minGapMs: 60_000,
490
470
  }),
491
- frontDecider: makeProgressDecider(generateProgressText),
471
+ progressNarrator: makeProgressNarrator(generateProgressText),
492
472
  });
493
473
 
494
474
  await startReleasedTurn(session, getCallbacks);
@@ -500,7 +480,7 @@ describe("LiveVoiceSession progress narration", () => {
500
480
  emitMessageComplete(getCallbacks);
501
481
  });
502
482
 
503
- test("a Hindi turn's idle fallback speaks the Hindi phrase and hints the decider", async () => {
483
+ test("a Hindi turn's idle fallback speaks the Hindi phrase and hints the narrator", async () => {
504
484
  const inputs: VoiceProgressTextInput[] = [];
505
485
  const generateProgressText = mock(async (input: VoiceProgressTextInput) => {
506
486
  inputs.push(input);
@@ -511,7 +491,7 @@ describe("LiveVoiceSession progress narration", () => {
511
491
  idleIntervalMs: 40,
512
492
  minGapMs: 60_000,
513
493
  }),
514
- frontDecider: makeProgressDecider(generateProgressText),
494
+ progressNarrator: makeProgressNarrator(generateProgressText),
515
495
  sttStopEvents: [
516
496
  { type: "final", text: "नमस्ते", languages: ["hi"] },
517
497
  { type: "closed" },
@@ -522,7 +502,7 @@ describe("LiveVoiceSession progress narration", () => {
522
502
  await waitFor(() => ttsTexts.length === 1);
523
503
 
524
504
  // The static fallback rotates through the caller's language's list, not
525
- // the English default, and the decider was told the language too.
505
+ // the English default, and the narrator was told the language too.
526
506
  expect(pickProgressPhrase(0, "hi")).toBe(
527
507
  PROGRESS_FALLBACK_PHRASES_BY_LANGUAGE.hi![0]!,
528
508
  );
@@ -560,7 +540,7 @@ describe("LiveVoiceSession progress narration", () => {
560
540
  idleIntervalMs: 40,
561
541
  minGapMs: 60_000,
562
542
  }),
563
- frontDecider: makeProgressDecider(generateProgressText),
543
+ progressNarrator: makeProgressNarrator(generateProgressText),
564
544
  },
565
545
  );
566
546
 
@@ -583,41 +563,9 @@ describe("LiveVoiceSession progress narration", () => {
583
563
  }
584
564
  });
585
565
 
586
- test("the tool-start ack passes the turn's language hint to the decider", async () => {
587
- const ackInputs: VoiceAckTextInput[] = [];
588
- const frontDecider: VoiceFrontDecider = {
589
- generateAckText: async (input) => {
590
- ackInputs.push(input);
591
- return GENERATED_TOOL_ACK;
592
- },
593
- generateProgressText: async () => null,
594
- };
595
- const { session, getCallbacks, ttsTexts } = createProgressHarness({
596
- frontModelConfig: progressConfig(),
597
- frontDecider,
598
- sttStopEvents: [
599
- { type: "final", text: "नमस्ते", languages: ["hi"] },
600
- { type: "closed" },
601
- ],
602
- });
603
-
604
- await startReleasedTurn(session, getCallbacks);
605
- emitToolStart(getCallbacks, "web_search", "tool-1");
606
- await waitFor(() => ttsTexts.length === 1);
607
-
608
- expect(ackInputs[0]?.languageHint).toBe("hi");
609
-
610
- emitMessageComplete(getCallbacks);
611
- });
612
-
613
- test("with no detected language the decider inputs carry no hint and the fallback stays English", async () => {
566
+ test("with no detected language the narrator gets no hint and fallback stays English", async () => {
614
567
  const inputs: VoiceProgressTextInput[] = [];
615
- const ackInputs: VoiceAckTextInput[] = [];
616
- const frontDecider: VoiceFrontDecider = {
617
- generateAckText: async (input) => {
618
- ackInputs.push(input);
619
- return GENERATED_TOOL_ACK;
620
- },
568
+ const progressNarrator: VoiceProgressNarrator = {
621
569
  generateProgressText: async (input) => {
622
570
  inputs.push(input);
623
571
  return null;
@@ -625,24 +573,22 @@ describe("LiveVoiceSession progress narration", () => {
625
573
  };
626
574
  const { session, getCallbacks, ttsTexts } = createProgressHarness({
627
575
  frontModelConfig: progressConfig({ idleIntervalMs: 40, minGapMs: 10 }),
628
- frontDecider,
576
+ progressNarrator,
629
577
  });
630
578
 
631
579
  await startReleasedTurn(session, getCallbacks);
632
580
  emitToolStart(getCallbacks, "web_search", "tool-1");
633
581
  await waitFor(() => ttsTexts.length === 1);
634
- await waitFor(() => ttsTexts.length === 2);
635
582
 
636
- // Language unknown: the inputs are exactly the language-blind shape (no
637
- // languageHint key at all) and the static fallback is the English list.
638
- expect(ackInputs[0]).not.toHaveProperty("languageHint");
583
+ // Language unknown: the input has no languageHint key and the static
584
+ // fallback comes from the English list.
639
585
  expect(inputs[0]).not.toHaveProperty("languageHint");
640
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK, EXPECTED_PROGRESS_FALLBACK]);
586
+ expect(ttsTexts).toEqual([EXPECTED_PROGRESS_FALLBACK]);
641
587
 
642
588
  emitMessageComplete(getCallbacks);
643
589
  });
644
590
 
645
- test("ops trigger with a null decider result stays silent and keeps the update budget", async () => {
591
+ test("ops trigger with a null narrator result stays silent and keeps the update budget", async () => {
646
592
  const generateProgressText = mock(async () => null);
647
593
  const { session, getCallbacks, ttsTexts } = createProgressHarness({
648
594
  frontModelConfig: progressConfig({
@@ -650,96 +596,19 @@ describe("LiveVoiceSession progress narration", () => {
650
596
  idleIntervalMs: 120,
651
597
  minGapMs: 10,
652
598
  }),
653
- frontDecider: makeProgressDecider(generateProgressText),
599
+ progressNarrator: makeProgressNarrator(generateProgressText),
654
600
  });
655
601
 
656
602
  await startReleasedTurn(session, getCallbacks);
657
603
  emitToolStart(getCallbacks, "web_search", "tool-1");
658
- await waitFor(() => ttsTexts.length === 1);
659
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK]);
660
-
661
- // Past the ack gap, the ops trigger fires but generation returns null:
662
- // nothing speaks and no update is consumed.
663
- await sleep(30);
664
- emitToolResult(getCallbacks, "web_search", "tool-1");
665
604
  await waitFor(() => generateProgressText.mock.calls.length === 1);
666
605
  await sleep(10);
667
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK]);
606
+ expect(ttsTexts).toEqual([]);
668
607
 
669
608
  // The idle trigger still has the full budget: its own null falls back to
670
609
  // the static phrase.
671
- await waitFor(() => ttsTexts.length === 2);
672
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK, EXPECTED_PROGRESS_FALLBACK]);
673
-
674
- emitMessageComplete(getCallbacks);
675
- });
676
-
677
- test("minGapMs: a tool-start ack suppresses narration until the gap elapses", async () => {
678
- const generateProgressText = mock(async () => GENERATED_NARRATION);
679
- const { session, getCallbacks, ttsTexts } = createProgressHarness({
680
- frontModelConfig: progressConfig({ opsThreshold: 1, minGapMs: 150 }),
681
- frontDecider: makeProgressDecider(generateProgressText),
682
- });
683
-
684
- await startReleasedTurn(session, getCallbacks);
685
- emitToolStart(getCallbacks, "web_search", "tool-1");
686
610
  await waitFor(() => ttsTexts.length === 1);
687
- // Threshold already met, but the just-spoken ack holds the floor.
688
- emitToolResult(getCallbacks, "web_search", "tool-1");
689
- await sleep(30);
690
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK]);
691
- expect(generateProgressText).not.toHaveBeenCalled();
692
-
693
- await sleep(150);
694
- emitToolStart(getCallbacks, "file_read", "tool-2");
695
- await waitFor(() => ttsTexts.length === 2);
696
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK, GENERATED_NARRATION]);
697
- expect(generateProgressText).toHaveBeenCalledTimes(1);
698
-
699
- emitMessageComplete(getCallbacks);
700
- });
701
-
702
- test("no narration generation launches while an ack generation is pending", async () => {
703
- const ackGeneration = deferred<string | null>();
704
- const generateAckText = mock(() => ackGeneration.promise);
705
- const generateProgressText = mock(async () => GENERATED_NARRATION);
706
- const frontDecider: VoiceFrontDecider = {
707
- generateAckText,
708
- generateProgressText,
709
- };
710
- const { session, getCallbacks, ttsTexts } = createProgressHarness({
711
- frontModelConfig: progressConfig({ opsThreshold: 1, minGapMs: 150 }),
712
- frontDecider,
713
- });
714
-
715
- await startReleasedTurn(session, getCallbacks);
716
- // The tool start speaks a generated ack (generation still pending) and
717
- // simultaneously crosses the ops threshold; the entry guard must stand
718
- // down instead of launching a narration generation whose result the
719
- // post-await re-check would only discard.
720
- emitToolStart(getCallbacks, "web_search", "tool-1");
721
- await sleep(30);
722
- expect(generateProgressText).not.toHaveBeenCalled();
723
- expect(ttsTexts).toEqual([]);
724
-
725
- ackGeneration.resolve("Generated ack.");
726
- await waitFor(() => ttsTexts.length === 1);
727
- expect(ttsTexts).toEqual(["Generated ack."]);
728
-
729
- // Inside the ack's minGapMs window nothing else speaks even as tool
730
- // activity continues.
731
- emitToolResult(getCallbacks, "web_search", "tool-1");
732
- await sleep(30);
733
- expect(generateProgressText).not.toHaveBeenCalled();
734
- expect(ttsTexts).toEqual(["Generated ack."]);
735
-
736
- // The stand-down preserved the update budget: once the gap elapses, the
737
- // next ops trigger narrates normally.
738
- await sleep(150);
739
- emitToolStart(getCallbacks, "file_read", "tool-2");
740
- await waitFor(() => ttsTexts.length === 2);
741
- expect(ttsTexts).toEqual(["Generated ack.", GENERATED_NARRATION]);
742
- expect(generateProgressText).toHaveBeenCalledTimes(1);
611
+ expect(ttsTexts).toEqual([EXPECTED_PROGRESS_FALLBACK]);
743
612
 
744
613
  emitMessageComplete(getCallbacks);
745
614
  });
@@ -752,7 +621,7 @@ describe("LiveVoiceSession progress narration", () => {
752
621
  });
753
622
  const { session, getCallbacks, ttsTexts } = createProgressHarness({
754
623
  frontModelConfig: progressConfig({ idleIntervalMs: 30, minGapMs: 1 }),
755
- frontDecider: makeProgressDecider(generateProgressText),
624
+ progressNarrator: makeProgressNarrator(generateProgressText),
756
625
  });
757
626
 
758
627
  await startReleasedTurn(session, getCallbacks);
@@ -786,7 +655,7 @@ describe("LiveVoiceSession progress narration", () => {
786
655
  idleIntervalMs: 40,
787
656
  minGapMs: 10,
788
657
  }),
789
- frontDecider: makeProgressDecider(generateProgressText),
658
+ progressNarrator: makeProgressNarrator(generateProgressText),
790
659
  });
791
660
 
792
661
  await startReleasedTurn(session, getCallbacks);
@@ -818,7 +687,7 @@ describe("LiveVoiceSession progress narration", () => {
818
687
  idleIntervalMs: 40,
819
688
  minGapMs: 60_000,
820
689
  }),
821
- frontDecider: makeProgressDecider(generateProgressText),
690
+ progressNarrator: makeProgressNarrator(generateProgressText),
822
691
  gateTtsText: (text) => (text === "Hello there." ? ttsGate.promise : null),
823
692
  });
824
693
 
@@ -843,7 +712,7 @@ describe("LiveVoiceSession progress narration", () => {
843
712
  const generateProgressText = mock(() => generation.promise);
844
713
  const { session, getCallbacks, ttsTexts } = createProgressHarness({
845
714
  frontModelConfig: progressConfig({ idleIntervalMs: 40 }),
846
- frontDecider: makeProgressDecider(generateProgressText),
715
+ progressNarrator: makeProgressNarrator(generateProgressText),
847
716
  });
848
717
 
849
718
  await startReleasedTurn(session, getCallbacks);
@@ -864,7 +733,7 @@ describe("LiveVoiceSession progress narration", () => {
864
733
  idleIntervalMs: 40,
865
734
  minGapMs: 60_000,
866
735
  }),
867
- frontDecider: makeProgressDecider(generateProgressText),
736
+ progressNarrator: makeProgressNarrator(generateProgressText),
868
737
  emitMetrics: true,
869
738
  });
870
739
 
@@ -889,7 +758,7 @@ describe("LiveVoiceSession progress narration", () => {
889
758
  });
890
759
  });
891
760
 
892
- test("progress.enabled false: zero narrations and zero decider calls", async () => {
761
+ test("progress.enabled false: zero narrations and zero narrator calls", async () => {
893
762
  const generateProgressText = mock(async () => GENERATED_NARRATION);
894
763
  const { session, getCallbacks, ttsTexts } = createProgressHarness({
895
764
  frontModelConfig: progressConfig({
@@ -898,7 +767,7 @@ describe("LiveVoiceSession progress narration", () => {
898
767
  idleIntervalMs: 30,
899
768
  minGapMs: 1,
900
769
  }),
901
- frontDecider: makeProgressDecider(generateProgressText),
770
+ progressNarrator: makeProgressNarrator(generateProgressText),
902
771
  });
903
772
 
904
773
  await startReleasedTurn(session, getCallbacks);
@@ -908,8 +777,7 @@ describe("LiveVoiceSession progress narration", () => {
908
777
  emitToolResult(getCallbacks, "file_read", "tool-2");
909
778
  await sleep(100);
910
779
 
911
- // Only the tool-start ack spoke; the decider was never consulted.
912
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK]);
780
+ expect(ttsTexts).toEqual([]);
913
781
  expect(generateProgressText).not.toHaveBeenCalled();
914
782
 
915
783
  emitMessageComplete(getCallbacks);
@@ -922,7 +790,7 @@ describe("LiveVoiceSession progress narration", () => {
922
790
  idleIntervalMs: 20,
923
791
  maxSilenceMs: 150,
924
792
  }),
925
- frontDecider: makeProgressDecider(generateProgressText),
793
+ progressNarrator: makeProgressNarrator(generateProgressText),
926
794
  });
927
795
 
928
796
  await startReleasedTurn(session, getCallbacks);
@@ -949,7 +817,7 @@ describe("LiveVoiceSession progress narration", () => {
949
817
  idleIntervalMs: 20,
950
818
  maxSilenceMs: 600_000,
951
819
  }),
952
- frontDecider: makeProgressDecider(generateProgressText),
820
+ progressNarrator: makeProgressNarrator(generateProgressText),
953
821
  });
954
822
 
955
823
  await startReleasedTurn(session, getCallbacks);
@@ -958,14 +826,14 @@ describe("LiveVoiceSession progress narration", () => {
958
826
 
959
827
  // A tool starting is news: the next tick narrates it.
960
828
  emitToolStart(getCallbacks, "web_search", "tool-1");
961
- await waitFor(() => ttsTexts.length === 2);
962
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK, GENERATED_NARRATION]);
829
+ await waitFor(() => ttsTexts.length === 1);
830
+ expect(ttsTexts).toEqual([GENERATED_NARRATION]);
963
831
 
964
832
  // Nothing has happened since, so the ticks that follow stay quiet
965
833
  // instead of repeating the same update on a clock.
966
834
  await sleep(80);
967
835
  expect(generateProgressText).toHaveBeenCalledTimes(1);
968
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK, GENERATED_NARRATION]);
836
+ expect(ttsTexts).toEqual([GENERATED_NARRATION]);
969
837
 
970
838
  emitMessageComplete(getCallbacks);
971
839
  });
@@ -984,17 +852,16 @@ describe("LiveVoiceSession progress narration", () => {
984
852
  idleIntervalMs: 60_000,
985
853
  longOpMs: 25,
986
854
  }),
987
- frontDecider: makeProgressDecider(generateProgressText),
855
+ progressNarrator: makeProgressNarrator(generateProgressText),
988
856
  });
989
857
 
990
858
  await startReleasedTurn(session, getCallbacks);
991
859
  emitToolStart(getCallbacks, "web_search", "tool-1");
992
- await waitFor(() => ttsTexts.length === 1);
993
860
  await sleep(40);
994
861
 
995
862
  emitToolResult(getCallbacks, "web_search", "tool-1");
996
- await waitFor(() => ttsTexts.length === 2);
997
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK, GENERATED_NARRATION]);
863
+ await waitFor(() => ttsTexts.length === 1);
864
+ expect(ttsTexts).toEqual([GENERATED_NARRATION]);
998
865
  expect(inputs[0]?.completedOps).toEqual([
999
866
  { toolName: "web_search", resultPreview: "ok" },
1000
867
  ]);
@@ -1010,18 +877,17 @@ describe("LiveVoiceSession progress narration", () => {
1010
877
  idleIntervalMs: 60_000,
1011
878
  longOpMs: 60_000,
1012
879
  }),
1013
- frontDecider: makeProgressDecider(generateProgressText),
880
+ progressNarrator: makeProgressNarrator(generateProgressText),
1014
881
  });
1015
882
 
1016
883
  await startReleasedTurn(session, getCallbacks);
1017
884
  emitToolStart(getCallbacks, "web_search", "tool-1");
1018
- await waitFor(() => ttsTexts.length === 1);
1019
885
  emitToolResult(getCallbacks, "web_search", "tool-1");
1020
886
  await sleep(60);
1021
887
 
1022
888
  // Narrating every quick lookup is the chatter the cadence avoids.
1023
889
  expect(generateProgressText).not.toHaveBeenCalled();
1024
- expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK]);
890
+ expect(ttsTexts).toEqual([]);
1025
891
 
1026
892
  emitMessageComplete(getCallbacks);
1027
893
  });
@@ -1041,7 +907,7 @@ describe("LiveVoiceSession progress narration", () => {
1041
907
  idleIntervalMs: 20,
1042
908
  maxSilenceMs: 600_000,
1043
909
  }),
1044
- frontDecider: makeProgressDecider(generateProgressText),
910
+ progressNarrator: makeProgressNarrator(generateProgressText),
1045
911
  });
1046
912
 
1047
913
  await startReleasedTurn(session, getCallbacks);
@@ -261,6 +261,20 @@ describe("LiveVoiceSession STT", () => {
261
261
  ]);
262
262
  });
263
263
 
264
+ test("ignores model-integrated turn-detection events", async () => {
265
+ const { frames, session, transcriber } = createSessionWithTranscriber();
266
+
267
+ await session.start();
268
+ transcriber.emit({ type: "turn-start" });
269
+ transcriber.emit({ type: "eager-turn-end", text: "eager" });
270
+ transcriber.emit({ type: "turn-resumed" });
271
+ transcriber.emit({ type: "turn-end", text: "committed", confidence: 0.9 });
272
+ await new Promise((resolve) => setTimeout(resolve, 10));
273
+
274
+ expect(frames.map((frame) => frame.type)).toEqual(["ready"]);
275
+ expect(session.finalTranscriptText).toBe("");
276
+ });
277
+
264
278
  test("marks transient transcriber errors recoverable while the session continues", async () => {
265
279
  const { frames, session, transcriber } = createSessionWithTranscriber();
266
280
 
@@ -201,6 +201,35 @@ describe("live-voice triage-and-escalate routing", () => {
201
201
  expect(escalated?.content).toBe(ESCALATION_CONTINUATION_CONTENT);
202
202
  });
203
203
 
204
+ test("no leg is told to refuse setup flows, and the escalated leg is told to run them", async () => {
205
+ const { starter } = scriptedStartVoiceTurn({
206
+ frontDoor: ["[1] ", "Let me think about that."],
207
+ escalated: ["The detailed answer is 42."],
208
+ });
209
+ const { frames, session } = createHarness(starter);
210
+
211
+ await driveTurn(session);
212
+ await waitFor(() => starter.mock.calls.length >= 2);
213
+ await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
214
+
215
+ const frontDoorPrompt =
216
+ starter.mock.calls[0]?.[0]?.voiceControlPrompt ?? "";
217
+ const escalatedPrompt =
218
+ starter.mock.calls[1]?.[0]?.voiceControlPrompt ?? "";
219
+ // The phone's no-setup-flows rule must not leak onto a channel that has a
220
+ // screen: it contradicts the base prompt's "never tell the user you cannot
221
+ // show them something".
222
+ expect(frontDoorPrompt).not.toContain("Never start account connections");
223
+ expect(escalatedPrompt).not.toContain("Never start account connections");
224
+ expect(frontDoorPrompt).not.toContain("finish it in text chat");
225
+ expect(escalatedPrompt).not.toContain("finish it in text chat");
226
+ // The leg that can actually run one is told to.
227
+ expect(escalatedPrompt).toContain("This includes connecting accounts");
228
+ // The toolless fast leg is not: a connection is not its to run, and its
229
+ // capability digest already routes anything needing a tool here.
230
+ expect(frontDoorPrompt).not.toContain("This includes connecting accounts");
231
+ });
232
+
204
233
  test("the screen-reveal teaching reaches the escalated leg's prompt but never the front-door leg's", async () => {
205
234
  const { starter } = scriptedStartVoiceTurn({
206
235
  frontDoor: ["[1] ", "Let me think about that."],