@vellumai/assistant 0.11.11-staging.4 → 0.12.0-dev.202609110918.dd3fa5e

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 (250) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +2 -2
  3. package/Dockerfile +5 -0
  4. package/docs/architecture/turn-actor.md +9 -0
  5. package/knip.json +1 -0
  6. package/node_modules/@vellumai/app-icons/package.json +18 -0
  7. package/node_modules/@vellumai/app-icons/src/index.test.ts +85 -0
  8. package/node_modules/@vellumai/app-icons/src/index.ts +387 -0
  9. package/node_modules/@vellumai/app-icons/tsconfig.json +20 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
  11. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
  14. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +10 -0
  15. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
  16. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +15 -2
  17. package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
  18. package/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
  19. package/openapi.yaml +830 -2
  20. package/package.json +3 -1
  21. package/scripts/smoke-container-workspace-dependencies.ts +19 -0
  22. package/src/__tests__/app-builder-icon-names.test.ts +29 -0
  23. package/src/__tests__/app-compiler.test.ts +6 -0
  24. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  25. package/src/__tests__/assistant-attachment-directive.test.ts +4 -0
  26. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  27. package/src/__tests__/config-schema.test.ts +16 -23
  28. package/src/__tests__/connection-model-compat.test.ts +2 -2
  29. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  30. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  31. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  32. package/src/__tests__/conversation-attachments.test.ts +142 -0
  33. package/src/__tests__/conversation-delete-activation-progress.test.ts +145 -0
  34. package/src/__tests__/conversation-event-sink.test.ts +50 -0
  35. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  36. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -2
  37. package/src/__tests__/conversation-queue.test.ts +297 -0
  38. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  39. package/src/__tests__/conversation-title-service.test.ts +14 -10
  40. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  41. package/src/__tests__/credential-record-retire.test.ts +171 -1
  42. package/src/__tests__/credential-routes.test.ts +396 -8
  43. package/src/__tests__/drain-kick-guard.test.ts +2 -0
  44. package/src/__tests__/drain-requeue-on-contention.test.ts +6 -0
  45. package/src/__tests__/git-porcelain.test.ts +47 -0
  46. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  47. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  48. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  49. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  50. package/src/__tests__/messaging-send-tool.test.ts +355 -506
  51. package/src/__tests__/oauth-commands-routes.test.ts +47 -0
  52. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  53. package/src/__tests__/plugin-import-boundary-guard.test.ts +10 -0
  54. package/src/__tests__/resolve-app.test.ts +135 -0
  55. package/src/__tests__/source-fingerprint.test.ts +184 -0
  56. package/src/__tests__/subagent-manager-notify.test.ts +25 -4
  57. package/src/__tests__/workspace-git-service.test.ts +26 -0
  58. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  59. package/src/activation/progress-store.test.ts +1564 -0
  60. package/src/activation/progress-store.ts +1296 -0
  61. package/src/activation/turn-hooks.test.ts +246 -0
  62. package/src/activation/turn-hooks.ts +126 -0
  63. package/src/api/events/app-files-changed.ts +4 -3
  64. package/src/api/events/subagent-status-changed.ts +7 -5
  65. package/src/api/responses/activation.ts +136 -0
  66. package/src/apps/app-store.ts +9 -1
  67. package/src/apps/resolve-app.ts +81 -0
  68. package/src/apps/source-fingerprint.ts +266 -0
  69. package/src/bundler/app-compiler.ts +15 -0
  70. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +31 -57
  71. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  72. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +5 -8
  73. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  74. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +36 -33
  75. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  76. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  77. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  78. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  79. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -1
  80. package/src/calls/__tests__/voice-triage-escalate.test.ts +76 -0
  81. package/src/calls/call-constants.ts +24 -4
  82. package/src/calls/call-controller.ts +24 -73
  83. package/src/calls/voice-control-protocol.ts +53 -21
  84. package/src/calls/voice-session-bridge.ts +13 -17
  85. package/src/calls/voice-triage-escalate.ts +141 -55
  86. package/src/cli/__tests__/catalog-search-help.test.ts +7 -5
  87. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  88. package/src/cli/commands/__tests__/cli-test-harness.ts +12 -3
  89. package/src/cli/commands/apps.help.ts +67 -13
  90. package/src/cli/commands/apps.ts +183 -0
  91. package/src/cli/commands/channels/__tests__/channels.test.ts +2 -0
  92. package/src/cli/commands/channels/__tests__/request.test.ts +191 -0
  93. package/src/cli/commands/channels/index.help.ts +70 -18
  94. package/src/cli/commands/channels/index.ts +17 -6
  95. package/src/cli/commands/channels/request.ts +66 -0
  96. package/src/cli/commands/credentials.help.ts +6 -2
  97. package/src/cli/commands/credentials.ts +2 -2
  98. package/src/cli/commands/oauth/request.test.ts +2 -0
  99. package/src/cli/commands/oauth/request.ts +227 -186
  100. package/src/config/bundled-skills/app-builder/SKILL.md +15 -10
  101. package/src/config/bundled-skills/app-builder/TOOLS.json +2 -2
  102. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  103. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  104. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -135
  105. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  106. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  107. package/src/config/feature-flag-registry.json +36 -14
  108. package/src/config/schemas/__tests__/live-voice.test.ts +8 -24
  109. package/src/config/schemas/calls.ts +1 -28
  110. package/src/config/schemas/live-voice.ts +1 -21
  111. package/src/credential-execution/process-manager.test.ts +18 -19
  112. package/src/daemon/app-change-notify.ts +45 -0
  113. package/src/daemon/assistant-attachments.ts +11 -0
  114. package/src/daemon/conversation-agent-loop-handlers.ts +5 -0
  115. package/src/daemon/conversation-agent-loop.ts +71 -3
  116. package/src/daemon/conversation-attachments.ts +42 -1
  117. package/src/daemon/conversation-event-sink.ts +25 -0
  118. package/src/daemon/conversation-process.ts +69 -21
  119. package/src/daemon/conversation-store.ts +4 -3
  120. package/src/daemon/conversation-surfaces.ts +19 -4
  121. package/src/daemon/lifecycle.ts +0 -5
  122. package/src/daemon/message-types/sync.ts +2 -0
  123. package/src/daemon/shutdown-handlers.ts +1 -9
  124. package/src/daemon/tool-side-effects.ts +7 -18
  125. package/src/hooks/types.ts +33 -0
  126. package/src/i18n/AGENTS.md +22 -0
  127. package/src/i18n/__tests__/i18n.test.ts +108 -0
  128. package/src/i18n/index.ts +16 -0
  129. package/src/i18n/locales.ts +93 -0
  130. package/src/i18n/messages.ts +61 -0
  131. package/src/i18n/title.ts +25 -0
  132. package/src/ipc/assistant-server.ts +2 -0
  133. package/src/ipc/routes/__tests__/activation-sync-ipc-routes.test.ts +51 -0
  134. package/src/ipc/routes/activation-sync-ipc-routes.ts +42 -0
  135. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  136. package/src/live-voice/live-voice-session.ts +44 -124
  137. package/src/live-voice/protocol.ts +0 -2
  138. package/src/messaging/provider.ts +10 -3
  139. package/src/messaging/providers/__tests__/proactive-address.test.ts +140 -0
  140. package/src/messaging/providers/callback-routing.ts +18 -0
  141. package/src/messaging/providers/channel-transport.ts +50 -0
  142. package/src/messaging/providers/discord/transport.ts +23 -0
  143. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  144. package/src/messaging/providers/slack/adapter.ts +7 -44
  145. package/src/messaging/providers/slack/api.ts +14 -0
  146. package/src/messaging/providers/slack/transport.ts +23 -1
  147. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  148. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  149. package/src/messaging/providers/telegram-bot/transport.ts +25 -0
  150. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  151. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  152. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  153. package/src/notifications/AGENTS.md +1 -1
  154. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  155. package/src/notifications/__tests__/proactive-home-thread.test.ts +111 -0
  156. package/src/notifications/assistant-reply-producer.ts +8 -3
  157. package/src/notifications/conversation-pairing.ts +47 -5
  158. package/src/notifications/delivered-post-record.ts +14 -1
  159. package/src/oauth/__tests__/seed-providers-managed.test.ts +6 -0
  160. package/src/oauth/seed-providers.ts +0 -1
  161. package/src/persistence/__tests__/slack-thread-root-evidence.test.ts +102 -0
  162. package/src/persistence/conversation-crud.ts +110 -63
  163. package/src/persistence/conversation-lineage.ts +12 -4
  164. package/src/persistence/conversation-title-placeholders.ts +23 -0
  165. package/src/persistence/conversation-title-service.ts +9 -26
  166. package/src/persistence/delivery-crud.ts +13 -0
  167. package/src/persistence/message-cursor.ts +78 -0
  168. package/src/persistence/message-reads.ts +30 -0
  169. package/src/persistence/schema/memory-core.ts +5 -0
  170. package/src/plugin-api/constants.ts +2 -0
  171. package/src/plugin-api/index.ts +1 -0
  172. package/src/plugin-api/types.ts +2 -0
  173. package/src/plugins/AGENTS.md +1 -0
  174. package/src/plugins/defaults/index.ts +2 -0
  175. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  176. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  177. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  178. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  179. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +7 -0
  180. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  181. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  182. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  183. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  184. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  185. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  186. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  187. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  188. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  189. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  190. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  191. package/src/plugins/defaults/memory/memory-retrospective-job.ts +10 -1
  192. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  193. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  194. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  195. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +89 -0
  196. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  197. package/src/providers/gemini/client.ts +9 -23
  198. package/src/providers/gemini-thought-signature.test.ts +63 -0
  199. package/src/providers/gemini-thought-signature.ts +44 -0
  200. package/src/providers/model-catalog.ts +32 -0
  201. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +45 -0
  202. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  203. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  204. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  205. package/src/providers/openai/chat-completions-provider.ts +187 -75
  206. package/src/providers/openai/codex-models.ts +0 -4
  207. package/src/providers/openai/google-thought-signature.ts +320 -0
  208. package/src/runtime/AGENTS.md +2 -0
  209. package/src/runtime/__tests__/channel-send.test.ts +442 -0
  210. package/src/runtime/auth/__tests__/route-policy.test.ts +37 -0
  211. package/src/runtime/channel-send.ts +319 -0
  212. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  213. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  214. package/src/runtime/routes/__tests__/user-routes-notices.test.ts +248 -0
  215. package/src/runtime/routes/activation-routes.test.ts +415 -0
  216. package/src/runtime/routes/activation-routes.ts +172 -0
  217. package/src/runtime/routes/app-management-routes.ts +73 -0
  218. package/src/runtime/routes/app-routes.ts +1 -1
  219. package/src/runtime/routes/channel-send-routes.ts +94 -0
  220. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  221. package/src/runtime/routes/credential-routes.ts +64 -12
  222. package/src/runtime/routes/index.ts +4 -0
  223. package/src/runtime/routes/oauth-commands-routes.ts +21 -8
  224. package/src/runtime/routes/platform-managed-credentials.ts +48 -0
  225. package/src/runtime/routes/secret-routes.ts +3 -12
  226. package/src/runtime/routes/user-route-resolution.ts +21 -0
  227. package/src/runtime/routes/user-routes.ts +112 -9
  228. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  229. package/src/runtime/services/conversation-serializer.ts +16 -3
  230. package/src/runtime/sync/activation-sidecar-publish.test.ts +78 -0
  231. package/src/runtime/sync/documents-sidecar-publish.test.ts +3 -0
  232. package/src/runtime/sync/resource-sync-events.ts +25 -1
  233. package/src/runtime/sync/worker-daemon-notify.test.ts +36 -0
  234. package/src/runtime/sync/worker-daemon-notify.ts +39 -1
  235. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  236. package/src/security/ces-http-record-backend.ts +120 -0
  237. package/src/security/ces-rpc-record-backend.ts +7 -0
  238. package/src/tools/apps/executors.ts +8 -8
  239. package/src/tools/capability-offer.test.ts +1 -0
  240. package/src/tools/credentials/metadata-store.ts +100 -0
  241. package/src/workspace/git-porcelain.ts +66 -0
  242. package/src/workspace/git-service.ts +5 -54
  243. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  244. package/src/workspace/migrations/registry.ts +2 -0
  245. package/src/__tests__/app-source-watcher.test.ts +0 -218
  246. package/src/__tests__/speaker-identification.test.ts +0 -56
  247. package/src/calls/speaker-identification.ts +0 -233
  248. package/src/daemon/app-source-watcher.ts +0 -218
  249. package/src/messaging/providers/whatsapp/client.ts +0 -29
  250. package/src/util/fs-watcher-error.ts +0 -36
