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
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { Message as ProviderMessage } from './provider-types.js';
10
10
  import type { Message, ToolDefinition } from '../providers/types.js';
11
+ import { truncate } from '../util/truncate.js';
11
12
  import { getProvider } from '../providers/registry.js';
12
13
  import { getConfig } from '../config/loader.js';
13
14
 
@@ -97,7 +98,7 @@ function buildCorpus(messages: ProviderMessage[]): string[] {
97
98
  for (const msg of messages) {
98
99
  if (!msg.text.trim()) continue;
99
100
  const to = msg.conversationId;
100
- const truncatedBody = msg.text.slice(0, 500);
101
+ const truncatedBody = truncate(msg.text, 500, '');
101
102
  entries.push(`To: ${to}\n\n${truncatedBody}`);
102
103
  }
103
104
  return entries;
@@ -143,7 +144,7 @@ export async function extractStylePatterns(
143
144
 
144
145
  const stylePatterns: StylePattern[] = (result.style_patterns ?? []).map((p) => ({
145
146
  aspect: p.aspect,
146
- summary: p.summary.slice(0, 500),
147
+ summary: truncate(p.summary, 500, ''),
147
148
  importance: p.importance,
148
149
  examples: p.examples,
149
150
  }));
@@ -1,11 +1,13 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import { getConfig } from '../config/loader.js';
3
3
  import { getLogger } from '../util/logger.js';
4
+ import { truncate } from '../util/truncate.js';
4
5
  import type { ThreadMessage, ThreadSummary } from './types.js';
5
6
 
6
7
  const log = getLogger('thread-summarizer');
7
8
 
8
9
  const SUMMARIZATION_MODEL = 'claude-haiku-4-5-20251001';
10
+ const SUMMARIZATION_TIMEOUT_MS = 20_000;
9
11
  const DEFAULT_MAX_TOKENS = 4000;
10
12
  const CHARS_PER_TOKEN = 4;
11
13
 
@@ -172,14 +174,10 @@ function extractParticipants(messages: ThreadMessage[]): Array<{ name: string }>
172
174
 
173
175
  function summarizeSingleMessage(message: ThreadMessage): ThreadSummary {
174
176
  return {
175
- summary: message.body.length > 200
176
- ? message.body.slice(0, 197) + '...'
177
- : message.body,
177
+ summary: truncate(message.body, 200),
178
178
  participants: [{ name: message.sender }],
179
179
  openQuestions: [],
180
- lastAction: message.body.length > 200
181
- ? message.body.slice(0, 197) + '...'
182
- : message.body,
180
+ lastAction: truncate(message.body, 200),
183
181
  sentiment: 'neutral',
184
182
  messageCount: 1,
185
183
  };
@@ -225,7 +223,7 @@ async function summarizeWithLLM(
225
223
  timer = setTimeout(() => {
226
224
  abortController.abort();
227
225
  reject(new Error('Thread summarization LLM timeout'));
228
- }, 20000);
226
+ }, SUMMARIZATION_TIMEOUT_MS);
229
227
  }),
230
228
  ]);
231
229
 
