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,385 @@
1
+ import { describe, test, expect, beforeEach, afterAll, mock } from 'bun:test';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { EventEmitter } from 'node:events';
6
+
7
+ const testDir = mkdtempSync(join(tmpdir(), 'call-bridge-test-'));
8
+
9
+ // ── Platform + logger mocks (must come before any source imports) ────
10
+
11
+ mock.module('../util/platform.js', () => ({
12
+ getDataDir: () => testDir,
13
+ isMacOS: () => process.platform === 'darwin',
14
+ isLinux: () => process.platform === 'linux',
15
+ isWindows: () => process.platform === 'win32',
16
+ getSocketPath: () => join(testDir, 'test.sock'),
17
+ getPidPath: () => join(testDir, 'test.pid'),
18
+ getDbPath: () => join(testDir, 'test.db'),
19
+ getLogPath: () => join(testDir, 'test.log'),
20
+ ensureDataDir: () => {},
21
+ }));
22
+
23
+ mock.module('../util/logger.js', () => ({
24
+ getLogger: () =>
25
+ new Proxy({} as Record<string, unknown>, {
26
+ get: () => () => {},
27
+ }),
28
+ }));
29
+
30
+ // ── Config mock ─────────────────────────────────────────────────────
31
+
32
+ mock.module('../config/loader.js', () => ({
33
+ getConfig: () => ({
34
+ apiKeys: { anthropic: 'test-key' },
35
+ memory: { enabled: false },
36
+ calls: {
37
+ enabled: true,
38
+ provider: 'twilio',
39
+ maxDurationSeconds: 3600,
40
+ userConsultTimeoutSeconds: 120,
41
+ disclosure: { enabled: false, text: '' },
42
+ safety: { denyCategories: [] },
43
+ },
44
+ }),
45
+ }));
46
+
47
+ // ── Anthropic SDK mock ──────────────────────────────────────────────
48
+
49
+ function createMockStream(tokens: string[]) {
50
+ const emitter = new EventEmitter();
51
+ const fullText = tokens.join('');
52
+
53
+ const stream = {
54
+ on: (event: string, handler: (...args: unknown[]) => void) => {
55
+ emitter.on(event, handler);
56
+ return stream;
57
+ },
58
+ finalMessage: () => {
59
+ for (const token of tokens) {
60
+ emitter.emit('text', token);
61
+ }
62
+ return Promise.resolve({
63
+ content: [{ type: 'text', text: fullText }],
64
+ });
65
+ },
66
+ };
67
+
68
+ return stream;
69
+ }
70
+
71
+ const mockStreamFn = mock((..._args: unknown[]) => createMockStream(['Hello']));
72
+
73
+ mock.module('@anthropic-ai/sdk', () => ({
74
+ default: class MockAnthropic {
75
+ messages = {
76
+ stream: (...args: unknown[]) => mockStreamFn(...args),
77
+ };
78
+ },
79
+ }));
80
+
81
+ // ── Import source modules after all mocks ───────────────────────────
82
+
83
+ import { initializeDb, getDb, resetDb } from '../memory/db.js';
84
+ import { conversations } from '../memory/schema.js';
85
+ import {
86
+ createCallSession,
87
+ getPendingQuestion,
88
+ updateCallSession,
89
+ recordCallEvent,
90
+ createPendingQuestion,
91
+ } from '../calls/call-store.js';
92
+ import {
93
+ registerCallQuestionNotifier,
94
+ unregisterCallQuestionNotifier,
95
+ registerCallCompletionNotifier,
96
+ unregisterCallCompletionNotifier,
97
+ fireCallQuestionNotifier,
98
+ fireCallCompletionNotifier,
99
+ } from '../calls/call-state.js';
100
+ import { CallOrchestrator } from '../calls/call-orchestrator.js';
101
+ import { tryHandlePendingCallAnswer } from '../calls/call-bridge.js';
102
+ import * as conversationStore from '../memory/conversation-store.js';
103
+ import type { RelayConnection } from '../calls/relay-server.js';
104
+
105
+ initializeDb();
106
+
107
+ afterAll(() => {
108
+ resetDb();
109
+ try {
110
+ rmSync(testDir, { recursive: true });
111
+ } catch {
112
+ /* best effort */
113
+ }
114
+ });
115
+
116
+ // ── Relay mock factory ──────────────────────────────────────────────
117
+
118
+ interface MockRelay extends RelayConnection {
119
+ sentTokens: Array<{ token: string; last: boolean }>;
120
+ endCalled: boolean;
121
+ }
122
+
123
+ function createMockRelay(): MockRelay {
124
+ const state = {
125
+ sentTokens: [] as Array<{ token: string; last: boolean }>,
126
+ _endCalled: false,
127
+ };
128
+
129
+ return {
130
+ get sentTokens() { return state.sentTokens; },
131
+ get endCalled() { return state._endCalled; },
132
+ sendTextToken(token: string, last: boolean) {
133
+ state.sentTokens.push({ token, last });
134
+ },
135
+ endSession(_reason?: string) {
136
+ state._endCalled = true;
137
+ },
138
+ } as unknown as MockRelay;
139
+ }
140
+
141
+ // ── Helpers ─────────────────────────────────────────────────────────
142
+
143
+ let ensuredConvIds = new Set<string>();
144
+ function ensureConversation(id: string): void {
145
+ if (ensuredConvIds.has(id)) return;
146
+ const db = getDb();
147
+ const now = Date.now();
148
+ db.insert(conversations).values({
149
+ id,
150
+ title: `Test conversation ${id}`,
151
+ createdAt: now,
152
+ updatedAt: now,
153
+ }).run();
154
+ ensuredConvIds.add(id);
155
+ }
156
+
157
+ function resetTables() {
158
+ const db = getDb();
159
+ db.run('DELETE FROM call_pending_questions');
160
+ db.run('DELETE FROM call_events');
161
+ db.run('DELETE FROM call_sessions');
162
+ db.run('DELETE FROM messages');
163
+ db.run('DELETE FROM conversations');
164
+ ensuredConvIds = new Set();
165
+ }
166
+
167
+ function getMessagesForConversation(conversationId: string) {
168
+ return conversationStore.getMessages(conversationId);
169
+ }
170
+
171
+ describe('call-bridge', () => {
172
+ beforeEach(() => {
173
+ resetTables();
174
+ mockStreamFn.mockImplementation(() => createMockStream(['Hello']));
175
+ });
176
+
177
+ // ── tryHandlePendingCallAnswer ──────────────────────────────────
178
+
179
+ test('returns handled:false when no active call exists', async () => {
180
+ ensureConversation('conv-no-call');
181
+ const result = await tryHandlePendingCallAnswer('conv-no-call', 'some answer');
182
+ expect(result.handled).toBe(false);
183
+ expect(result.reason).toBe('no_active_call');
184
+ });
185
+
186
+ test('returns handled:false when call exists but no pending question', async () => {
187
+ ensureConversation('conv-no-question');
188
+ createCallSession({
189
+ conversationId: 'conv-no-question',
190
+ provider: 'twilio',
191
+ fromNumber: '+15551111111',
192
+ toNumber: '+15552222222',
193
+ });
194
+ const result = await tryHandlePendingCallAnswer('conv-no-question', 'some answer');
195
+ expect(result.handled).toBe(false);
196
+ expect(result.reason).toBe('no_pending_question');
197
+ });
198
+
199
+ test('returns handled:false when orchestrator is not found (call still active but no orchestrator)', async () => {
200
+ ensureConversation('conv-ended');
201
+ const callSession = createCallSession({
202
+ conversationId: 'conv-ended',
203
+ provider: 'twilio',
204
+ fromNumber: '+15551111111',
205
+ toNumber: '+15552222222',
206
+ });
207
+ // Leave the session in an active (non-terminal) state but do NOT register an orchestrator.
208
+ // This simulates a race where the orchestrator was destroyed but the session hasn't
209
+ // been marked terminal yet.
210
+ updateCallSession(callSession.id, { status: 'in_progress' });
211
+
212
+ // Create a pending question without an orchestrator
213
+ createPendingQuestion(callSession.id, 'What time?');
214
+
215
+ const result = await tryHandlePendingCallAnswer('conv-ended', 'Too late');
216
+ expect(result.handled).toBe(false);
217
+ expect(result.reason).toBe('orchestrator_not_found');
218
+ });
219
+
220
+ test('returns no_active_call when call has already completed', async () => {
221
+ ensureConversation('conv-completed');
222
+ const callSession = createCallSession({
223
+ conversationId: 'conv-completed',
224
+ provider: 'twilio',
225
+ fromNumber: '+15551111111',
226
+ toNumber: '+15552222222',
227
+ });
228
+ // Mark the call as completed — getActiveCallSessionForConversation will return null
229
+ updateCallSession(callSession.id, { status: 'completed', endedAt: Date.now() });
230
+
231
+ const result = await tryHandlePendingCallAnswer('conv-completed', 'Too late');
232
+ expect(result.handled).toBe(false);
233
+ expect(result.reason).toBe('no_active_call');
234
+ });
235
+
236
+ test('returns handled:false when orchestrator is not in waiting_on_user state', async () => {
237
+ ensureConversation('conv-not-waiting');
238
+ const callSession = createCallSession({
239
+ conversationId: 'conv-not-waiting',
240
+ provider: 'twilio',
241
+ fromNumber: '+15551111111',
242
+ toNumber: '+15552222222',
243
+ });
244
+
245
+ // Create orchestrator (state=idle by default)
246
+ const relay = createMockRelay();
247
+ const orchestrator = new CallOrchestrator(callSession.id, relay as unknown as RelayConnection, null);
248
+
249
+ // Create a pending question in the DB but orchestrator is idle, not waiting_on_user
250
+ createPendingQuestion(callSession.id, 'What time?');
251
+
252
+ const result = await tryHandlePendingCallAnswer('conv-not-waiting', 'answer');
253
+ expect(result.handled).toBe(false);
254
+ expect(result.reason).toBe('orchestrator_not_waiting');
255
+
256
+ orchestrator.destroy();
257
+ });
258
+
259
+ test('routes answer to orchestrator when waiting and returns handled:true', async () => {
260
+ // Setup: trigger ASK_USER to put orchestrator in waiting_on_user state
261
+ mockStreamFn.mockImplementation(() =>
262
+ createMockStream(['Hold on. [ASK_USER: Preferred date?]']),
263
+ );
264
+
265
+ ensureConversation('conv-bridge');
266
+ const callSession = createCallSession({
267
+ conversationId: 'conv-bridge',
268
+ provider: 'twilio',
269
+ fromNumber: '+15551111111',
270
+ toNumber: '+15552222222',
271
+ });
272
+
273
+ const relay = createMockRelay();
274
+ const orchestrator = new CallOrchestrator(callSession.id, relay as unknown as RelayConnection, 'test task');
275
+
276
+ await orchestrator.handleCallerUtterance('I need a reservation');
277
+
278
+ // Verify the orchestrator is now waiting
279
+ expect(orchestrator.getState()).toBe('waiting_on_user');
280
+
281
+ // Now provide the answer — set up mock for the LLM call after answer
282
+ mockStreamFn.mockImplementation(() => createMockStream(['Great, booking for tomorrow.']));
283
+
284
+ const result = await tryHandlePendingCallAnswer('conv-bridge', 'Tomorrow at noon');
285
+ expect(result.handled).toBe(true);
286
+
287
+ // Wait for the fire-and-forget LLM call
288
+ await new Promise((r) => setTimeout(r, 50));
289
+
290
+ // Verify the pending question was answered
291
+ const question = getPendingQuestion(callSession.id);
292
+ // After answering, there should be no pending question left
293
+ expect(question).toBeNull();
294
+
295
+ orchestrator.destroy();
296
+ });
297
+
298
+ // ── Call question notifier ──────────────────────────────────────
299
+
300
+ test('call question notifier persists assistant message and emits events', () => {
301
+ ensureConversation('conv-notifier-q');
302
+
303
+ const emittedEvents: Array<{ type: string; text?: string }> = [];
304
+ const sendToClient = (msg: { type: string; text?: string }) => {
305
+ emittedEvents.push(msg);
306
+ };
307
+
308
+ // Register notifier (as Session would)
309
+ registerCallQuestionNotifier('conv-notifier-q', (_callSessionId: string, question: string) => {
310
+ const questionText = `**Live call question**:\n\n${question}\n\n_Reply in this thread to answer._`;
311
+ conversationStore.addMessage(
312
+ 'conv-notifier-q',
313
+ 'assistant',
314
+ JSON.stringify([{ type: 'text', text: questionText }]),
315
+ );
316
+ sendToClient({ type: 'assistant_text_delta', text: questionText });
317
+ sendToClient({ type: 'message_complete' });
318
+ });
319
+
320
+ // Fire the notifier
321
+ fireCallQuestionNotifier('conv-notifier-q', 'call-session-1', 'What time works best?');
322
+
323
+ // Verify message was persisted
324
+ const msgs = getMessagesForConversation('conv-notifier-q');
325
+ expect(msgs.length).toBe(1);
326
+ expect(msgs[0].role).toBe('assistant');
327
+ expect(msgs[0].content).toContain('What time works best?');
328
+
329
+ // Verify events were emitted
330
+ expect(emittedEvents.length).toBe(2);
331
+ expect(emittedEvents[0].type).toBe('assistant_text_delta');
332
+ expect(emittedEvents[0].text).toContain('What time works best?');
333
+ expect(emittedEvents[1].type).toBe('message_complete');
334
+
335
+ unregisterCallQuestionNotifier('conv-notifier-q');
336
+ });
337
+
338
+ // ── Call completion notifier ────────────────────────────────────
339
+
340
+ test('call completion notifier persists summary and emits events', () => {
341
+ ensureConversation('conv-notifier-c');
342
+
343
+ const emittedEvents: Array<{ type: string; text?: string }> = [];
344
+ const sendToClient = (msg: { type: string; text?: string }) => {
345
+ emittedEvents.push(msg);
346
+ };
347
+
348
+ // Create a call session so getCallSession works
349
+ const callSession = createCallSession({
350
+ conversationId: 'conv-notifier-c',
351
+ provider: 'twilio',
352
+ fromNumber: '+15551111111',
353
+ toNumber: '+15552222222',
354
+ });
355
+ updateCallSession(callSession.id, { status: 'completed', startedAt: Date.now() - 30000, endedAt: Date.now() });
356
+ recordCallEvent(callSession.id, 'call_started', {});
357
+ recordCallEvent(callSession.id, 'call_ended', {});
358
+
359
+ registerCallCompletionNotifier('conv-notifier-c', (_callSessionId: string) => {
360
+ const summaryText = `**Call completed**. Events recorded.`;
361
+ conversationStore.addMessage(
362
+ 'conv-notifier-c',
363
+ 'assistant',
364
+ JSON.stringify([{ type: 'text', text: summaryText }]),
365
+ );
366
+ sendToClient({ type: 'assistant_text_delta', text: summaryText });
367
+ sendToClient({ type: 'message_complete' });
368
+ });
369
+
370
+ fireCallCompletionNotifier('conv-notifier-c', callSession.id);
371
+
372
+ // Verify message persisted
373
+ const msgs = getMessagesForConversation('conv-notifier-c');
374
+ expect(msgs.length).toBe(1);
375
+ expect(msgs[0].role).toBe('assistant');
376
+ expect(msgs[0].content).toContain('Call completed');
377
+
378
+ // Verify events emitted
379
+ expect(emittedEvents.length).toBe(2);
380
+ expect(emittedEvents[0].type).toBe('assistant_text_delta');
381
+ expect(emittedEvents[1].type).toBe('message_complete');
382
+
383
+ unregisterCallCompletionNotifier('conv-notifier-c');
384
+ });
385
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { isDeniedNumber } from '../calls/call-constants.js';
3
+
4
+ describe('isDeniedNumber', () => {
5
+ // Numbers that MUST be blocked
6
+ const blocked = [
7
+ '911',
8
+ '112',
9
+ '999',
10
+ '000',
11
+ '110',
12
+ '119',
13
+ '+112', // '+' stripped → '112' exact match
14
+ '+911', // '+' stripped → '911' exact match
15
+ '+1911', // country code 1 + 911
16
+ '+44999', // country code 44 + 999
17
+ '+61000', // country code 61 + 000
18
+ '+49110', // country code 49 + 110
19
+ '+81119', // country code 81 + 119
20
+ ];
21
+
22
+ for (const num of blocked) {
23
+ test(`blocks ${num}`, () => {
24
+ expect(isDeniedNumber(num)).toBe(true);
25
+ });
26
+ }
27
+
28
+ // Numbers that MUST be allowed (legitimate phone numbers)
29
+ const allowed = [
30
+ '+14155551212', // US number — digits after any CC split don't match short codes
31
+ '+442071234567', // UK number
32
+ '+15559998888', // US number
33
+ ];
34
+
35
+ for (const num of allowed) {
36
+ test(`allows ${num}`, () => {
37
+ expect(isDeniedNumber(num)).toBe(false);
38
+ });
39
+ }
40
+ });