package/AGENTS.md CHANGED
@@ -87,6 +87,10 @@ The module-level dependency-injection pattern (`registerFooDeps()`) used by some
87
87
 
88
88
  Telemetry event types are defined by a platform-generated wire contract (`src/telemetry/telemetry-wire.generated.ts`) that `src/telemetry/types.ts` layers over, with pre-flush validation against it. Adding a new event type starts platform-side, not here. The mechanics, the drift guards, and the cross-repo ordering are documented next to the code they govern: see [`src/telemetry/AGENTS.md`](src/telemetry/AGENTS.md).
89
89
 
90
+ ## Daemon i18n
91
+
92
+ User-facing copy the daemon generates as a hardcoded constant goes through `src/i18n/`. Persist the message key. Resolve with `t(key, locale)` at the HTTP/CLI/channel edge. Never persist a translated string, and never match stored display strings back to keys. See [`src/i18n/AGENTS.md`](src/i18n/AGENTS.md).
93
+
90
94
  ## Code comments
91
95
 
92
96
  When writing or updating comments, **do not reference code that has been removed.** Comments should describe the current state of the codebase, not narrate its history. Avoid phrases like "no longer does X", "previously used Y", or "was removed in PR Z" — future readers should not need to understand past implementations to understand the current code.
package/ARCHITECTURE.md CHANGED
@@ -729,9 +729,9 @@ The assistant-side live voice module is intentionally bounded under `src/live-vo
729
729
  | `live-voice-archive.ts` | Audio artifact creation/linking for user utterance and assistant response message IDs |
730
730
  | `live-voice-metrics.ts` | Per-session and per-turn latency snapshots emitted as `metrics` frames |
731
731
 
732
- Live voice STT uses the same `resolveStreamingTranscriber()` path as conversation streaming. For V1 latency-sensitive behavior, the selected `services.stt.provider` must resolve to a `daemon-streaming` transcriber whose catalog entry has `conversationStreamingMode: "realtime-ws"` and usable credentials. Providers that only support batch or incremental-batch transcription remain valid for other voice surfaces, but do not satisfy live voice's streaming STT requirement.
732
+ Live voice STT uses the same `resolveStreamingTranscriber()` path as conversation streaming, dialed with the provider the `liveVoice` STT role resolves to after managed-speech defaulting (`resolveEffectiveSpeechProviders`, see `config/managed-speech-defaults.ts`). For V1 latency-sensitive behavior, that provider must resolve to a `daemon-streaming` transcriber whose catalog entry has `conversationStreamingMode: "realtime-ws"` and usable credentials. Providers that only support batch or incremental-batch transcription remain valid for other voice surfaces, but do not satisfy live voice's streaming STT requirement.
733
733
 
734
- Live voice TTS uses `streamLiveVoiceTtsAudio()` and the configured `services.tts.provider`. The selected provider must be registered, catalog-compatible, and expose `capabilities.supportsStreaming` plus `synthesizeStream()`. Providers whose catalog entry advertises `supportsStreaming` (currently all four catalog providers: ElevenLabs, Fish Audio, Deepgram, and xAI) satisfy this requirement; a buffered-only provider would remain available for buffered message playback or other supported surfaces, but live voice reports a TTS error instead of silently falling back to buffered playback.
734
+ Live voice TTS uses `streamLiveVoiceTtsAudio()` and the TTS provider that `resolveEffectiveSpeechProviders` settles on (the configured `services.tts.provider`, or the managed `vellum` provider when the configured one has no usable credentials). The selected provider must be registered, catalog-compatible, and expose `capabilities.supportsStreaming` plus `synthesizeStream()`. Providers whose catalog entry advertises `supportsStreaming` (currently all five catalog providers: ElevenLabs, Fish Audio, Deepgram, xAI, and the managed Vellum provider) satisfy this requirement; a buffered-only provider would remain available for buffered message playback or other supported surfaces, but live voice reports a TTS error instead of silently falling back to buffered playback.
735
735
 
736
736
  The `voiceFrontDoor` prompt skips current-turn legacy and v3 memory retrieval, while prior frozen memory cards and static memory context remain available. The front-door rule escalates rather than guessing when required personal context is absent. The escalated leg runs the ordinary memory pipeline against the latest visible caller prompt before the quality model, with low selector effort. This keeps memory work off the front-door TTFT path without cross-leg speculative state.
737
737
 
package/Dockerfile CHANGED
@@ -108,6 +108,7 @@ RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx
108
108
  COPY --link --from=builder /app /app
109
109
 
110
110
  # Copy source after installing dependencies for better layer caching.
111
+ COPY packages/app-icons /app/packages/app-icons
111
112
  COPY packages/avatar-catalog /app/packages/avatar-catalog
112
113
  COPY packages/avatar-manifest /app/packages/avatar-manifest
113
114
  COPY packages/ces-client /app/packages/ces-client
@@ -122,6 +123,10 @@ COPY packages/twilio-client /app/packages/twilio-client
122
123
 
123
124
  COPY assistant /app/assistant
124
125
 
126
+ # Resolve production workspace dependencies from the filesystem layout shipped
127
+ # in the image.
128
+ RUN bun run scripts/smoke-container-workspace-dependencies.ts
129
+
125
130
  # The repo-root .dockerignore decides which skills (and files) ship.
126
131
  COPY skills /app/skills
127
132
 
@@ -60,6 +60,15 @@ and restore the prior value afterwards, each guarding the restore so a turn that
60
60
  started in between is not clobbered. They are supplying the acting actor for
61
61
  their run, and are covered by this contract.
62
62
 
63
+ A queued message commits to a run at its drain, not at its enqueue. The drains
64
+ (`drainSingleMessage` and `drainBatch` in `conversation-process.ts`) stamp the
65
+ queued sender the way `processMessage` stamps its committing actor, and re-scope
66
+ the resident history to them, so a turn drained behind another actor's turn
67
+ runs as its sender everywhere the resting actor is read, not only in the
68
+ per-turn snapshot. A steered drain skips the re-scope: its sender owned the
69
+ turn it cut off, and the resident history may carry the in-memory repair of the
70
+ abandoned `tool_use`.
71
+
63
72
  `call-controller` keeps its own `trustContext` on its own object and never reads
64
73
  the conversation's. It is outside this contract.
65
74
 
package/knip.json CHANGED
@@ -15,6 +15,7 @@
15
15
  "ignore": ["src/config/preloaded-apps/**"],
16
16
  "ignoreDependencies": [
17
17
  "@microsoft/api-extractor",
18
+ "@vellumai/app-icons",
18
19
  "@vellumai/avatar-catalog",
19
20
  "@vellumai/avatar-manifest",
20
21
  "@vellumai/ces-client",
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@vellumai/app-icons",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "scripts": {
11
+ "typecheck": "bunx tsc --noEmit",
12
+ "test": "bun test src/"
13
+ },
14
+ "devDependencies": {
15
+ "@types/bun": "1.3.10",
16
+ "typescript": "5.9.3"
17
+ }
18
+ }
@@ -0,0 +1,85 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ APP_ICON_NAMES,
5
+ bridgeEmojiAppIcon,
6
+ EMOJI_ICON_NAMES,
7
+ isAppIconName,
8
+ isEmojiAppIcon,
9
+ normalizeAppIcon,
10
+ } from "./index.js";
11
+
12
+ describe("APP_ICON_NAMES", () => {
13
+ test("is kebab-case and free of duplicates", () => {
14
+ expect(new Set(APP_ICON_NAMES).size).toBe(APP_ICON_NAMES.length);
15
+ for (const name of APP_ICON_NAMES) {
16
+ expect(name).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
17
+ }
18
+ expect(isAppIconName("calculator")).toBe(true);
19
+ expect(isAppIconName("not-an-icon")).toBe(false);
20
+ });
21
+
22
+ test("every emoji maps to a registry name", () => {
23
+ for (const name of Object.values(EMOJI_ICON_NAMES)) {
24
+ expect(isAppIconName(name)).toBe(true);
25
+ }
26
+ });
27
+ });
28
+
29
+ describe("isEmojiAppIcon", () => {
30
+ test("accepts an emoji, a sequence, and a keycap", () => {
31
+ expect(isEmojiAppIcon("☕")).toBe(true);
32
+ expect(isEmojiAppIcon("☕️")).toBe(true);
33
+ expect(isEmojiAppIcon("👨‍💻")).toBe(true);
34
+ expect(isEmojiAppIcon("1️⃣")).toBe(true);
35
+ expect(isEmojiAppIcon("👍🏽")).toBe(true);
36
+ });
37
+
38
+ test("rejects text, text around an emoji, and bare marks", () => {
39
+ expect(isEmojiAppIcon("coffee")).toBe(false);
40
+ expect(isEmojiAppIcon("coffee ☕")).toBe(false);
41
+ expect(isEmojiAppIcon("☕ coffee")).toBe(false);
42
+ expect(isEmojiAppIcon("\u200D")).toBe(false);
43
+ expect(isEmojiAppIcon("")).toBe(false);
44
+ });
45
+ });
46
+
47
+ describe("bridgeEmojiAppIcon", () => {
48
+ test("maps a known emoji to its name, with or without the selector", () => {
49
+ expect(bridgeEmojiAppIcon("🔢")).toBe("calculator");
50
+ expect(bridgeEmojiAppIcon("☕️")).toBe("coffee");
51
+ });
52
+
53
+ test("passes everything else through, prototype names included", () => {
54
+ expect(bridgeEmojiAppIcon("calculator")).toBe("calculator");
55
+ expect(bridgeEmojiAppIcon("1️⃣")).toBe("1️⃣");
56
+ expect(bridgeEmojiAppIcon("constructor")).toBe("constructor");
57
+ expect(bridgeEmojiAppIcon(undefined)).toBeUndefined();
58
+ });
59
+ });
60
+
61
+ describe("normalizeAppIcon", () => {
62
+ test("keeps a registry name, normalised to its kebab-case key", () => {
63
+ expect(normalizeAppIcon("calculator")).toBe("calculator");
64
+ expect(normalizeAppIcon(" Calculator ")).toBe("calculator");
65
+ expect(normalizeAppIcon("List-Todo")).toBe("list-todo");
66
+ });
67
+
68
+ test("maps a known emoji and keeps an unknown one", () => {
69
+ expect(normalizeAppIcon("☕")).toBe("coffee");
70
+ expect(normalizeAppIcon("🔢")).toBe("calculator");
71
+ expect(normalizeAppIcon("1️⃣")).toBe("1️⃣");
72
+ });
73
+
74
+ test("drops URLs, text around an emoji, unknown names, empties, non-strings", () => {
75
+ expect(normalizeAppIcon("https://example.com/icon.png")).toBeUndefined();
76
+ expect(normalizeAppIcon("http://x/y.svg")).toBeUndefined();
77
+ expect(normalizeAppIcon("coffee ☕")).toBeUndefined();
78
+ expect(normalizeAppIcon("not-an-icon")).toBeUndefined();
79
+ expect(normalizeAppIcon("constructor")).toBeUndefined();
80
+ expect(normalizeAppIcon("")).toBeUndefined();
81
+ expect(normalizeAppIcon(" ")).toBeUndefined();
82
+ expect(normalizeAppIcon(42)).toBeUndefined();
83
+ expect(normalizeAppIcon(undefined)).toBeUndefined();
84
+ });
85
+ });
@@ -0,0 +1,387 @@
1
+ /**
2
+ * The icons an app can wear, shared by the assistant (which picks and stores
3
+ * one on an app's manifest) and the web client (which draws it).
4
+ *
5
+ * An app's `icon` is a Lucide kebab-case name from {@link APP_ICON_NAMES}.
6
+ * The assistant chooses it at `app_create`; the web client keeps the
7
+ * name-to-glyph map, typed against {@link AppIconName} so a name added here
8
+ * without a glyph there fails its typecheck. A manifest may also carry an
9
+ * emoji in place of a name; {@link EMOJI_ICON_NAMES} maps the common ones to
10
+ * the name each stands for, and both sides apply the map.
11
+ */
12
+
13
+ export const APP_ICON_NAMES = [
14
+ "calculator",
15
+ "calendar",
16
+ "list-todo",
17
+ "list-checks",
18
+ "square-check",
19
+ "timer",
20
+ "clock",
21
+ "alarm-clock",
22
+ "notebook-pen",
23
+ "sticky-note",
24
+ "pencil",
25
+ "file-text",
26
+ "clipboard-list",
27
+ "bookmark",
28
+ "book",
29
+ "book-open",
30
+ "chart-bar",
31
+ "chart-line",
32
+ "chart-pie",
33
+ "table",
34
+ "square-kanban",
35
+ "database",
36
+ "gauge",
37
+ "activity",
38
+ "target",
39
+ "flag",
40
+ "trophy",
41
+ "wallet",
42
+ "dollar-sign",
43
+ "piggy-bank",
44
+ "credit-card",
45
+ "receipt",
46
+ "percent",
47
+ "shopping-cart",
48
+ "package",
49
+ "gift",
50
+ "ticket",
51
+ "mail",
52
+ "inbox",
53
+ "message-square",
54
+ "phone",
55
+ "bell",
56
+ "users",
57
+ "contact",
58
+ "music",
59
+ "headphones",
60
+ "mic",
61
+ "video",
62
+ "film",
63
+ "tv",
64
+ "play",
65
+ "image",
66
+ "camera",
67
+ "gamepad-2",
68
+ "puzzle",
69
+ "party-popper",
70
+ "smile",
71
+ "map",
72
+ "map-pin",
73
+ "compass",
74
+ "globe",
75
+ "plane",
76
+ "car",
77
+ "bus",
78
+ "bike",
79
+ "ship",
80
+ "truck",
81
+ "house",
82
+ "bed",
83
+ "briefcase",
84
+ "graduation-cap",
85
+ "languages",
86
+ "brain",
87
+ "lightbulb",
88
+ "heart",
89
+ "heart-pulse",
90
+ "dumbbell",
91
+ "pill",
92
+ "stethoscope",
93
+ "baby",
94
+ "paw-print",
95
+ "utensils",
96
+ "coffee",
97
+ "wine",
98
+ "beer",
99
+ "cake",
100
+ "apple",
101
+ "carrot",
102
+ "salad",
103
+ "egg",
104
+ "fish",
105
+ "cloud",
106
+ "sun",
107
+ "moon",
108
+ "umbrella",
109
+ "snowflake",
110
+ "thermometer",
111
+ "droplets",
112
+ "flame",
113
+ "leaf",
114
+ "mountain",
115
+ "code",
116
+ "terminal",
117
+ "cpu",
118
+ "bot",
119
+ "wifi",
120
+ "lock",
121
+ "key",
122
+ "shield",
123
+ "settings",
124
+ "wrench",
125
+ "plug",
126
+ "battery",
127
+ "search",
128
+ "link",
129
+ "hash",
130
+ "layers",
131
+ "folder-open",
132
+ "palette",
133
+ "pen-tool",
134
+ "ruler",
135
+ "scale",
136
+ "scissors",
137
+ "shirt",
138
+ "newspaper",
139
+ "repeat",
140
+ "shuffle",
141
+ "volume-2",
142
+ "speaker",
143
+ "star",
144
+ "sparkles",
145
+ "zap",
146
+ "rocket",
147
+ "home",
148
+ ] as const;
149
+
150
+ export type AppIconName = (typeof APP_ICON_NAMES)[number];
151
+
152
+ const APP_ICON_NAME_SET: ReadonlySet<string> = new Set(APP_ICON_NAMES);
153
+
154
+ export function isAppIconName(value: string): value is AppIconName {
155
+ return APP_ICON_NAME_SET.has(value);
156
+ }
157
+
158
+ /**
159
+ * Emoji a manifest may carry as its icon, each mapped to the registry name
160
+ * it stands for. Keys carry no variation selector; the lookup strips it.
161
+ */
162
+ export const EMOJI_ICON_NAMES: Readonly<Record<string, AppIconName>> = {
163
+ "🔢": "calculator",
164
+ "🧮": "calculator",
165
+ "📅": "calendar",
166
+ "🗓": "calendar",
167
+ "✅": "list-todo",
168
+ "☑": "list-checks",
169
+ "⏱": "timer",
170
+ "⏲": "timer",
171
+ "⏰": "alarm-clock",
172
+ "🕐": "clock",
173
+ "🕒": "clock",
174
+ "📝": "notebook-pen",
175
+ "🗒": "sticky-note",
176
+ "✏": "pencil",
177
+ "📄": "file-text",
178
+ "📋": "clipboard-list",
179
+ "🔖": "bookmark",
180
+ "📚": "book",
181
+ "📖": "book-open",
182
+ "📔": "notebook-pen",
183
+ "📊": "chart-bar",
184
+ "📈": "chart-line",
185
+ "📉": "chart-line",
186
+ "🥧": "chart-pie",
187
+ "🗄": "database",
188
+ "🎯": "target",
189
+ "🚩": "flag",
190
+ "🏆": "trophy",
191
+ "🏁": "flag",
192
+ "💰": "wallet",
193
+ "💵": "dollar-sign",
194
+ "💲": "dollar-sign",
195
+ "🐷": "piggy-bank",
196
+ "💳": "credit-card",
197
+ "🧾": "receipt",
198
+ "🛒": "shopping-cart",
199
+ "🛍": "shopping-cart",
200
+ "📦": "package",
201
+ "🎁": "gift",
202
+ "🎟": "ticket",
203
+ "🎫": "ticket",
204
+ "✉": "mail",
205
+ "📧": "mail",
206
+ "📨": "mail",
207
+ "📥": "inbox",
208
+ "💬": "message-square",
209
+ "🗨": "message-square",
210
+ "📞": "phone",
211
+ "☎": "phone",
212
+ "🔔": "bell",
213
+ "👥": "users",
214
+ "👤": "contact",
215
+ "🎵": "music",
216
+ "🎶": "music",
217
+ "🎧": "headphones",
218
+ "🎤": "mic",
219
+ "🎙": "mic",
220
+ "📹": "video",
221
+ "🎬": "film",
222
+ "🎥": "film",
223
+ "📺": "tv",
224
+ "▶": "play",
225
+ "🖼": "image",
226
+ "📷": "camera",
227
+ "📸": "camera",
228
+ "🎮": "gamepad-2",
229
+ "🕹": "gamepad-2",
230
+ "🧩": "puzzle",
231
+ "🎉": "party-popper",
232
+ "🎊": "party-popper",
233
+ "😀": "smile",
234
+ "🙂": "smile",
235
+ "🗺": "map",
236
+ "📍": "map-pin",
237
+ "🧭": "compass",
238
+ "🌍": "globe",
239
+ "🌎": "globe",
240
+ "🌏": "globe",
241
+ "✈": "plane",
242
+ "🚗": "car",
243
+ "🚌": "bus",
244
+ "🚲": "bike",
245
+ "🚢": "ship",
246
+ "🚚": "truck",
247
+ "🏠": "house",
248
+ "🏡": "house",
249
+ "🛏": "bed",
250
+ "💼": "briefcase",
251
+ "🎓": "graduation-cap",
252
+ "🌐": "languages",
253
+ "🧠": "brain",
254
+ "💡": "lightbulb",
255
+ "❤": "heart",
256
+ "💗": "heart-pulse",
257
+ "🏋": "dumbbell",
258
+ "💪": "dumbbell",
259
+ "💊": "pill",
260
+ "🩺": "stethoscope",
261
+ "👶": "baby",
262
+ "🐾": "paw-print",
263
+ "🐶": "paw-print",
264
+ "🐱": "paw-print",
265
+ "🍽": "utensils",
266
+ "🍴": "utensils",
267
+ "☕": "coffee",
268
+ "🍷": "wine",
269
+ "🍺": "beer",
270
+ "🍰": "cake",
271
+ "🎂": "cake",
272
+ "🍎": "apple",
273
+ "🥕": "carrot",
274
+ "🥗": "salad",
275
+ "🥚": "egg",
276
+ "🐟": "fish",
277
+ "☁": "cloud",
278
+ "🌤": "cloud",
279
+ "☀": "sun",
280
+ "🌞": "sun",
281
+ "🌙": "moon",
282
+ "☂": "umbrella",
283
+ "🌧": "umbrella",
284
+ "❄": "snowflake",
285
+ "🌡": "thermometer",
286
+ "💧": "droplets",
287
+ "🔥": "flame",
288
+ "🌱": "leaf",
289
+ "🍃": "leaf",
290
+ "🏔": "mountain",
291
+ "⛰": "mountain",
292
+ "💻": "code",
293
+ "👨‍💻": "code",
294
+ "🖥": "terminal",
295
+ "⌨": "terminal",
296
+ "🤖": "bot",
297
+ "📡": "wifi",
298
+ "🔒": "lock",
299
+ "🔐": "lock",
300
+ "🔑": "key",
301
+ "🛡": "shield",
302
+ "⚙": "settings",
303
+ "🔧": "wrench",
304
+ "🛠": "wrench",
305
+ "🔌": "plug",
306
+ "🔋": "battery",
307
+ "🔍": "search",
308
+ "🔎": "search",
309
+ "🔗": "link",
310
+ "#️⃣": "hash",
311
+ "📁": "folder-open",
312
+ "📂": "folder-open",
313
+ "🎨": "palette",
314
+ "🖌": "pen-tool",
315
+ "📏": "ruler",
316
+ "⚖": "scale",
317
+ "✂": "scissors",
318
+ "👕": "shirt",
319
+ "📰": "newspaper",
320
+ "🔁": "repeat",
321
+ "🔀": "shuffle",
322
+ "🔊": "volume-2",
323
+ "⭐": "star",
324
+ "🌟": "star",
325
+ "✨": "sparkles",
326
+ "⚡": "zap",
327
+ "🚀": "rocket",
328
+ };
329
+
330
+ const VARIATION_SELECTORS = /[\uFE0E\uFE0F]/gu;
331
+
332
+ /* A value that is nothing but emoji: pictographs, emoji-presentation
333
+ characters, skin-tone modifiers, the joiner that composes a sequence, the
334
+ variation selectors, and keycap sequences (a digit, `#` or `*` with the
335
+ keycap mark). Anchored, so text with an emoji in it is not an emoji. */
336
+ const EMOJI_ONLY =
337
+ /^(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|\p{Emoji_Modifier}|\u200D|\uFE0E|\uFE0F|[0-9#*]\uFE0F?\u20E3)+$/u;
338
+ /* At least one glyph-bearing character, so a bare joiner or selector is not
339
+ an emoji either. */
340
+ const EMOJI_GLYPH = /\p{Extended_Pictographic}|\p{Emoji_Presentation}|\u20E3/u;
341
+
342
+ /** True when `value` is an emoji (or emoji sequence) and nothing else. */
343
+ export function isEmojiAppIcon(value: string): boolean {
344
+ return EMOJI_ONLY.test(value) && EMOJI_GLYPH.test(value);
345
+ }
346
+
347
+ /**
348
+ * A stored icon as a client should read it: an emoji the map knows becomes
349
+ * its registry name, and anything else passes through untouched, so reading
350
+ * a manifest never loses an icon the map cannot place.
351
+ */
352
+ export function bridgeEmojiAppIcon(
353
+ icon: string | undefined,
354
+ ): string | undefined {
355
+ if (!icon) {
356
+ return icon;
357
+ }
358
+ const key = icon.trim().replace(VARIATION_SELECTORS, "");
359
+ /* An own-property check: a bare index would find `constructor` and its
360
+ kin on the object's prototype. */
361
+ return Object.hasOwn(EMOJI_ICON_NAMES, key) ? EMOJI_ICON_NAMES[key] : icon;
362
+ }
363
+
364
+ /**
365
+ * The icon to persist on an app's manifest for a value the model passed: a
366
+ * registry name (any case) as its kebab-case key, an emoji as the name it
367
+ * maps to (or as itself when the map has none), and `undefined` for anything
368
+ * else. URLs and text around an emoji are dropped: they would render as raw
369
+ * strings, and an image icon is `app_generate_icon`'s job.
370
+ */
371
+ export function normalizeAppIcon(raw: unknown): string | undefined {
372
+ if (typeof raw !== "string") {
373
+ return undefined;
374
+ }
375
+ const trimmed = raw.trim();
376
+ if (trimmed === "" || /^https?:\/\//i.test(trimmed)) {
377
+ return undefined;
378
+ }
379
+ const name = trimmed.toLowerCase();
380
+ if (isAppIconName(name)) {
381
+ return name;
382
+ }
383
+ if (isEmojiAppIcon(trimmed)) {
384
+ return bridgeEmojiAppIcon(trimmed);
385
+ }
386
+ return undefined;
387
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+ "sourceMap": true,
14
+ "outDir": "./dist",
15
+ "rootDir": "./src",
16
+ "types": ["bun"]
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }
@@ -1,8 +1,10 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
+ botProviderForChannel,
4
5
  CHANNEL_BOT_PROVIDER,
5
6
  CHANNEL_IDS,
7
+ channelForBotProvider,
6
8
  isChannelBotProvider,
7
9
  isChannelUserIntegration,
8
10
  isChannelId,
@@ -41,6 +43,35 @@ describe("isChannelId", () => {
41
43
  });
42
44
  });
