vellum 0.2.0 → 0.2.2

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 (361) hide show
  1. package/README.md +15 -2
  2. package/bun.lock +5 -2
  3. package/package.json +4 -2
  4. package/scripts/capture-x-graphql.ts +562 -0
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
  6. package/scripts/test.sh +5 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +161 -34
  8. package/src/__tests__/account-registry.test.ts +2 -1
  9. package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
  10. package/src/__tests__/app-bundler.test.ts +12 -33
  11. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  12. package/src/__tests__/asset-search-tool.test.ts +23 -22
  13. package/src/__tests__/attachments-store.test.ts +56 -127
  14. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  15. package/src/__tests__/browser-skill-endstate.test.ts +5 -8
  16. package/src/__tests__/call-bridge.test.ts +385 -0
  17. package/src/__tests__/call-constants.test.ts +40 -0
  18. package/src/__tests__/call-orchestrator.test.ts +454 -0
  19. package/src/__tests__/call-recovery.test.ts +518 -0
  20. package/src/__tests__/call-routes-http.test.ts +459 -0
  21. package/src/__tests__/call-state-machine.test.ts +143 -0
  22. package/src/__tests__/call-state.test.ts +133 -0
  23. package/src/__tests__/call-store.test.ts +691 -0
  24. package/src/__tests__/cli-discover.test.ts +1 -1
  25. package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
  26. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  27. package/src/__tests__/computer-use-tools.test.ts +250 -0
  28. package/src/__tests__/config-schema.test.ts +348 -3
  29. package/src/__tests__/conflict-store.test.ts +2 -1
  30. package/src/__tests__/contacts-tools.test.ts +331 -0
  31. package/src/__tests__/conversation-store.test.ts +30 -32
  32. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  33. package/src/__tests__/date-context.test.ts +373 -0
  34. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  35. package/src/__tests__/doordash-session.test.ts +9 -0
  36. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  37. package/src/__tests__/followup-tools.test.ts +303 -0
  38. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  39. package/src/__tests__/intent-routing.test.ts +64 -57
  40. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  41. package/src/__tests__/ipc-snapshot.test.ts +96 -28
  42. package/src/__tests__/llm-usage-store.test.ts +3 -8
  43. package/src/__tests__/media-generate-image.test.ts +1 -1
  44. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  45. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  46. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  47. package/src/__tests__/playbook-tools.test.ts +342 -0
  48. package/src/__tests__/profile-compiler.test.ts +2 -1
  49. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  50. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  51. package/src/__tests__/recurrence-engine.test.ts +69 -0
  52. package/src/__tests__/recurrence-types.test.ts +71 -0
  53. package/src/__tests__/registry.test.ts +17 -10
  54. package/src/__tests__/relay-server.test.ts +633 -0
  55. package/src/__tests__/reminder-store.test.ts +6 -3
  56. package/src/__tests__/reminder.test.ts +43 -77
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +222 -0
  58. package/src/__tests__/run-orchestrator.test.ts +7 -7
  59. package/src/__tests__/runtime-attachment-metadata.test.ts +19 -20
  60. package/src/__tests__/runtime-runs-http.test.ts +5 -23
  61. package/src/__tests__/runtime-runs.test.ts +11 -11
  62. package/src/__tests__/schedule-store.test.ts +482 -0
  63. package/src/__tests__/schedule-tools.test.ts +700 -0
  64. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  65. package/src/__tests__/server-history-render.test.ts +14 -13
  66. package/src/__tests__/session-error.test.ts +28 -0
  67. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  68. package/src/__tests__/session-queue.test.ts +89 -16
  69. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  70. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  71. package/src/__tests__/signup-e2e.test.ts +2 -1
  72. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  73. package/src/__tests__/skill-script-runner.test.ts +159 -0
  74. package/src/__tests__/speaker-identification.test.ts +52 -0
  75. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  76. package/src/__tests__/subagent-tools.test.ts +141 -41
  77. package/src/__tests__/task-compiler.test.ts +2 -1
  78. package/src/__tests__/task-runner.test.ts +2 -1
  79. package/src/__tests__/task-scheduler.test.ts +2 -1
  80. package/src/__tests__/task-tools.test.ts +49 -56
  81. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  82. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  84. package/src/__tests__/tool-executor.test.ts +13 -17
  85. package/src/__tests__/turn-commit.test.ts +273 -2
  86. package/src/__tests__/twilio-provider.test.ts +143 -0
  87. package/src/__tests__/twilio-routes.test.ts +789 -0
  88. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  89. package/src/__tests__/view-image-tool.test.ts +217 -0
  90. package/src/__tests__/workspace-git-service.test.ts +403 -0
  91. package/src/__tests__/workspace-heartbeat-service.test.ts +141 -2
  92. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  93. package/src/bundler/app-bundler.ts +35 -14
  94. package/src/calls/call-bridge.ts +95 -0
  95. package/src/calls/call-constants.ts +48 -0
  96. package/src/calls/call-domain.ts +276 -0
  97. package/src/calls/call-orchestrator.ts +390 -0
  98. package/src/calls/call-recovery.ts +207 -0
  99. package/src/calls/call-state-machine.ts +68 -0
  100. package/src/calls/call-state.ts +64 -0
  101. package/src/calls/call-store.ts +416 -0
  102. package/src/calls/relay-server.ts +335 -0
  103. package/src/calls/speaker-identification.ts +213 -0
  104. package/src/calls/twilio-config.ts +34 -0
  105. package/src/calls/twilio-provider.ts +173 -0
  106. package/src/calls/twilio-routes.ts +250 -0
  107. package/src/calls/types.ts +37 -0
  108. package/src/calls/voice-provider.ts +14 -0
  109. package/src/cli/config-commands.ts +334 -0
  110. package/src/cli/core-commands.ts +776 -0
  111. package/src/cli/doordash.ts +256 -25
  112. package/src/cli/ipc-client.ts +82 -0
  113. package/src/cli/map.ts +246 -0
  114. package/src/cli/twitter.ts +575 -0
  115. package/src/cli.ts +7 -5
  116. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  117. package/src/commands/cc-command-registry.ts +209 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  119. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  120. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  121. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  122. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  123. package/src/config/bundled-skills/document/SKILL.md +18 -0
  124. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  125. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  126. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  127. package/src/config/bundled-skills/doordash/SKILL.md +163 -0
  128. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  129. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  130. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  131. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  132. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  133. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  134. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -24
  135. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  136. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  137. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  138. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  139. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  140. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  141. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  142. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  143. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  144. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  145. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  146. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  149. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  150. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  151. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  152. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  153. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  154. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  155. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  156. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  157. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  158. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  159. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  160. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  161. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  162. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  163. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  164. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  165. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  166. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  167. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  168. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  169. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  170. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  171. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  172. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  173. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  174. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  175. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  176. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  177. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  178. package/src/config/defaults.ts +44 -0
  179. package/src/config/loader.ts +4 -1
  180. package/src/config/schema.ts +218 -1
  181. package/src/config/system-prompt.ts +100 -6
  182. package/src/config/templates/IDENTITY.md +7 -0
  183. package/src/config/types.ts +5 -0
  184. package/src/contacts/contact-store.ts +4 -4
  185. package/src/daemon/assistant-attachments.ts +10 -0
  186. package/src/daemon/classifier.ts +3 -1
  187. package/src/daemon/computer-use-session.ts +3 -1
  188. package/src/daemon/date-context.ts +136 -0
  189. package/src/daemon/handlers/apps.ts +16 -1
  190. package/src/daemon/handlers/browser.ts +54 -0
  191. package/src/daemon/handlers/computer-use.ts +7 -1
  192. package/src/daemon/handlers/config.ts +192 -4
  193. package/src/daemon/handlers/diagnostics.ts +5 -1
  194. package/src/daemon/handlers/documents.ts +18 -29
  195. package/src/daemon/handlers/home-base.ts +5 -1
  196. package/src/daemon/handlers/index.ts +40 -271
  197. package/src/daemon/handlers/misc.ts +9 -1
  198. package/src/daemon/handlers/publish.ts +6 -1
  199. package/src/daemon/handlers/sessions.ts +65 -12
  200. package/src/daemon/handlers/shared.ts +36 -1
  201. package/src/daemon/handlers/signing.ts +37 -0
  202. package/src/daemon/handlers/skills.ts +20 -6
  203. package/src/daemon/handlers/subagents.ts +8 -3
  204. package/src/daemon/handlers/twitter-auth.ts +169 -0
  205. package/src/daemon/handlers/work-items.ts +495 -39
  206. package/src/daemon/ipc-contract-inventory.json +40 -4
  207. package/src/daemon/ipc-contract.ts +185 -37
  208. package/src/daemon/ipc-protocol.ts +7 -2
  209. package/src/daemon/lifecycle.ts +48 -5
  210. package/src/daemon/main.ts +10 -4
  211. package/src/daemon/ride-shotgun-handler.ts +74 -10
  212. package/src/daemon/server.ts +144 -29
  213. package/src/daemon/session-agent-loop.ts +887 -0
  214. package/src/daemon/session-attachments.ts +28 -5
  215. package/src/daemon/session-error.ts +24 -3
  216. package/src/daemon/session-lifecycle.ts +147 -0
  217. package/src/daemon/session-media-retry.ts +147 -0
  218. package/src/daemon/session-messaging.ts +145 -0
  219. package/src/daemon/session-notifiers.ts +164 -0
  220. package/src/daemon/session-process.ts +2 -2
  221. package/src/daemon/session-queue-manager.ts +1 -0
  222. package/src/daemon/session-runtime-assembly.ts +52 -0
  223. package/src/daemon/session-skill-tools.ts +124 -5
  224. package/src/daemon/session-slash.ts +3 -0
  225. package/src/daemon/session-surfaces.ts +77 -2
  226. package/src/daemon/session-tool-setup.ts +222 -2
  227. package/src/daemon/session-usage.ts +0 -2
  228. package/src/daemon/session.ts +114 -1365
  229. package/src/daemon/video-thumbnail.ts +60 -0
  230. package/src/doordash/client.ts +121 -27
  231. package/src/doordash/queries.ts +1 -2
  232. package/src/export/formatter.ts +3 -1
  233. package/src/followups/followup-store.ts +4 -2
  234. package/src/followups/types.ts +6 -0
  235. package/src/hooks/templates.ts +1 -1
  236. package/src/index.ts +32 -1151
  237. package/src/media/gemini-image-service.ts +1 -1
  238. package/src/memory/attachments-store.ts +28 -83
  239. package/src/memory/channel-delivery-store.ts +7 -21
  240. package/src/memory/clarification-resolver.ts +6 -5
  241. package/src/memory/contradiction-checker.ts +3 -2
  242. package/src/memory/conversation-key-store.ts +10 -29
  243. package/src/memory/conversation-store.ts +2 -1
  244. package/src/memory/db.ts +362 -2
  245. package/src/memory/entity-extractor.ts +6 -3
  246. package/src/memory/items-extractor.ts +5 -4
  247. package/src/memory/jobs-store.ts +3 -2
  248. package/src/memory/llm-usage-store.ts +1 -2
  249. package/src/memory/runs-store.ts +1 -2
  250. package/src/memory/schema.ts +65 -2
  251. package/src/messaging/style-analyzer.ts +3 -2
  252. package/src/messaging/thread-summarizer.ts +8 -12
  253. package/src/messaging/triage-engine.ts +4 -2
  254. package/src/providers/openrouter/client.ts +20 -0
  255. package/src/providers/registry.ts +8 -0
  256. package/src/runtime/http-server.ts +277 -25
  257. package/src/runtime/http-types.ts +0 -2
  258. package/src/runtime/routes/attachment-routes.ts +5 -6
  259. package/src/runtime/routes/call-routes.ts +140 -0
  260. package/src/runtime/routes/channel-routes.ts +12 -19
  261. package/src/runtime/routes/conversation-routes.ts +5 -9
  262. package/src/runtime/routes/run-routes.ts +4 -8
  263. package/src/runtime/run-orchestrator.ts +39 -6
  264. package/src/schedule/recurrence-engine.ts +138 -0
  265. package/src/schedule/recurrence-types.ts +67 -0
  266. package/src/schedule/schedule-store.ts +102 -57
  267. package/src/schedule/scheduler.ts +9 -6
  268. package/src/security/oauth2.ts +29 -4
  269. package/src/security/secret-allowlist.ts +46 -0
  270. package/src/skills/clawhub.ts +1 -1
  271. package/src/subagent/manager.ts +40 -8
  272. package/src/swarm/backend-claude-code.ts +64 -9
  273. package/src/swarm/worker-prompts.ts +2 -1
  274. package/src/tasks/SPEC.md +34 -28
  275. package/src/tasks/ephemeral-permissions.ts +16 -7
  276. package/src/tasks/task-compiler.ts +5 -4
  277. package/src/tasks/task-runner.ts +10 -5
  278. package/src/tasks/task-scheduler.ts +1 -1
  279. package/src/tasks/tool-sanitizer.ts +36 -0
  280. package/src/tools/assets/search.ts +4 -4
  281. package/src/tools/browser/api-map.ts +220 -0
  282. package/src/tools/browser/auto-navigate.ts +270 -0
  283. package/src/tools/browser/browser-execution.ts +2 -1
  284. package/src/tools/browser/browser-manager.ts +2 -2
  285. package/src/tools/browser/network-recorder.ts +5 -4
  286. package/src/tools/browser/x-auto-navigate.ts +207 -0
  287. package/src/tools/calls/call-end.ts +67 -0
  288. package/src/tools/calls/call-start.ts +73 -0
  289. package/src/tools/calls/call-status.ts +81 -0
  290. package/src/tools/claude-code/claude-code.ts +77 -11
  291. package/src/tools/contacts/contact-merge.ts +46 -78
  292. package/src/tools/contacts/contact-search.ts +35 -79
  293. package/src/tools/contacts/contact-upsert.ts +35 -108
  294. package/src/tools/credentials/vault.ts +21 -5
  295. package/src/tools/document/document-tool.ts +71 -144
  296. package/src/tools/executor.ts +129 -10
  297. package/src/tools/followups/followup_create.ts +46 -88
  298. package/src/tools/followups/followup_list.ts +34 -74
  299. package/src/tools/followups/followup_resolve.ts +31 -66
  300. package/src/tools/host-terminal/cli-discover.ts +2 -1
  301. package/src/tools/host-terminal/host-shell.ts +10 -0
  302. package/src/tools/memory/handlers.ts +5 -4
  303. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  304. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  305. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  306. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  307. package/src/tools/network/web-fetch.ts +18 -6
  308. package/src/tools/playbooks/index.ts +4 -5
  309. package/src/tools/playbooks/playbook-create.ts +3 -47
  310. package/src/tools/playbooks/playbook-delete.ts +1 -25
  311. package/src/tools/playbooks/playbook-list.ts +1 -28
  312. package/src/tools/playbooks/playbook-update.ts +3 -51
  313. package/src/tools/registry.ts +2 -4
  314. package/src/tools/reminder/reminder.ts +5 -78
  315. package/src/tools/schedule/create.ts +69 -74
  316. package/src/tools/schedule/delete.ts +21 -47
  317. package/src/tools/schedule/list.ts +55 -74
  318. package/src/tools/schedule/update.ts +77 -84
  319. package/src/tools/subagent/abort.ts +29 -58
  320. package/src/tools/subagent/message.ts +30 -63
  321. package/src/tools/subagent/read.ts +53 -84
  322. package/src/tools/subagent/spawn.ts +43 -82
  323. package/src/tools/subagent/status.ts +42 -71
  324. package/src/tools/swarm/delegate.ts +2 -1
  325. package/src/tools/tasks/index.ts +8 -6
  326. package/src/tools/tasks/task-delete.ts +69 -56
  327. package/src/tools/tasks/task-list.ts +31 -52
  328. package/src/tools/tasks/task-run.ts +74 -102
  329. package/src/tools/tasks/task-save.ts +33 -65
  330. package/src/tools/tasks/work-item-enqueue.ts +192 -134
  331. package/src/tools/tasks/work-item-list.ts +33 -78
  332. package/src/tools/tasks/work-item-remove.ts +60 -0
  333. package/src/tools/tasks/work-item-update.ts +114 -0
  334. package/src/tools/terminal/backends/native.ts +3 -1
  335. package/src/tools/tool-manifest.ts +20 -74
  336. package/src/tools/types.ts +6 -0
  337. package/src/tools/ui-surface/definitions.ts +6 -1
  338. package/src/tools/watch/screen-watch.ts +3 -1
  339. package/src/tools/watcher/create.ts +52 -98
  340. package/src/tools/watcher/delete.ts +20 -46
  341. package/src/tools/watcher/digest.ts +36 -70
  342. package/src/tools/watcher/list.ts +49 -79
  343. package/src/tools/watcher/update.ts +45 -91
  344. package/src/twitter/client.ts +690 -0
  345. package/src/twitter/session.ts +91 -0
  346. package/src/usage/types.ts +0 -1
  347. package/src/util/truncate.ts +6 -0
  348. package/src/watcher/providers/slack.ts +2 -1
  349. package/src/watcher/watcher-store.ts +3 -2
  350. package/src/work-items/work-item-store.ts +236 -2
  351. package/src/workspace/commit-message-enrichment-service.ts +284 -0
  352. package/src/workspace/commit-message-provider.ts +95 -0
  353. package/src/workspace/git-service.ts +272 -52
  354. package/src/workspace/heartbeat-service.ts +70 -13
  355. package/src/workspace/provider-commit-message-generator.ts +242 -0
  356. package/src/workspace/turn-commit.ts +100 -51
  357. package/src/tools/contacts/index.ts +0 -4
  358. package/src/tools/document/index.ts +0 -5
  359. package/src/tools/followups/index.ts +0 -3
  360. package/src/tools/subagent/index.ts +0 -5
  361. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