@@ -249,7 +247,7 @@ async function summarizeWithLLM(
249
247
  : 'neutral';
250
248
 
251
249
  return {
252
- summary: String(input.summary ?? '').slice(0, 2000),
250
+ summary: truncate(String(input.summary ?? ''), 2000, ''),
253
251
  participants: Array.isArray(input.participants)
254
252
  ? input.participants.map((p) => ({
255
253
  name: String(p.name),
@@ -259,7 +257,7 @@ async function summarizeWithLLM(
259
257
  openQuestions: Array.isArray(input.openQuestions)
260
258
  ? input.openQuestions.map((q) => String(q))
261
259
  : [],
262
- lastAction: String(input.lastAction ?? '').slice(0, 500),
260
+ lastAction: truncate(String(input.lastAction ?? ''), 500, ''),
263
261
  sentiment,
264
262
  messageCount: messages.length,
265
263
  };
@@ -280,9 +278,7 @@ function buildFallbackSummary(messages: ThreadMessage[]): ThreadSummary {
280
278
  summary: `Thread with ${messages.length} message(s) from ${extractParticipants(messages).map((p) => p.name).join(', ')}.`,
281
279
  participants: extractParticipants(messages),
282
280
  openQuestions: [],
283
- lastAction: lastMsg.body.length > 200
284
- ? lastMsg.body.slice(0, 197) + '...'
285
- : lastMsg.body,
281
+ lastAction: truncate(lastMsg.body, 200),
286
282
  sentiment: 'neutral',
287
283
  messageCount: messages.length,
288
284
  };
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import Anthropic from '@anthropic-ai/sdk';
11
+ import { truncate } from '../util/truncate.js';
11
12
  import { v4 as uuid } from 'uuid';
12
13
  import { and, eq, isNull, desc } from 'drizzle-orm';
13
14
  import { getConfig } from '../config/loader.js';
@@ -24,6 +25,7 @@ import { DEFAULT_TRIAGE_CATEGORIES } from './types.js';
24
25
  const log = getLogger('triage-engine');
25
26
 
26
27
  const TRIAGE_MODEL = 'claude-haiku-4-5-20251001';
28
+ const TRIAGE_CLASSIFICATION_TIMEOUT_MS = 15_000;
27
29
 
28
30
  // ── Playbook fetching ────────────────────────────────────────────────
29
31
 
@@ -244,7 +246,7 @@ async function classifyWithLLM(
244
246
  timer = setTimeout(() => {
245
247
  abortController.abort();
246
248
  reject(new Error('Triage classification LLM timeout'));
247
- }, 15000);
249
+ }, TRIAGE_CLASSIFICATION_TIMEOUT_MS);
248
250
  }),
249
251
  ]);
250
252
 
@@ -282,7 +284,7 @@ async function classifyWithLLM(
282
284
  category: typeof input.category === 'string' ? input.category : 'needs_response',
283
285
  confidence,
284
286
  suggestedAction: typeof input.suggestedAction === 'string'
285
- ? input.suggestedAction.slice(0, 500)
287
+ ? truncate(input.suggestedAction, 500, '')
286
288
  : 'Review manually',
287
289
  matchedPlaybooks,
288
290
  };
@@ -0,0 +1,20 @@
1
+ import { OpenAIProvider } from '../openai/client.js';
2
+
3
+ export interface OpenRouterProviderOptions {
4
+ apiKey?: string;
5
+ baseURL?: string;
6
+ streamTimeoutMs?: number;
7
+ }
8
+
9
+ const DEFAULT_OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
10
+
11
+ export class OpenRouterProvider extends OpenAIProvider {
12
+ constructor(apiKey: string, model: string, options: OpenRouterProviderOptions = {}) {
13
+ super(apiKey, model, {
14
+ baseURL: options.baseURL?.trim() || DEFAULT_OPENROUTER_BASE_URL,
15
+ providerName: 'openrouter',
16
+ providerLabel: 'OpenRouter',
17
+ streamTimeoutMs: options.streamTimeoutMs,
18
+ });
19
+ }
20
+ }
@@ -4,6 +4,7 @@ import { OpenAIProvider } from "./openai/client.js";
4
4
  import { GeminiProvider } from "./gemini/client.js";
5
5
  import { OllamaProvider } from "./ollama/client.js";
6
6
  import { FireworksProvider } from "./fireworks/client.js";
7
+ import { OpenRouterProvider } from "./openrouter/client.js";
7
8
  import { RetryProvider } from "./retry.js";
8
9
  import { FailoverProvider } from "./failover.js";
9
10
  import { wrapWithLogfire } from "../logfire.js";
@@ -15,6 +16,7 @@ const DEFAULT_MODELS: Record<string, string> = {
15
16
  gemini: 'gemini-3-flash',
16
17
  ollama: 'llama3.2',
17
18
  fireworks: 'accounts/fireworks/models/kimi-k2p5',
19
+ openrouter: 'x-ai/grok-4',
18
20
  };