43
45
 
46
+ describe("botProviderForChannel and channelForBotProvider", () => {
47
+ test("are inverses over every entry of the map", () => {
48
+ for (const [channelId, botProviderKey] of Object.entries(
49
+ CHANNEL_BOT_PROVIDER,
50
+ )) {
51
+ expect(botProviderForChannel(channelId)).toBe(botProviderKey);
52
+ expect(channelForBotProvider(botProviderKey)).toBe(
53
+ channelId as keyof typeof CHANNEL_BOT_PROVIDER,
54
+ );
55
+ }
56
+ });
57
+
58
+ test("answer nothing for a channel without a bot, a person's grant, or an unknown key", () => {
59
+ expect(botProviderForChannel("phone")).toBeUndefined();
60
+ expect(botProviderForChannel("vellum")).toBeUndefined();
61
+ expect(botProviderForChannel("not-a-channel")).toBeUndefined();
62
+ // `slack` is the person's integration standing beside the bot's key.
63
+ expect(channelForBotProvider("slack")).toBeUndefined();
64
+ expect(channelForBotProvider("google")).toBeUndefined();
65
+ });
66
+
67
+ test("never resolve an inherited object property as a channel or a key", () => {
68
+ for (const name of ["constructor", "toString", "__proto__"]) {
69
+ expect(botProviderForChannel(name)).toBeUndefined();
70
+ expect(channelForBotProvider(name)).toBeUndefined();
71
+ }
72
+ });
73
+ });
74
+
44
75
  describe("isChannelUserIntegration", () => {
45
76
  test("names the grant standing beside a bot of the same brand", () => {
46
77
  expect(isChannelUserIntegration("slack")).toBe(true);
@@ -93,9 +93,34 @@ export const CHANNEL_BOT_PROVIDER = {
93
93
  * connected" question, for any provider key.
94
94
  */
95
95
  export function isChannelBotProvider(providerKey: string): boolean {
96
- return (Object.values(CHANNEL_BOT_PROVIDER) as readonly string[]).includes(
97
- providerKey,
98
- );
96
+ return channelForBotProvider(providerKey) !== undefined;
97
+ }
98
+
99
+ /**
100
+ * The provider key holding a channel's bot credential, or `undefined` when
101
+ * the channel has none, or is not a channel at all. Walks the map's own
102
+ * entries, so an inherited object property name is never a channel.
103
+ */
104
+ export function botProviderForChannel(channelId: string): string | undefined {
105
+ return Object.entries(CHANNEL_BOT_PROVIDER).find(
106
+ ([id]) => id === channelId,
107
+ )?.[1];
108
+ }
109
+
110
+ /**
111
+ * The channel whose bot credential a provider key holds, or `undefined` when
112
+ * the key is not a bot's. The inverse of `botProviderForChannel`, for a
113
+ * surface that has only the provider key in hand and needs to speak in the
114
+ * channel's vocabulary.
115
+ */
116
+ export function channelForBotProvider(
117
+ providerKey: string,
118
+ ): keyof typeof CHANNEL_BOT_PROVIDER | undefined {
119
+ return (
120
+ Object.entries(CHANNEL_BOT_PROVIDER) as Array<
121
+ [keyof typeof CHANNEL_BOT_PROVIDER, string]
122
+ >
123
+ ).find(([, key]) => key === providerKey)?.[0];
99
124
  }
100
125
 
101
126
  /**