@@ -0,0 +1,335 @@
1
+ /**
2
+ * WebSocket handler for Twilio ConversationRelay protocol.
3
+ *
4
+ * Manages real-time voice conversations over WebSocket. Each active call
5
+ * has a single RelayConnection instance that processes inbound messages
6
+ * from Twilio and can send text tokens back for TTS.
7
+ */
8
+
9
+ import type { ServerWebSocket } from 'bun';
10
+ import { getLogger } from '../util/logger.js';
11
+ import {
12
+ getCallSession,
13
+ updateCallSession,
14
+ recordCallEvent,
15
+ } from './call-store.js';
16
+ import { CallOrchestrator } from './call-orchestrator.js';
17
+ import {
18
+ extractPromptSpeakerMetadata,
19
+ SpeakerIdentityTracker,
20
+ type PromptSpeakerContext,
21
+ } from './speaker-identification.js';
22
+
23
+ const log = getLogger('relay-server');
24
+
25
+ // ── ConversationRelay message types ──────────────────────────────────
26
+
27
+ // Messages FROM Twilio
28
+ export interface RelaySetupMessage {
29
+ type: 'setup';
30
+ callSid: string;
31
+ from: string;
32
+ to: string;
33
+ customParameters?: Record<string, string>;
34
+ }
35
+
36
+ export interface RelayPromptMessage {
37
+ type: 'prompt';
38
+ voicePrompt: string;
39
+ lang: string;
40
+ last: boolean;
41
+ speakerId?: string;
42
+ speakerLabel?: string;
43
+ speakerName?: string;
44
+ speakerConfidence?: number;
45
+ participantId?: string;
46
+ participant?: {
47
+ id?: string;
48
+ name?: string;
49
+ };
50
+ speaker?: {
51
+ id?: string;
52
+ label?: string;
53
+ name?: string;
54
+ confidence?: number;
55
+ };
56
+ metadata?: Record<string, unknown>;
57
+ providerMetadata?: Record<string, unknown>;
58
+ }
59
+
60
+ export interface RelayInterruptMessage {
61
+ type: 'interrupt';
62
+ utteranceUntilInterrupt: string;
63
+ }
64
+
65
+ export interface RelayDtmfMessage {
66
+ type: 'dtmf';
67
+ digit: string;
68
+ }
69
+
70
+ export interface RelayErrorMessage {
71
+ type: 'error';
72
+ description: string;
73
+ }
74
+
75
+ export type RelayInboundMessage =
76
+ | RelaySetupMessage
77
+ | RelayPromptMessage
78
+ | RelayInterruptMessage
79
+ | RelayDtmfMessage
80
+ | RelayErrorMessage;
81
+
82
+ // Messages TO Twilio
83
+ export interface RelayTextMessage {
84
+ type: 'text';
85
+ token: string;
86
+ last: boolean;
87
+ }
88
+
89
+ export interface RelayEndMessage {
90
+ type: 'end';
91
+ handoffData?: string;
92
+ }
93
+
94
+ // ── WebSocket data type ──────────────────────────────────────────────
95
+
96
+ export interface RelayWebSocketData {
97
+ callSessionId: string;
98
+ }
99
+
100
+ // ── Module-level state ───────────────────────────────────────────────
101
+
102
+ /** Active relay connections keyed by callSessionId. */
103
+ export const activeRelayConnections = new Map<string, RelayConnection>();
104
+
105
+ // ── RelayConnection ──────────────────────────────────────────────────
106
+
107
+ /**
108
+ * Manages a single WebSocket connection for one call.
109
+ */
110
+ export class RelayConnection {
111
+ private ws: ServerWebSocket<RelayWebSocketData>;
112
+ private callSessionId: string;
113
+ private conversationHistory: Array<{
114
+ role: 'caller' | 'assistant';
115
+ text: string;
116
+ timestamp: number;
117
+ speaker?: PromptSpeakerContext;
118
+ }>;
119
+ private abortController: AbortController;
120
+ private orchestrator: CallOrchestrator | null = null;
121
+ private speakerIdentityTracker: SpeakerIdentityTracker;
122
+
123
+ constructor(ws: ServerWebSocket<RelayWebSocketData>, callSessionId: string) {
124
+ this.ws = ws;
125
+ this.callSessionId = callSessionId;
126
+ this.conversationHistory = [];
127
+ this.abortController = new AbortController();
128
+ this.speakerIdentityTracker = new SpeakerIdentityTracker();
129
+ }
130
+
131
+ /**
132
+ * Handle an inbound message from Twilio via the ConversationRelay WebSocket.
133
+ */
134
+ async handleMessage(data: string): Promise<void> {
135
+ let parsed: RelayInboundMessage;
136
+ try {
137
+ parsed = JSON.parse(data) as RelayInboundMessage;
138
+ } catch {
139
+ log.warn({ callSessionId: this.callSessionId, data }, 'Failed to parse relay message');
140
+ return;
141
+ }
142
+
143
+ switch (parsed.type) {
144
+ case 'setup':
145
+ await this.handleSetup(parsed);
146
+ break;
147
+ case 'prompt':
148
+ await this.handlePrompt(parsed);
149
+ break;
150
+ case 'interrupt':
151
+ this.handleInterrupt(parsed);
152
+ break;
153
+ case 'dtmf':
154
+ this.handleDtmf(parsed);
155
+ break;
156
+ case 'error':
157
+ this.handleError(parsed);
158
+ break;
159
+ default:
160
+ log.warn({ callSessionId: this.callSessionId, type: (parsed as Record<string, unknown>).type }, 'Unknown relay message type');
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Send a text token to the caller for TTS playback.
166
+ */
167
+ sendTextToken(token: string, last: boolean): void {
168
+ const message: RelayTextMessage = { type: 'text', token, last };
169
+ try {
170
+ this.ws.send(JSON.stringify(message));
171
+ } catch (err) {
172
+ log.error({ err, callSessionId: this.callSessionId }, 'Failed to send text token');
173
+ }
174
+ }
175
+
176
+ /**
177
+ * End the ConversationRelay session.
178
+ */
179
+ endSession(reason?: string): void {
180
+ const message: RelayEndMessage = { type: 'end' };
181
+ if (reason) {
182
+ message.handoffData = JSON.stringify({ reason });
183
+ }
184
+ try {
185
+ this.ws.send(JSON.stringify(message));
186
+ } catch (err) {
187
+ log.error({ err, callSessionId: this.callSessionId }, 'Failed to send end message');
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Get the conversation history for context.
193
+ */
194
+ getConversationHistory(): Array<{ role: string; text: string; speaker?: PromptSpeakerContext }> {
195
+ return this.conversationHistory.map(({ role, text, speaker }) => ({ role, text, speaker }));
196
+ }
197
+
198
+ /**
199
+ * Get the call session ID for this connection.
200
+ */
201
+ getCallSessionId(): string {
202
+ return this.callSessionId;
203
+ }
204
+
205
+ /**
206
+ * Set the orchestrator for this connection.
207
+ */
208
+ setOrchestrator(orchestrator: CallOrchestrator): void {
209
+ this.orchestrator = orchestrator;
210
+ }
211
+
212
+ /**
213
+ * Get the orchestrator for this connection.
214
+ */
215
+ getOrchestrator(): CallOrchestrator | null {
216
+ return this.orchestrator;
217
+ }
218
+
219
+ /**
220
+ * Clean up resources on disconnect.
221
+ */
222
+ destroy(): void {
223
+ if (this.orchestrator) {
224
+ this.orchestrator.destroy();
225
+ this.orchestrator = null;
226
+ }
227
+ this.abortController.abort();
228
+ log.info({ callSessionId: this.callSessionId }, 'RelayConnection destroyed');
229
+ }
230
+
231
+ // ── Private handlers ─────────────────────────────────────────────
232
+
233
+ private async handleSetup(msg: RelaySetupMessage): Promise<void> {
234
+ log.info(
235
+ { callSessionId: this.callSessionId, callSid: msg.callSid, from: msg.from, to: msg.to },
236
+ 'ConversationRelay setup received',
237
+ );
238
+
239
+ // Store the callSid association on the call session
240
+ const session = getCallSession(this.callSessionId);
241
+ if (session) {
242
+ updateCallSession(this.callSessionId, { providerCallSid: msg.callSid });
243
+ }
244
+
245
+ recordCallEvent(this.callSessionId, 'call_connected', {
246
+ callSid: msg.callSid,
247
+ from: msg.from,
248
+ to: msg.to,
249
+ customParameters: msg.customParameters,
250
+ });
251
+
252
+ // Create and attach the LLM-driven orchestrator
253
+ const orchestrator = new CallOrchestrator(this.callSessionId, this, session?.task ?? null);
254
+ this.setOrchestrator(orchestrator);
255
+ }
256
+
257
+ private async handlePrompt(msg: RelayPromptMessage): Promise<void> {
258
+ if (!msg.last) {
259
+ // Partial transcript, wait for final
260
+ return;
261
+ }
262
+
263
+ log.info(
264
+ { callSessionId: this.callSessionId, transcript: msg.voicePrompt, lang: msg.lang },
265
+ 'Caller transcript received (final)',
266
+ );
267
+
268
+ const speakerMetadata = extractPromptSpeakerMetadata(msg as unknown as Record<string, unknown>);
269
+ const speaker = this.speakerIdentityTracker.identifySpeaker(speakerMetadata);
270
+
271
+ // Record in conversation history
272
+ this.conversationHistory.push({
273
+ role: 'caller',
274
+ text: msg.voicePrompt,
275
+ timestamp: Date.now(),
276
+ speaker,
277
+ });
278
+
279
+ // Record event
280
+ recordCallEvent(this.callSessionId, 'caller_spoke', {
281
+ transcript: msg.voicePrompt,
282
+ lang: msg.lang,
283
+ speakerId: speaker.speakerId,
284
+ speakerLabel: speaker.speakerLabel,
285
+ speakerConfidence: speaker.speakerConfidence,
286
+ speakerSource: speaker.source,
287
+ });
288
+
289
+ // Route to orchestrator for LLM-driven response
290
+ if (this.orchestrator) {
291
+ await this.orchestrator.handleCallerUtterance(msg.voicePrompt, speaker);
292
+ } else {
293
+ // Fallback if orchestrator not yet initialized
294
+ this.sendTextToken('I\'m still setting up. Please hold.', true);
295
+ }
296
+ }
297
+
298
+ private handleInterrupt(msg: RelayInterruptMessage): void {
299
+ log.info(
300
+ { callSessionId: this.callSessionId, utteranceUntilInterrupt: msg.utteranceUntilInterrupt },
301
+ 'Caller interrupted assistant',
302
+ );
303
+
304
+ // Abort any in-flight processing
305
+ this.abortController.abort();
306
+ this.abortController = new AbortController();
307
+
308
+ // Notify the orchestrator of the interruption
309
+ if (this.orchestrator) {
310
+ this.orchestrator.handleInterrupt();
311
+ }
312
+ }
313
+
314
+ private handleDtmf(msg: RelayDtmfMessage): void {
315
+ log.info(
316
+ { callSessionId: this.callSessionId, digit: msg.digit },
317
+ 'DTMF digit received',
318
+ );
319
+
320
+ recordCallEvent(this.callSessionId, 'caller_spoke', {
321
+ dtmfDigit: msg.digit,
322
+ });
323
+ }
324
+
325
+ private handleError(msg: RelayErrorMessage): void {
326
+ log.error(
327
+ { callSessionId: this.callSessionId, description: msg.description },
328
+ 'ConversationRelay error',
329
+ );
330
+
331
+ recordCallEvent(this.callSessionId, 'call_failed', {
332
+ error: msg.description,
333
+ });
334
+ }
335
+ }
@@ -0,0 +1,213 @@
1
+ export interface PromptSpeakerContext {
2
+ speakerId: string;
3
+ speakerLabel: string;
4
+ speakerConfidence: number | null;
5
+ source: 'provider' | 'inferred';
6
+ }
7
+
8
+ export interface PromptSpeakerMetadata {
9
+ speakerId?: string;
10
+ speakerLabel?: string;
11
+ speakerName?: string;
12
+ speakerConfidence?: number;
13
+ participantId?: string;
14
+ }
15
+
16
+ interface SpeakerProfile {
17
+ speakerId: string;
18
+ speakerLabel: string;
19
+ speakerConfidence: number | null;
20
+ source: 'provider' | 'inferred';
21
+ utteranceCount: number;
22
+ firstSeenAt: number;
23
+ lastSeenAt: number;
24
+ }
25
+
26
+ function toCleanString(value: unknown): string | null {
27
+ if (typeof value !== 'string') return null;
28
+ const trimmed = value.trim();
29
+ return trimmed.length > 0 ? trimmed : null;
30
+ }
31
+
32
+ function toNumber(value: unknown): number | null {
33
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
34
+ if (typeof value === 'string') {
35
+ const parsed = Number(value);
36
+ if (Number.isFinite(parsed)) return parsed;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ function getObject(value: unknown): Record<string, unknown> | null {
42
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
43
+ return value as Record<string, unknown>;
44
+ }
45
+
46
+ function normalizeSpeakerLabel(metadata: PromptSpeakerMetadata, fallbackIndex: number): string {
47
+ const preferredLabel = toCleanString(metadata.speakerName) ?? toCleanString(metadata.speakerLabel);
48
+ if (preferredLabel) return preferredLabel;
49
+ return `Speaker ${fallbackIndex}`;
50
+ }
51
+
52
+ export function extractPromptSpeakerMetadata(message: Record<string, unknown>): PromptSpeakerMetadata {
53
+ const providerMetadata = getObject(message.providerMetadata);
54
+ const metadata = getObject(message.metadata);
55
+ const participant = getObject(message.participant);
56
+ const speaker = getObject(message.speaker);
57
+
58
+ const pick = (...values: unknown[]): string | undefined => {
59
+ for (const value of values) {
60
+ const cleaned = toCleanString(value);
61
+ if (cleaned) return cleaned;
62
+ }
63
+ return undefined;
64
+ };
65
+
66
+ const pickNumber = (...values: unknown[]): number | undefined => {
67
+ for (const value of values) {
68
+ const parsed = toNumber(value);
69
+ if (parsed !== null) return parsed;
70
+ }
71
+ return undefined;
72
+ };
73
+
74
+ return {
75
+ speakerId: pick(
76
+ message.speakerId,
77
+ message.speaker_id,
78
+ speaker?.id,
79
+ speaker?.speakerId,
80
+ metadata?.speakerId,
81
+ providerMetadata?.speakerId,
82
+ metadata?.speaker_id,
83
+ providerMetadata?.speaker_id,
84
+ ),
85
+ speakerLabel: pick(
86
+ message.speakerLabel,
87
+ message.speaker_label,
88
+ message.speaker,
89
+ speaker?.label,
90
+ speaker?.name,
91
+ metadata?.speakerLabel,
92
+ providerMetadata?.speakerLabel,
93
+ metadata?.speaker_label,
94
+ providerMetadata?.speaker_label,
95
+ ),
96
+ speakerName: pick(
97
+ message.speakerName,
98
+ message.speaker_name,
99
+ participant?.name,
100
+ metadata?.speakerName,
101
+ providerMetadata?.speakerName,
102
+ metadata?.speaker_name,
103
+ providerMetadata?.speaker_name,
104
+ ),
105
+ speakerConfidence: pickNumber(
106
+ message.speakerConfidence,
107
+ message.speaker_confidence,
108
+ message.confidence,
109
+ speaker?.confidence,
110
+ metadata?.speakerConfidence,
111
+ providerMetadata?.speakerConfidence,
112
+ metadata?.speaker_confidence,
113
+ providerMetadata?.speaker_confidence,
114
+ ),
115
+ participantId: pick(
116
+ message.participantId,
117
+ message.participant_id,
118
+ participant?.id,
119
+ metadata?.participantId,
120
+ providerMetadata?.participantId,
121
+ metadata?.participant_id,
122
+ providerMetadata?.participant_id,
123
+ ),
124
+ };
125
+ }
126
+
127
+ export class SpeakerIdentityTracker {
128
+ private profiles = new Map<string, SpeakerProfile>();
129
+ private nextInferredIndex = 1;
130
+
131
+ identifySpeaker(metadata: PromptSpeakerMetadata): PromptSpeakerContext {
132
+ const providerSpeakerId =
133
+ toCleanString(metadata.speakerId)
134
+ ?? toCleanString(metadata.participantId)
135
+ ?? null;
136
+
137
+ if (providerSpeakerId) {
138
+ const existing = this.profiles.get(providerSpeakerId);
139
+ if (existing) {
140
+ existing.lastSeenAt = Date.now();
141
+ existing.utteranceCount += 1;
142
+ if (metadata.speakerConfidence !== undefined) {
143
+ existing.speakerConfidence = metadata.speakerConfidence;
144
+ }
145
+ return {
146
+ speakerId: existing.speakerId,
147
+ speakerLabel: existing.speakerLabel,
148
+ speakerConfidence: existing.speakerConfidence,
149
+ source: existing.source,
150
+ };
151
+ }
152
+
153
+ const profile: SpeakerProfile = {
154
+ speakerId: providerSpeakerId,
155
+ speakerLabel: normalizeSpeakerLabel(metadata, this.nextInferredIndex),
156
+ speakerConfidence: metadata.speakerConfidence ?? null,
157
+ source: 'provider',
158
+ utteranceCount: 1,
159
+ firstSeenAt: Date.now(),
160
+ lastSeenAt: Date.now(),
161
+ };
162
+ this.profiles.set(providerSpeakerId, profile);
163
+ this.nextInferredIndex += 1;
164
+ return {
165
+ speakerId: profile.speakerId,
166
+ speakerLabel: profile.speakerLabel,
167
+ speakerConfidence: profile.speakerConfidence,
168
+ source: profile.source,
169
+ };
170
+ }
171
+
172
+ const inferredSpeakerId = 'primary-speaker';
173
+ const existingPrimary = this.profiles.get(inferredSpeakerId);
174
+ if (existingPrimary) {
175
+ existingPrimary.lastSeenAt = Date.now();
176
+ existingPrimary.utteranceCount += 1;
177
+ return {
178
+ speakerId: existingPrimary.speakerId,
179
+ speakerLabel: existingPrimary.speakerLabel,
180
+ speakerConfidence: existingPrimary.speakerConfidence,
181
+ source: existingPrimary.source,
182
+ };
183
+ }
184
+
185
+ const inferredProfile: SpeakerProfile = {
186
+ speakerId: inferredSpeakerId,
187
+ speakerLabel: normalizeSpeakerLabel(metadata, this.nextInferredIndex),
188
+ speakerConfidence: metadata.speakerConfidence ?? null,
189
+ source: 'inferred',
190
+ utteranceCount: 1,
191
+ firstSeenAt: Date.now(),
192
+ lastSeenAt: Date.now(),
193
+ };
194
+ this.profiles.set(inferredSpeakerId, inferredProfile);
195
+ this.nextInferredIndex += 1;
196
+
197
+ return {
198
+ speakerId: inferredProfile.speakerId,
199
+ speakerLabel: inferredProfile.speakerLabel,
200
+ speakerConfidence: inferredProfile.speakerConfidence,
201
+ source: inferredProfile.source,
202
+ };
203
+ }
204
+
205
+ listProfiles(): PromptSpeakerContext[] {
206
+ return [...this.profiles.values()].map((profile) => ({
207
+ speakerId: profile.speakerId,
208
+ speakerLabel: profile.speakerLabel,
209
+ speakerConfidence: profile.speakerConfidence,
210
+ source: profile.source,
211
+ }));
212
+ }
213
+ }
@@ -0,0 +1,34 @@
1
+ import { getSecureKey } from '../security/secure-keys.js';
2
+ import { getLogger } from '../util/logger.js';
3
+
4
+ const log = getLogger('twilio-config');
5
+
6
+ export interface TwilioConfig {
7
+ accountSid: string;
8
+ authToken: string;
9
+ phoneNumber: string;
10
+ webhookBaseUrl: string;
11
+ wssBaseUrl: string;
12
+ }
13
+
14
+ export function getTwilioConfig(): TwilioConfig {
15
+ const accountSid = getSecureKey('twilio_account_sid');
16
+ const authToken = getSecureKey('twilio_auth_token');
17
+ const phoneNumber = process.env.TWILIO_PHONE_NUMBER || getSecureKey('twilio_phone_number') || '';
18
+ const webhookBaseUrl = process.env.TWILIO_WEBHOOK_BASE_URL || '';
19
+ const wssBaseUrl = process.env.TWILIO_WSS_BASE_URL || '';
20
+
21
+ if (!accountSid || !authToken) {
22
+ throw new Error('Twilio credentials not configured. Set twilio_account_sid and twilio_auth_token via the credential_store tool.');
23
+ }
24
+ if (!phoneNumber) {
25
+ throw new Error('TWILIO_PHONE_NUMBER not configured.');
26
+ }
27
+ if (!webhookBaseUrl) {
28
+ throw new Error('TWILIO_WEBHOOK_BASE_URL not configured.');
29
+ }
30
+
31
+ log.debug('Twilio config loaded successfully');
32
+
33
+ return { accountSid, authToken, phoneNumber, webhookBaseUrl, wssBaseUrl };
34
+ }