19
21
 
20
22
  const providers = new Map<string, Provider>();
@@ -135,4 +137,10 @@ export function initializeProviders(config: ProvidersConfig): void {
135
137
  wrapWithLogfire(new FireworksProvider(config.apiKeys.fireworks, model, { streamTimeoutMs })),
136
138
  ));
137
139
  }
140
+ if (config.apiKeys.openrouter) {
141
+ const model = resolveModel(config, 'openrouter');
142
+ registerProvider('openrouter', new RetryProvider(
143
+ wrapWithLogfire(new OpenRouterProvider(config.apiKeys.openrouter, model, { streamTimeoutMs })),
144
+ ));
145
+ }
138
146
  }
@@ -10,6 +10,7 @@ import { resolve } from 'node:path';
10
10
  import { timingSafeEqual } from 'node:crypto';
11
11
  import { ConfigError, IngressBlockedError } from '../util/errors.js';
12
12
  import { getLogger } from '../util/logger.js';
13
+ import { TwilioConversationRelayProvider } from '../calls/twilio-provider.js';
13
14
  import type { RunOrchestrator } from './run-orchestrator.js';
14
15
 
15
16
  // Route handlers — grouped by domain
@@ -45,6 +46,19 @@ import {
45
46
  handleDeleteSharedApp,
46
47
  } from './routes/app-routes.js';
47
48
  import { handleAddSecret } from './routes/secret-routes.js';
49
+ import {
50
+ handleStartCall,
51
+ handleGetCallStatus,
52
+ handleCancelCall,
53
+ handleAnswerCall,
54
+ } from './routes/call-routes.js';
55
+ import {
56
+ handleVoiceWebhook,
57
+ handleStatusCallback,
58
+ handleConnectAction,
59
+ } from '../calls/twilio-routes.js';
60
+ import { RelayConnection, activeRelayConnections } from '../calls/relay-server.js';
61
+ import type { RelayWebSocketData } from '../calls/relay-server.js';
48
62
 
49
63
  // Re-export shared types so existing consumers don't need to update imports
50
64
  export type {
@@ -95,6 +109,108 @@ function getDiskSpaceInfo(): DiskSpaceInfo | null {
95
109
  }
96
110
  }
97
111
 
112
+ /**
113
+ * Regex to extract the Twilio webhook subpath from both top-level and
114
+ * assistant-scoped route shapes:
115
+ * /v1/calls/twilio/<subpath>
116
+ * /v1/assistants/<id>/calls/twilio/<subpath>
117
+ */
118
+ const TWILIO_WEBHOOK_RE = /^\/v1\/(?:assistants\/[^/]+\/)?calls\/twilio\/(.+)$/;
119
+
120
+ /**
121
+ * Gateway-compatible Twilio webhook paths:
122
+ * /webhooks/twilio/<subpath>
123
+ *
124
+ * Maps gateway path segments to the internal subpath names used by the
125
+ * dispatcher below (e.g. "voice" -> "voice-webhook").
126
+ */
127
+ const TWILIO_GATEWAY_WEBHOOK_RE = /^\/webhooks\/twilio\/(.+)$/;
128
+ const GATEWAY_SUBPATH_MAP: Record<string, string> = {
129
+ voice: 'voice-webhook',
130
+ status: 'status',
131
+ 'connect-action': 'connect-action',
132
+ };
133
+
134
+ /**
135
+ * Validate a Twilio webhook request's X-Twilio-Signature header.
136
+ *
137
+ * Returns the raw body text on success so callers can reconstruct the Request
138
+ * for downstream handlers (which also need to read the body).
139
+ * Returns a 403 Response if signature validation fails.
140
+ *
141
+ * Fail-closed: if the auth token is not configured, the request is rejected
142
+ * with 403 rather than silently skipping validation. An explicit local-dev
143
+ * bypass is available via TWILIO_WEBHOOK_VALIDATION_DISABLED=true.
144
+ */
145
+ async function validateTwilioWebhook(
146
+ req: Request,
147
+ ): Promise<{ body: string } | Response> {
148
+ const rawBody = await req.text();
149
+
150
+ // Allow explicit local-dev bypass — must be exactly "true"
151
+ if (process.env.TWILIO_WEBHOOK_VALIDATION_DISABLED === 'true') {
152
+ log.warn('Twilio webhook signature validation explicitly disabled via TWILIO_WEBHOOK_VALIDATION_DISABLED');
153
+ return { body: rawBody };
154
+ }
155
+
156
+ const authToken = TwilioConversationRelayProvider.getAuthToken();
157
+
158
+ // Fail-closed: reject if no auth token is configured
159
+ if (!authToken) {
160
+ log.error('Twilio auth token not configured — rejecting webhook request (fail-closed)');
161
+ return Response.json({ error: 'Forbidden' }, { status: 403 });
162
+ }
163
+
164
+ const signature = req.headers.get('x-twilio-signature');
165
+ if (!signature) {
166
+ log.warn('Twilio webhook request missing X-Twilio-Signature header');
167
+ return Response.json({ error: 'Forbidden' }, { status: 403 });
168
+ }
169
+
170
+ // Parse form-urlencoded body into key-value params for signature computation
171
+ const params: Record<string, string> = {};
172
+ const formData = new URLSearchParams(rawBody);
173
+ for (const [key, value] of formData.entries()) {
174
+ params[key] = value;
175
+ }
176
+
177
+ // Reconstruct the public-facing URL that Twilio signed against.
178
+ // Behind proxies/gateways, req.url is the local server URL (e.g.
179
+ // http://127.0.0.1:7821/...) which differs from the public URL Twilio
180
+ // used to compute the HMAC-SHA1 signature.
181
+ const publicBaseUrl = process.env.TWILIO_WEBHOOK_BASE_URL;
182
+ const parsedUrl = new URL(req.url);
183
+ const publicUrl = publicBaseUrl
184
+ ? publicBaseUrl.replace(/\/$/, '') + parsedUrl.pathname + parsedUrl.search
185
+ : req.url;
186
+
187
+ const isValid = TwilioConversationRelayProvider.verifyWebhookSignature(
188
+ publicUrl,
189
+ params,
190
+ signature,
191
+ authToken,
192
+ );
193
+
194
+ if (!isValid) {
195
+ log.warn('Twilio webhook signature validation failed');
196
+ return Response.json({ error: 'Forbidden' }, { status: 403 });
197
+ }
198
+
199
+ return { body: rawBody };
200
+ }
201
+
202
+ /**
203
+ * Re-create a Request with the same method, headers, and URL but with a
204
+ * pre-read body string so downstream handlers can call req.text() again.
205
+ */
206
+ function cloneRequestWithBody(original: Request, body: string): Request {
207
+ return new Request(original.url, {
208
+ method: original.method,
209
+ headers: original.headers,
210
+ body,
211
+ });
212
+ }
213
+
98
214
  export class RuntimeHttpServer {
99
215
  private server: ReturnType<typeof Bun.serve> | null = null;
100
216
  private port: number;
@@ -120,11 +236,37 @@ export class RuntimeHttpServer {
120
236
  }
121
237
 
122
238
  async start(): Promise<void> {
123
- this.server = Bun.serve({
239
+ this.server = Bun.serve<RelayWebSocketData>({
124
240
  port: this.port,
125
241
  hostname: this.hostname,
126
242
  maxRequestBodySize: MAX_REQUEST_BODY_BYTES,
127
- fetch: (req) => this.handleRequest(req),
243
+ fetch: (req, server) => this.handleRequest(req, server),
244
+ websocket: {
245
+ open(ws) {
246
+ const callSessionId = ws.data?.callSessionId;
247
+ log.info({ callSessionId }, 'ConversationRelay WebSocket opened');
248
+ if (callSessionId) {
249
+ const connection = new RelayConnection(ws, callSessionId);
250
+ activeRelayConnections.set(callSessionId, connection);
251
+ }
252
+ },
253
+ message(ws, message) {
254
+ const callSessionId = ws.data?.callSessionId;
255
+ if (callSessionId) {
256
+ const connection = activeRelayConnections.get(callSessionId);
257
+ connection?.handleMessage(typeof message === 'string' ? message : new TextDecoder().decode(message));
258
+ }
259
+ },
260
+ close(ws, code, reason) {
261
+ const callSessionId = ws.data?.callSessionId;
262
+ log.info({ callSessionId, code, reason: reason?.toString() }, 'ConversationRelay WebSocket closed');
263
+ if (callSessionId) {
264
+ const connection = activeRelayConnections.get(callSessionId);
265
+ connection?.destroy();
266
+ activeRelayConnections.delete(callSessionId);
267
+ }
268
+ },
269
+ },
128
270
  });
129
271
 
130
272
  // Sweep failed channel inbound events for retry every 30 seconds
@@ -162,7 +304,7 @@ export class RuntimeHttpServer {
162
304
  return timingSafeEqual(a, b);
163
305
  }
164
306
 
165
- private async handleRequest(req: Request): Promise<Response> {
307
+ private async handleRequest(req: Request, server: ReturnType<typeof Bun.serve>): Promise<Response> {
166
308
  const url = new URL(req.url);
167
309
  const path = url.pathname;
168
310
 
@@ -171,6 +313,56 @@ export class RuntimeHttpServer {
171
313
  return this.handleHealth();
172
314
  }
173
315
 
316
+ // WebSocket upgrade for ConversationRelay — before auth check because
317
+ // Twilio WebSocket connections don't use bearer tokens.
318
+ if (path.startsWith('/v1/calls/relay') && req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
319
+ const wsUrl = new URL(req.url);
320
+ const callSessionId = wsUrl.searchParams.get('callSessionId');
321
+ if (!callSessionId) {
322
+ return new Response('Missing callSessionId', { status: 400 });
323
+ }
324
+ const upgraded = server.upgrade(req, { data: { callSessionId } });
325
+ if (!upgraded) {
326
+ return new Response('WebSocket upgrade failed', { status: 500 });
327
+ }
328
+ // Bun handles the response after a successful upgrade.
329
+ // The RelayConnection is created in the websocket.open handler.
330
+ return undefined as unknown as Response;
331
+ }
332
+
333
+ // ── Twilio webhook endpoints — before auth check because Twilio
334
+ // webhook POSTs don't include bearer tokens.
335
+ // Supports /v1/calls/twilio/*, /v1/assistants/:id/calls/twilio/*,
336
+ // and gateway-compatible /webhooks/twilio/* paths.
337
+ // Validates X-Twilio-Signature to prevent unauthorized access. ──
338
+ const twilioMatch = path.match(TWILIO_WEBHOOK_RE);
339
+ const gatewayTwilioMatch = !twilioMatch ? path.match(TWILIO_GATEWAY_WEBHOOK_RE) : null;
340
+ const resolvedTwilioSubpath = twilioMatch
341
+ ? twilioMatch[1]
342
+ : gatewayTwilioMatch
343
+ ? GATEWAY_SUBPATH_MAP[gatewayTwilioMatch[1]]
344
+ : null;
345
+ if (resolvedTwilioSubpath && req.method === 'POST') {
346
+ const twilioSubpath = resolvedTwilioSubpath;
347
+
348
+ // Validate Twilio request signature before dispatching
349
+ const validation = await validateTwilioWebhook(req);
350
+ if (validation instanceof Response) return validation;
351
+
352
+ // Reconstruct request so handlers can read the body
353
+ const validatedReq = cloneRequestWithBody(req, validation.body);
354
+
355
+ if (twilioSubpath === 'voice-webhook') {
356
+ return await handleVoiceWebhook(validatedReq);
357
+ }
358
+ if (twilioSubpath === 'status') {
359
+ return await handleStatusCallback(validatedReq);
360
+ }
361
+ if (twilioSubpath === 'connect-action') {
362
+ return await handleConnectAction(validatedReq);
363
+ }
364
+ }
365
+
174
366
  // Require bearer token when configured
175
367
  if ((process.env.DISABLE_HTTP_AUTH ?? "").toLowerCase() !== "true" && this.bearerToken) {
176
368
  const authHeader = req.headers.get('authorization');
@@ -247,7 +439,7 @@ export class RuntimeHttpServer {
247
439
  // Paths already handled above (/v1/apps/..., /v1/secrets) will never reach here.
248
440
  const newRouteMatch = path.match(/^\/v1\/(?!assistants\/)(.+)$/);
249
441
  if (newRouteMatch) {
250
- return this.dispatchEndpoint('local-assistant', newRouteMatch[1], req, url);
442
+ return this.dispatchEndpoint(newRouteMatch[1], req, url);
251
443
  }
252
444
 
253
445
  // Legacy: /v1/assistants/:assistantId/<endpoint>
@@ -259,7 +451,7 @@ export class RuntimeHttpServer {
259
451
  const assistantId = match[1];
260
452
  const endpoint = match[2];
261
453
  log.warn({ endpoint, assistantId }, '[deprecated] /v1/assistants/:assistantId/... route used; migrate to /v1/...');
262
- return this.dispatchEndpoint(assistantId, endpoint, req, url);
454
+ return this.dispatchEndpoint(endpoint, req, url);
263
455
  }
264
456
 
265
457
  /**
@@ -268,7 +460,6 @@ export class RuntimeHttpServer {
268
460
  * legacy assistant-scoped routes (/v1/assistants/:assistantId/<endpoint>).
269
461
  */
270
462
  private async dispatchEndpoint(
271
- assistantId: string,
272
463
  endpoint: string,
273
464
  req: Request,
274
465
  url: URL,
@@ -279,32 +470,32 @@ export class RuntimeHttpServer {
279
470
  }
280
471
 
281
472
  if (endpoint === 'messages' && req.method === 'GET') {
282
- return handleListMessages(assistantId, url, this.interfacesDir);
473
+ return handleListMessages(url, this.interfacesDir);
283
474
  }
284
475
 
285
476
  if (endpoint === 'messages' && req.method === 'POST') {
286
- return await handleSendMessage(assistantId, req, {
477
+ return await handleSendMessage(req, {
287
478
  processMessage: this.processMessage,
288
479
  persistAndProcessMessage: this.persistAndProcessMessage,
289
480
  });
290
481
  }
291
482
 
292
483
  if (endpoint === 'attachments' && req.method === 'POST') {
293
- return await handleUploadAttachment(assistantId, req);
484
+ return await handleUploadAttachment(req);
294
485
  }
295
486
 
296
487
  if (endpoint === 'attachments' && req.method === 'DELETE') {
297
- return await handleDeleteAttachment(assistantId, req);
488
+ return await handleDeleteAttachment(req);
298
489
  }
299
490
 
300
491
  // Match attachments/:attachmentId
301
492
  const attachmentMatch = endpoint.match(/^attachments\/([^/]+)$/);
302
493
  if (attachmentMatch && req.method === 'GET') {
303
- return handleGetAttachment(assistantId, attachmentMatch[1]);
494
+ return handleGetAttachment(attachmentMatch[1]);
304
495
  }
305
496
 
306
497
  if (endpoint === 'suggestion' && req.method === 'GET') {
307
- return await handleGetSuggestion(assistantId, url, {
498
+ return await handleGetSuggestion(url, {
308
499
  suggestionCache: this.suggestionCache,
309
500
  suggestionInFlight: this.suggestionInFlight,
310
501
  });
@@ -314,7 +505,7 @@ export class RuntimeHttpServer {
314
505
  if (!this.runOrchestrator) {
315
506
  return Response.json({ error: 'Run orchestration not configured' }, { status: 503 });
316
507
  }
317
- return await handleCreateRun(assistantId, req, this.runOrchestrator);
508
+ return await handleCreateRun(req, this.runOrchestrator);
318
509
  }
319
510
 
320
511
  // Match runs/:runId, runs/:runId/decision, runs/:runId/trust-rule
@@ -325,17 +516,17 @@ export class RuntimeHttpServer {
325
516
  }
326
517
  const runId = runsMatch[1];
327
518
  if (runsMatch[2] === '/decision' && req.method === 'POST') {
328
- return await handleRunDecision(assistantId, runId, req, this.runOrchestrator);
519
+ return await handleRunDecision(runId, req, this.runOrchestrator);
329
520
  }
330
521
  if (runsMatch[2] === '/trust-rule' && req.method === 'POST') {
331
522
  const run = this.runOrchestrator.getRun(runId);
332
- if (!run || run.assistantId !== assistantId) {
523
+ if (!run) {
333
524
  return Response.json({ error: 'Run not found' }, { status: 404 });
334
525
  }
335
526
  return await handleAddTrustRule(runId, req);
336
527
  }
337
528
  if (req.method === 'GET') {
338
- return handleGetRun(assistantId, runId, this.runOrchestrator);
529
+ return handleGetRun(runId, this.runOrchestrator);
339
530
  }
340
531
  }
341
532
 
@@ -345,36 +536,98 @@ export class RuntimeHttpServer {
345
536
  }
346
537
 
347
538
  if (endpoint === 'channels/conversation' && req.method === 'DELETE') {
348
- return await handleDeleteConversation(assistantId, req);
539
+ return await handleDeleteConversation(req);
349
540
  }
350
541
 
351
542
  if (endpoint === 'channels/inbound' && req.method === 'POST') {
352
- return await handleChannelInbound(assistantId, req, this.processMessage);
543
+ return await handleChannelInbound(req, this.processMessage);
353
544
  }
354
545
 
355
546
  if (endpoint === 'channels/delivery-ack' && req.method === 'POST') {
356
- return await handleChannelDeliveryAck(assistantId, req);
547
+ return await handleChannelDeliveryAck(req);
357
548
  }
358
549
 
359
550
  if (endpoint === 'channels/dead-letters' && req.method === 'GET') {
360
- return handleListDeadLetters(assistantId);
551
+ return handleListDeadLetters();
361
552
  }
362
553
 
363
554
  if (endpoint === 'channels/replay' && req.method === 'POST') {
364
- return await handleReplayDeadLetters(assistantId, req);
555
+ return await handleReplayDeadLetters(req);
556
+ }
557
+
558
+ // ── Call API routes ───────────────────────────────────────────
559
+ if (endpoint === 'calls/start' && req.method === 'POST') {
560
+ return await handleStartCall(req);
561
+ }
562
+
563
+ // Match calls/:callSessionId and calls/:callSessionId/cancel, calls/:callSessionId/answer
564
+ const callsMatch = endpoint.match(/^calls\/([^/]+?)(\/cancel|\/answer)?$/);
565
+ if (callsMatch) {
566
+ const callSessionId = callsMatch[1];
567
+ // Skip known sub-paths that are handled elsewhere (twilio, relay)
568
+ if (callSessionId !== 'twilio' && callSessionId !== 'relay' && callSessionId !== 'start') {
569
+ if (callsMatch[2] === '/cancel' && req.method === 'POST') {
570
+ return await handleCancelCall(req, callSessionId);
571
+ }
572
+ if (callsMatch[2] === '/answer' && req.method === 'POST') {
573
+ return await handleAnswerCall(req, callSessionId);
574
+ }
575
+ if (!callsMatch[2] && req.method === 'GET') {
576
+ return handleGetCallStatus(callSessionId);
577
+ }
578
+ }
579
+ }
580
+
581
+ // ── Internal Twilio forwarding endpoints (gateway → runtime) ──
582
+ // These accept JSON payloads from the gateway (which already validated
583
+ // the Twilio signature) and reconstruct requests for the existing
584
+ // Twilio route handlers.
585
+ if (endpoint === 'internal/twilio/voice-webhook' && req.method === 'POST') {
586
+ const json = await req.json() as { params: Record<string, string>; originalUrl?: string };
587
+ const formBody = new URLSearchParams(json.params).toString();
588
+ // Reconstruct request URL: keep the original URL query string (callSessionId)
589
+ const reconstructedUrl = json.originalUrl ?? req.url;
590
+ const fakeReq = new Request(reconstructedUrl, {
591
+ method: 'POST',
592
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
593
+ body: formBody,
594
+ });
595
+ return await handleVoiceWebhook(fakeReq);
596
+ }
597
+
598
+ if (endpoint === 'internal/twilio/status' && req.method === 'POST') {
599
+ const json = await req.json() as { params: Record<string, string> };
600
+ const formBody = new URLSearchParams(json.params).toString();
601
+ const fakeReq = new Request(req.url, {
602
+ method: 'POST',
603
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
604
+ body: formBody,
605
+ });
606
+ return await handleStatusCallback(fakeReq);
607
+ }
608
+
609
+ if (endpoint === 'internal/twilio/connect-action' && req.method === 'POST') {
610
+ const json = await req.json() as { params: Record<string, string> };
611
+ const formBody = new URLSearchParams(json.params).toString();
612
+ const fakeReq = new Request(req.url, {
613
+ method: 'POST',
614
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
615
+ body: formBody,
616
+ });
617
+ return await handleConnectAction(fakeReq);
365
618
  }
366
619
 
367
620
  return Response.json({ error: 'Not found', source: 'runtime' }, { status: 404 });
368
621
  } catch (err) {
369
622
  if (err instanceof IngressBlockedError) {
370
- log.warn({ endpoint, assistantId, detectedTypes: err.detectedTypes }, 'Blocked HTTP request containing secrets');
623
+ log.warn({ endpoint, detectedTypes: err.detectedTypes }, 'Blocked HTTP request containing secrets');
371
624
  return Response.json({ error: err.message, code: err.code }, { status: 422 });
372
625
  }
373
626
  if (err instanceof ConfigError) {
374
- log.warn({ err, endpoint, assistantId }, 'Runtime HTTP config error');
627
+ log.warn({ err, endpoint }, 'Runtime HTTP config error');
375
628
  return Response.json({ error: err.message, code: err.code }, { status: 422 });
376
629
  }
377
- log.error({ err, endpoint, assistantId }, 'Runtime HTTP handler error');
630
+ log.error({ err, endpoint }, 'Runtime HTTP handler error');
378
631
  const message = err instanceof Error ? err.message : 'Internal server error';
379
632
  return Response.json({ error: message }, { status: 500 });
380
633
  }
@@ -428,7 +681,6 @@ export class RuntimeHttpServer {
428
681
 
429
682
  try {
430
683
  const { messageId: userMessageId } = await this.processMessage(
431
- event.assistantId,
432
684
  event.conversationId,
433
685
  content,
434
686
  attachmentIds,
@@ -12,7 +12,6 @@ export interface RuntimeMessageSessionOptions {
12
12
  }
13
13
 
14
14
  export type MessageProcessor = (
15
- assistantId: string,
16
15
  conversationId: string,
17
16
  content: string,
18
17
  attachmentIds?: string[],
@@ -26,7 +25,6 @@ export type MessageProcessor = (
26
25
  * immediately.
27
26
  */
28
27
  export type NonBlockingMessageProcessor = (
29
- assistantId: string,
30
28
  conversationId: string,
31
29
  content: string,
32
30
  attachmentIds?: string[],