@vellumai/assistant 0.5.0 → 0.5.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 (347) hide show
  1. package/ARCHITECTURE.md +54 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop.test.ts +111 -0
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  8. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  9. package/src/__tests__/app-executors.test.ts +1 -291
  10. package/src/__tests__/app-git-history.test.ts +4 -4
  11. package/src/__tests__/app-routes-csp.test.ts +1 -0
  12. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -9
  14. package/src/__tests__/attachments-store.test.ts +169 -21
  15. package/src/__tests__/attachments.test.ts +115 -1
  16. package/src/__tests__/btw-routes.test.ts +1 -0
  17. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  18. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  19. package/src/__tests__/checker.test.ts +54 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  22. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  23. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  24. package/src/__tests__/config-schema.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  27. package/src/__tests__/conversation-attachments.test.ts +17 -19
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  29. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  30. package/src/__tests__/conversation-error.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  32. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  33. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  34. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  35. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  36. package/src/__tests__/conversation-queue.test.ts +36 -1
  37. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  38. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  39. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  41. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  42. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  43. package/src/__tests__/conversation-store.test.ts +24 -21
  44. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  45. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  46. package/src/__tests__/conversation-title-service.test.ts +137 -0
  47. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  48. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  49. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  51. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  52. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  53. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  54. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  55. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  56. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  57. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  58. package/src/__tests__/diagnostics-export.test.ts +70 -1
  59. package/src/__tests__/filesystem-tools.test.ts +4 -2
  60. package/src/__tests__/first-greeting.test.ts +80 -0
  61. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  63. package/src/__tests__/history-repair.test.ts +103 -10
  64. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  65. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  66. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  67. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  68. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  69. package/src/__tests__/media-generate-image.test.ts +47 -94
  70. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  71. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  72. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  73. package/src/__tests__/migration-export-http.test.ts +3 -1
  74. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  75. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  76. package/src/__tests__/mime-builder.test.ts +3 -2
  77. package/src/__tests__/non-member-access-request.test.ts +12 -1
  78. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  79. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  80. package/src/__tests__/oauth-store.test.ts +115 -0
  81. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  82. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  83. package/src/__tests__/recording-handler.test.ts +17 -0
  84. package/src/__tests__/registry.test.ts +3 -8
  85. package/src/__tests__/relay-server.test.ts +1 -1
  86. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  87. package/src/__tests__/schema-transforms.test.ts +165 -5
  88. package/src/__tests__/server-history-render.test.ts +2 -2
  89. package/src/__tests__/skill-feature-flags-integration.test.ts +18 -17
  90. package/src/__tests__/skill-feature-flags.test.ts +13 -13
  91. package/src/__tests__/skill-load-feature-flag.test.ts +4 -4
  92. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  93. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  94. package/src/__tests__/starter-task-flow.test.ts +1 -0
  95. package/src/__tests__/suggestion-routes.test.ts +443 -0
  96. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  97. package/src/__tests__/swarm-recursion.test.ts +1 -0
  98. package/src/__tests__/swarm-tool.test.ts +1 -0
  99. package/src/__tests__/system-prompt.test.ts +8 -0
  100. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  101. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  102. package/src/__tests__/top-level-renderer.test.ts +22 -0
  103. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  104. package/src/__tests__/web-fetch.test.ts +6 -2
  105. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  106. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  107. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  108. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  109. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  110. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  111. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  112. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  113. package/src/agent/attachments.ts +27 -1
  114. package/src/agent/loop.ts +29 -1
  115. package/src/avatar/traits-png-sync.ts +80 -25
  116. package/src/bundler/app-bundler.ts +4 -4
  117. package/src/calls/call-domain.ts +1 -0
  118. package/src/calls/voice-session-bridge.ts +1 -0
  119. package/src/cli/commands/auth.ts +92 -0
  120. package/src/cli/commands/avatar.ts +7 -6
  121. package/src/cli/commands/config.ts +2 -0
  122. package/src/cli/commands/oauth/providers.ts +29 -0
  123. package/src/cli/program.ts +12 -0
  124. package/src/cli.ts +15 -48
  125. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  126. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  127. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  128. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  129. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  130. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  131. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  135. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  136. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  137. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  139. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  140. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  141. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  142. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  143. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  144. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  145. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  146. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  148. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  149. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  150. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  151. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  152. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  153. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  154. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  155. package/src/config/bundled-tool-registry.ts +2 -14
  156. package/src/config/feature-flag-registry.json +16 -0
  157. package/src/config/loader.ts +64 -0
  158. package/src/config/raw-config-utils.ts +30 -0
  159. package/src/config/schema-utils.ts +28 -7
  160. package/src/config/schema.ts +8 -0
  161. package/src/config/schemas/elevenlabs.ts +18 -0
  162. package/src/config/schemas/memory-lifecycle.ts +4 -2
  163. package/src/config/schemas/memory-storage.ts +1 -1
  164. package/src/config/schemas/services.ts +8 -6
  165. package/src/contacts/contact-store.ts +13 -6
  166. package/src/contacts/contacts-write.ts +0 -1
  167. package/src/context/window-manager.ts +13 -2
  168. package/src/daemon/conversation-agent-loop-handlers.ts +46 -42
  169. package/src/daemon/conversation-agent-loop.ts +56 -19
  170. package/src/daemon/conversation-attachments.ts +18 -36
  171. package/src/daemon/conversation-error.ts +2 -1
  172. package/src/daemon/conversation-history.ts +18 -4
  173. package/src/daemon/conversation-lifecycle.ts +39 -15
  174. package/src/daemon/conversation-messaging.ts +70 -26
  175. package/src/daemon/conversation-process.ts +58 -34
  176. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  177. package/src/daemon/conversation-slash.ts +121 -256
  178. package/src/daemon/conversation-surfaces.ts +143 -20
  179. package/src/daemon/conversation-tool-setup.ts +0 -6
  180. package/src/daemon/conversation-workspace.ts +21 -1
  181. package/src/daemon/conversation.ts +51 -29
  182. package/src/daemon/first-greeting.ts +35 -0
  183. package/src/daemon/handlers/config-embeddings.ts +148 -0
  184. package/src/daemon/handlers/config-model.ts +71 -26
  185. package/src/daemon/handlers/conversations.ts +0 -23
  186. package/src/daemon/handlers/recording.ts +26 -21
  187. package/src/daemon/history-repair.ts +28 -8
  188. package/src/daemon/host-cu-proxy.ts +2 -2
  189. package/src/daemon/lifecycle.ts +106 -64
  190. package/src/daemon/message-protocol.ts +3 -0
  191. package/src/daemon/message-types/conversations.ts +19 -0
  192. package/src/daemon/message-types/messages.ts +1 -0
  193. package/src/daemon/message-types/shared.ts +2 -0
  194. package/src/daemon/message-types/surfaces.ts +2 -0
  195. package/src/daemon/message-types/upgrades.ts +23 -0
  196. package/src/daemon/server.ts +83 -12
  197. package/src/daemon/shutdown-handlers.ts +8 -5
  198. package/src/daemon/startup-error.ts +9 -0
  199. package/src/daemon/tool-side-effects.ts +11 -28
  200. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  201. package/src/instrument.ts +0 -4
  202. package/src/media/app-icon-generator.ts +2 -2
  203. package/src/memory/app-git-service.ts +28 -16
  204. package/src/memory/app-store.ts +230 -41
  205. package/src/memory/attachments-store.ts +558 -130
  206. package/src/memory/conversation-attention-store.ts +70 -0
  207. package/src/memory/conversation-crud.ts +442 -3
  208. package/src/memory/conversation-directories.ts +125 -0
  209. package/src/memory/conversation-disk-view.ts +390 -0
  210. package/src/memory/conversation-key-store.ts +17 -5
  211. package/src/memory/conversation-queries.ts +5 -1
  212. package/src/memory/conversation-title-service.ts +21 -49
  213. package/src/memory/db-init.ts +28 -0
  214. package/src/memory/embedding-backend.ts +42 -53
  215. package/src/memory/embedding-gemini.test.ts +4 -4
  216. package/src/memory/embedding-local.ts +1 -3
  217. package/src/memory/embedding-ollama.ts +1 -3
  218. package/src/memory/embedding-openai.ts +1 -3
  219. package/src/memory/indexer.ts +9 -7
  220. package/src/memory/items-extractor.ts +42 -13
  221. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  222. package/src/memory/job-handlers/embedding.test.ts +1 -4
  223. package/src/memory/llm-request-log-store.ts +100 -1
  224. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  225. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  226. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  227. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  228. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  229. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  230. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  231. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  232. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  233. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  234. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  235. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  236. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  237. package/src/memory/migrations/index.ts +7 -0
  238. package/src/memory/migrations/registry.ts +13 -0
  239. package/src/memory/retriever.test.ts +601 -2
  240. package/src/memory/retriever.ts +85 -9
  241. package/src/memory/schema/conversations.ts +6 -0
  242. package/src/memory/schema/infrastructure.ts +13 -7
  243. package/src/memory/schema/oauth.ts +6 -0
  244. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  245. package/src/notifications/copy-composer.ts +26 -0
  246. package/src/notifications/decision-engine.ts +14 -1
  247. package/src/notifications/emit-signal.ts +1 -1
  248. package/src/notifications/signal.ts +36 -0
  249. package/src/oauth/byo-connection.test.ts +1 -45
  250. package/src/oauth/byo-connection.ts +2 -8
  251. package/src/oauth/connect-orchestrator.ts +15 -11
  252. package/src/oauth/connection-resolver.test.ts +191 -0
  253. package/src/oauth/connection-resolver.ts +66 -38
  254. package/src/oauth/connection.ts +0 -1
  255. package/src/oauth/oauth-store.ts +97 -47
  256. package/src/oauth/platform-connection.test.ts +0 -1
  257. package/src/oauth/platform-connection.ts +11 -3
  258. package/src/oauth/seed-providers.ts +78 -3
  259. package/src/oauth/token-persistence.ts +16 -10
  260. package/src/permissions/checker.ts +62 -19
  261. package/src/prompts/system-prompt.ts +2 -0
  262. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  263. package/src/providers/anthropic/client.ts +8 -1
  264. package/src/providers/failover.ts +4 -1
  265. package/src/providers/gemini/client.ts +50 -0
  266. package/src/providers/model-catalog.ts +92 -0
  267. package/src/providers/model-intents.ts +29 -20
  268. package/src/providers/openai/client.ts +49 -0
  269. package/src/providers/types.ts +2 -0
  270. package/src/runtime/access-request-helper.ts +16 -7
  271. package/src/runtime/auth/credential-service.ts +3 -1
  272. package/src/runtime/auth/route-policy.ts +14 -1
  273. package/src/runtime/btw-sidechain.ts +101 -0
  274. package/src/runtime/channel-reply-delivery.ts +17 -1
  275. package/src/runtime/http-router.ts +3 -1
  276. package/src/runtime/http-server.ts +196 -141
  277. package/src/runtime/http-types.ts +1 -0
  278. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  279. package/src/runtime/routes/access-request-decision.ts +41 -0
  280. package/src/runtime/routes/app-management-routes.ts +6 -3
  281. package/src/runtime/routes/app-routes.ts +7 -3
  282. package/src/runtime/routes/approval-routes.ts +1 -0
  283. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  284. package/src/runtime/routes/attachment-routes.ts +45 -15
  285. package/src/runtime/routes/btw-routes.ts +21 -61
  286. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  287. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  288. package/src/runtime/routes/conversation-routes.ts +222 -28
  289. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  290. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  291. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  292. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  293. package/src/runtime/routes/log-export-routes.ts +3 -0
  294. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  295. package/src/runtime/routes/memory-item-routes.ts +4 -0
  296. package/src/runtime/routes/migration-routes.ts +4 -1
  297. package/src/runtime/routes/oauth-apps.ts +291 -0
  298. package/src/runtime/routes/secret-routes.ts +28 -1
  299. package/src/runtime/routes/settings-routes.ts +14 -0
  300. package/src/runtime/routes/trace-event-routes.ts +4 -1
  301. package/src/schedule/schedule-store.ts +9 -21
  302. package/src/security/secure-keys.ts +21 -0
  303. package/src/signals/bash.ts +1 -1
  304. package/src/swarm/backend-claude-code.ts +3 -6
  305. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  306. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  307. package/src/tools/AGENTS.md +6 -10
  308. package/src/tools/apps/executors.ts +17 -232
  309. package/src/tools/claude-code/claude-code.ts +2 -3
  310. package/src/tools/credentials/vault.ts +7 -12
  311. package/src/tools/host-filesystem/read.ts +13 -10
  312. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  313. package/src/tools/schedule/list.ts +2 -7
  314. package/src/tools/schema-transforms.ts +5 -0
  315. package/src/tools/shared/filesystem/format-diff.ts +4 -21
  316. package/src/tools/skills/execute.ts +1 -1
  317. package/src/tools/tool-manifest.ts +0 -6
  318. package/src/tools/ui-surface/definitions.ts +2 -2
  319. package/src/util/device-id.ts +28 -5
  320. package/src/util/platform.ts +6 -0
  321. package/src/util/pricing.ts +1 -0
  322. package/src/util/retry.ts +1 -3
  323. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  324. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  325. package/src/workspace/migrations/006-services-config.ts +5 -0
  326. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  327. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  328. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  329. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  330. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  331. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  332. package/src/workspace/migrations/registry.ts +10 -0
  333. package/src/workspace/top-level-renderer.ts +12 -0
  334. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  335. package/src/__tests__/asset-search-tool.test.ts +0 -536
  336. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  337. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  338. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  339. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  340. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  341. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  342. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  343. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  344. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  345. package/src/daemon/media-visibility-policy.ts +0 -59
  346. package/src/tools/assets/materialize.ts +0 -248
  347. package/src/tools/assets/search.ts +0 -400
@@ -0,0 +1,1116 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { normalizeLlmContextPayloads } from "../runtime/routes/llm-context-normalization.js";
4
+
5
+ describe("normalizeLlmContextPayloads", () => {
6
+ test("normalizes OpenAI request and response payloads", () => {
7
+ const normalized = normalizeLlmContextPayloads({
8
+ createdAt: 1_742_400_000_000,
9
+ requestPayload: {
10
+ model: "gpt-4.1",
11
+ temperature: 0.2,
12
+ tool_choice: "auto",
13
+ messages: [
14
+ { role: "system", content: "Be concise." },
15
+ {
16
+ role: "user",
17
+ content: [
18
+ { type: "text", text: "What's the weather in Boston?" },
19
+ {
20
+ type: "image_url",
21
+ image_url: { url: "data:image/png;base64,abc" },
22
+ },
23
+ ],
24
+ },
25
+ ],
26
+ tools: [
27
+ {
28
+ type: "function",
29
+ function: {
30
+ name: "web_search",
31
+ description: "Search the web",
32
+ parameters: { type: "object" },
33
+ },
34
+ },
35
+ {
36
+ type: "function",
37
+ function: {
38
+ name: "get_weather",
39
+ description: "Read forecast data",
40
+ parameters: { type: "object" },
41
+ },
42
+ },
43
+ ],
44
+ },
45
+ responsePayload: {
46
+ model: "gpt-4.1-2026-03-01",
47
+ choices: [
48
+ {
49
+ finish_reason: "tool_calls",
50
+ message: {
51
+ role: "assistant",
52
+ content: "I'll check the forecast.",
53
+ tool_calls: [
54
+ {
55
+ id: "call_1",
56
+ type: "function",
57
+ function: {
58
+ name: "web_search",
59
+ arguments: JSON.stringify({ query: "Boston weather" }),
60
+ },
61
+ },
62
+ {
63
+ id: "call_2",
64
+ type: "function",
65
+ function: {
66
+ name: "get_weather",
67
+ arguments: JSON.stringify({ city: "Boston" }),
68
+ },
69
+ },
70
+ ],
71
+ },
72
+ },
73
+ ],
74
+ usage: {
75
+ prompt_tokens: 321,
76
+ completion_tokens: 54,
77
+ },
78
+ },
79
+ });
80
+
81
+ expect(normalized.summary).toEqual({
82
+ provider: "openai",
83
+ model: "gpt-4.1-2026-03-01",
84
+ inputTokens: 321,
85
+ outputTokens: 54,
86
+ stopReason: "tool_calls",
87
+ requestMessageCount: 2,
88
+ requestToolCount: 2,
89
+ responseMessageCount: 1,
90
+ responseToolCallCount: 2,
91
+ responsePreview: "I'll check the forecast.",
92
+ toolCallNames: ["web_search", "get_weather"],
93
+ });
94
+ expect(normalized.requestSections).toEqual([
95
+ {
96
+ kind: "system",
97
+ label: "System prompt",
98
+ role: "system",
99
+ text: "Be concise.",
100
+ },
101
+ {
102
+ kind: "message",
103
+ label: "User message 2",
104
+ role: "user",
105
+ text: "What's the weather in Boston?\n\n[image]",
106
+ },
107
+ {
108
+ kind: "tool_definitions",
109
+ label: "Available tools",
110
+ data: {
111
+ tools: [
112
+ {
113
+ type: "function",
114
+ function: {
115
+ name: "web_search",
116
+ description: "Search the web",
117
+ parameters: { type: "object" },
118
+ },
119
+ },
120
+ {
121
+ type: "function",
122
+ function: {
123
+ name: "get_weather",
124
+ description: "Read forecast data",
125
+ parameters: { type: "object" },
126
+ },
127
+ },
128
+ ],
129
+ },
130
+ language: "json",
131
+ },
132
+ {
133
+ kind: "settings",
134
+ label: "Request settings",
135
+ data: {
136
+ model: "gpt-4.1",
137
+ temperature: 0.2,
138
+ tool_choice: "auto",
139
+ },
140
+ language: "json",
141
+ },
142
+ ]);
143
+ expect(normalized.responseSections).toEqual([
144
+ {
145
+ kind: "message",
146
+ label: "Assistant response",
147
+ role: "assistant",
148
+ text: "I'll check the forecast.",
149
+ },
150
+ {
151
+ kind: "function_call",
152
+ label: "Response tool call 1",
153
+ role: "assistant",
154
+ toolName: "web_search",
155
+ data: { query: "Boston weather" },
156
+ text: '{"query":"Boston weather"}',
157
+ },
158
+ {
159
+ kind: "function_call",
160
+ label: "Response tool call 2",
161
+ role: "assistant",
162
+ toolName: "get_weather",
163
+ data: { city: "Boston" },
164
+ text: '{"city":"Boston"}',
165
+ },
166
+ ]);
167
+ });
168
+
169
+ test("normalizes Anthropic request and response payloads", () => {
170
+ const normalized = normalizeLlmContextPayloads({
171
+ createdAt: 1_742_400_000_001,
172
+ requestPayload: {
173
+ model: "claude-sonnet",
174
+ max_tokens: 1_024,
175
+ temperature: 0.1,
176
+ tool_choice: { type: "auto" },
177
+ system: [
178
+ {
179
+ type: "text",
180
+ text: "Use tools when they improve accuracy.",
181
+ cache_control: { type: "ephemeral" },
182
+ },
183
+ ],
184
+ messages: [
185
+ {
186
+ role: "user",
187
+ content: [{ type: "text", text: "Find the latest changelog." }],
188
+ },
189
+ {
190
+ role: "assistant",
191
+ content: [
192
+ { type: "text", text: "Checking sources." },
193
+ {
194
+ type: "thinking",
195
+ thinking: "I should search the changelog.",
196
+ },
197
+ {
198
+ type: "redacted_thinking",
199
+ signature: "sig_req_1",
200
+ },
201
+ {
202
+ type: "server_tool_use",
203
+ id: "srvtoolu_req_1",
204
+ name: "web_search",
205
+ input: { query: "vellum changelog" },
206
+ },
207
+ ],
208
+ },
209
+ ],
210
+ tools: [
211
+ {
212
+ name: "web_search",
213
+ description: "Search the web",
214
+ input_schema: { type: "object" },
215
+ },
216
+ ],
217
+ },
218
+ responsePayload: {
219
+ model: "claude-sonnet-4-6",
220
+ stop_reason: "tool_use",
221
+ usage: {
222
+ input_tokens: 410,
223
+ output_tokens: 73,
224
+ cache_creation_input_tokens: 200,
225
+ cache_read_input_tokens: 80,
226
+ },
227
+ content: [
228
+ { type: "text", text: "I found the changelog." },
229
+ {
230
+ type: "thinking",
231
+ thinking: "I should fetch the page.",
232
+ },
233
+ {
234
+ type: "redacted_thinking",
235
+ signature: "sig_resp_1",
236
+ },
237
+ {
238
+ type: "server_tool_use",
239
+ id: "srvtoolu_resp_1",
240
+ name: "web_search",
241
+ input: { query: "vellum changelog" },
242
+ },
243
+ {
244
+ type: "tool_use",
245
+ id: "toolu_resp_1",
246
+ name: "fetch_page",
247
+ input: { url: "https://example.com/changelog" },
248
+ },
249
+ ],
250
+ },
251
+ });
252
+
253
+ expect(normalized.summary).toEqual({
254
+ provider: "anthropic",
255
+ model: "claude-sonnet-4-6",
256
+ inputTokens: 410,
257
+ outputTokens: 73,
258
+ cacheCreationInputTokens: 200,
259
+ cacheReadInputTokens: 80,
260
+ stopReason: "tool_use",
261
+ requestMessageCount: 2,
262
+ requestToolCount: 1,
263
+ responseMessageCount: 1,
264
+ responseToolCallCount: 2,
265
+ responsePreview: "I found the changelog.",
266
+ toolCallNames: ["web_search", "fetch_page"],
267
+ });
268
+ expect(normalized.requestSections).toEqual([
269
+ {
270
+ kind: "system",
271
+ label: "System prompt",
272
+ role: "system",
273
+ text: "Use tools when they improve accuracy.",
274
+ },
275
+ {
276
+ kind: "message",
277
+ label: "User message 1",
278
+ role: "user",
279
+ text: "Find the latest changelog.",
280
+ },
281
+ {
282
+ kind: "message",
283
+ label: "Assistant message 2",
284
+ role: "assistant",
285
+ text: "Checking sources.",
286
+ },
287
+ {
288
+ kind: "reasoning",
289
+ label: "Assistant message 2 reasoning",
290
+ role: "assistant",
291
+ text: "I should search the changelog.",
292
+ },
293
+ {
294
+ kind: "reasoning",
295
+ label: "Assistant message 2 reasoning",
296
+ role: "assistant",
297
+ text: "[redacted thinking]",
298
+ },
299
+ {
300
+ kind: "tool_use",
301
+ label: "Assistant message 2 tool use",
302
+ role: "assistant",
303
+ toolName: "web_search",
304
+ data: { query: "vellum changelog" },
305
+ text: '{"query":"vellum changelog"}',
306
+ },
307
+ {
308
+ kind: "tool_definitions",
309
+ label: "Available tools",
310
+ data: {
311
+ tools: [
312
+ {
313
+ name: "web_search",
314
+ description: "Search the web",
315
+ input_schema: { type: "object" },
316
+ },
317
+ ],
318
+ },
319
+ language: "json",
320
+ },
321
+ {
322
+ kind: "settings",
323
+ label: "Request settings",
324
+ data: {
325
+ model: "claude-sonnet",
326
+ max_tokens: 1_024,
327
+ temperature: 0.1,
328
+ tool_choice: { type: "auto" },
329
+ },
330
+ language: "json",
331
+ },
332
+ ]);
333
+ expect(normalized.responseSections).toEqual([
334
+ {
335
+ kind: "message",
336
+ label: "Assistant response",
337
+ role: "assistant",
338
+ text: "I found the changelog.",
339
+ },
340
+ {
341
+ kind: "reasoning",
342
+ label: "Assistant response reasoning",
343
+ role: "assistant",
344
+ text: "I should fetch the page.",
345
+ },
346
+ {
347
+ kind: "reasoning",
348
+ label: "Assistant response reasoning",
349
+ role: "assistant",
350
+ text: "[redacted thinking]",
351
+ },
352
+ {
353
+ kind: "tool_use",
354
+ label: "Assistant response tool use",
355
+ role: "assistant",
356
+ toolName: "web_search",
357
+ data: { query: "vellum changelog" },
358
+ text: '{"query":"vellum changelog"}',
359
+ },
360
+ {
361
+ kind: "tool_use",
362
+ label: "Assistant response tool use",
363
+ role: "assistant",
364
+ toolName: "fetch_page",
365
+ data: { url: "https://example.com/changelog" },
366
+ text: '{"url":"https://example.com/changelog"}',
367
+ },
368
+ ]);
369
+ });
370
+
371
+ test("keeps Anthropic reasoning separate from response preview text", () => {
372
+ const normalized = normalizeLlmContextPayloads({
373
+ createdAt: 1_742_400_000_008,
374
+ requestPayload: {
375
+ model: "claude-sonnet",
376
+ messages: [{ role: "user", content: "Give me the answer." }],
377
+ },
378
+ responsePayload: {
379
+ model: "claude-sonnet-4-6",
380
+ stop_reason: "end_turn",
381
+ usage: {
382
+ input_tokens: 21,
383
+ output_tokens: 10,
384
+ },
385
+ content: [
386
+ {
387
+ type: "thinking",
388
+ thinking: "I have enough context now.",
389
+ },
390
+ {
391
+ type: "redacted_thinking",
392
+ signature: "sig_resp_2",
393
+ },
394
+ { type: "text", text: "The answer is 42." },
395
+ ],
396
+ },
397
+ });
398
+
399
+ expect(normalized.summary).toEqual({
400
+ provider: "anthropic",
401
+ model: "claude-sonnet-4-6",
402
+ inputTokens: 21,
403
+ outputTokens: 10,
404
+ cacheCreationInputTokens: undefined,
405
+ cacheReadInputTokens: undefined,
406
+ stopReason: "end_turn",
407
+ requestMessageCount: 1,
408
+ requestToolCount: 0,
409
+ responseMessageCount: 1,
410
+ responseToolCallCount: undefined,
411
+ responsePreview: "The answer is 42.",
412
+ toolCallNames: undefined,
413
+ });
414
+ expect(normalized.responseSections).toEqual([
415
+ {
416
+ kind: "message",
417
+ label: "Assistant response",
418
+ role: "assistant",
419
+ text: "The answer is 42.",
420
+ },
421
+ {
422
+ kind: "reasoning",
423
+ label: "Assistant response reasoning",
424
+ role: "assistant",
425
+ text: "I have enough context now.",
426
+ },
427
+ {
428
+ kind: "reasoning",
429
+ label: "Assistant response reasoning",
430
+ role: "assistant",
431
+ text: "[redacted thinking]",
432
+ },
433
+ ]);
434
+ });
435
+
436
+ test("normalizes plain-text OpenAI requests when the response identifies OpenAI", () => {
437
+ const normalized = normalizeLlmContextPayloads({
438
+ createdAt: 1_742_400_000_003,
439
+ requestPayload: {
440
+ model: "gpt-4.1",
441
+ messages: [
442
+ { role: "system", content: "Stay brief." },
443
+ { role: "user", content: "What should I pack for Boston?" },
444
+ ],
445
+ },
446
+ responsePayload: {
447
+ model: "gpt-4.1-2026-03-01",
448
+ choices: [
449
+ {
450
+ finish_reason: "stop",
451
+ message: {
452
+ role: "assistant",
453
+ content: "Bring a warm coat and an umbrella.",
454
+ },
455
+ },
456
+ ],
457
+ usage: {
458
+ prompt_tokens: 24,
459
+ completion_tokens: 11,
460
+ },
461
+ },
462
+ });
463
+
464
+ expect(normalized.summary).toEqual({
465
+ provider: "openai",
466
+ model: "gpt-4.1-2026-03-01",
467
+ inputTokens: 24,
468
+ outputTokens: 11,
469
+ cacheCreationInputTokens: undefined,
470
+ cacheReadInputTokens: undefined,
471
+ stopReason: "stop",
472
+ requestMessageCount: 2,
473
+ requestToolCount: 0,
474
+ responseMessageCount: 1,
475
+ responseToolCallCount: undefined,
476
+ responsePreview: "Bring a warm coat and an umbrella.",
477
+ toolCallNames: undefined,
478
+ });
479
+ expect(normalized.requestSections).toEqual([
480
+ {
481
+ kind: "system",
482
+ label: "System prompt",
483
+ role: "system",
484
+ text: "Stay brief.",
485
+ },
486
+ {
487
+ kind: "message",
488
+ label: "User message 2",
489
+ role: "user",
490
+ text: "What should I pack for Boston?",
491
+ },
492
+ ]);
493
+ expect(normalized.responseSections).toEqual([
494
+ {
495
+ kind: "message",
496
+ label: "Assistant response",
497
+ role: "assistant",
498
+ text: "Bring a warm coat and an umbrella.",
499
+ },
500
+ ]);
501
+ });
502
+
503
+ test("normalizes plain-text Anthropic requests when the response identifies Anthropic", () => {
504
+ const normalized = normalizeLlmContextPayloads({
505
+ createdAt: 1_742_400_000_004,
506
+ requestPayload: {
507
+ model: "claude-sonnet",
508
+ messages: [
509
+ { role: "user", content: "Find the latest changelog entry." },
510
+ ],
511
+ },
512
+ responsePayload: {
513
+ model: "claude-sonnet-4-6",
514
+ stop_reason: "end_turn",
515
+ usage: {
516
+ input_tokens: 19,
517
+ output_tokens: 9,
518
+ },
519
+ content: [{ type: "text", text: "I found one from this morning." }],
520
+ },
521
+ });
522
+
523
+ expect(normalized.summary).toEqual({
524
+ provider: "anthropic",
525
+ model: "claude-sonnet-4-6",
526
+ inputTokens: 19,
527
+ outputTokens: 9,
528
+ cacheCreationInputTokens: undefined,
529
+ cacheReadInputTokens: undefined,
530
+ stopReason: "end_turn",
531
+ requestMessageCount: 1,
532
+ requestToolCount: 0,
533
+ responseMessageCount: 1,
534
+ responseToolCallCount: undefined,
535
+ responsePreview: "I found one from this morning.",
536
+ toolCallNames: undefined,
537
+ });
538
+ expect(normalized.requestSections).toEqual([
539
+ {
540
+ kind: "message",
541
+ label: "User message 1",
542
+ role: "user",
543
+ text: "Find the latest changelog entry.",
544
+ },
545
+ ]);
546
+ expect(normalized.responseSections).toEqual([
547
+ {
548
+ kind: "message",
549
+ label: "Assistant response",
550
+ role: "assistant",
551
+ text: "I found one from this morning.",
552
+ },
553
+ ]);
554
+ });
555
+
556
+ test("rejects ambiguous request payloads that match OpenAI and Anthropic", () => {
557
+ const normalized = normalizeLlmContextPayloads({
558
+ createdAt: 1_742_400_000_010,
559
+ requestPayload: {
560
+ model: "gpt-4.1",
561
+ tool_choice: { type: "auto" },
562
+ parallel_tool_calls: true,
563
+ messages: [{ role: "user", content: "Hello there." }],
564
+ },
565
+ responsePayload: {
566
+ model: "gpt-4.1-2026-03-01",
567
+ choices: [
568
+ {
569
+ finish_reason: "stop",
570
+ message: {
571
+ role: "assistant",
572
+ content: "Hello back.",
573
+ },
574
+ },
575
+ ],
576
+ usage: {
577
+ prompt_tokens: 12,
578
+ completion_tokens: 4,
579
+ },
580
+ },
581
+ });
582
+
583
+ expect(normalized).toEqual({});
584
+ });
585
+
586
+ test("rejects ambiguous response payloads that match OpenAI and Anthropic", () => {
587
+ const normalized = normalizeLlmContextPayloads({
588
+ createdAt: 1_742_400_000_011,
589
+ requestPayload: {
590
+ model: "gpt-4.1",
591
+ parallel_tool_calls: true,
592
+ messages: [{ role: "user", content: "Hello there." }],
593
+ },
594
+ responsePayload: {
595
+ model: "gpt-4.1-2026-03-01",
596
+ choices: [
597
+ {
598
+ finish_reason: "stop",
599
+ message: {
600
+ role: "assistant",
601
+ content: "Hello back.",
602
+ },
603
+ },
604
+ ],
605
+ content: [{ type: "text", text: "Hello back." }],
606
+ stop_reason: "end_turn",
607
+ usage: {
608
+ prompt_tokens: 12,
609
+ completion_tokens: 4,
610
+ },
611
+ },
612
+ });
613
+
614
+ expect(normalized).toEqual({});
615
+ });
616
+
617
+ test("normalizes Anthropic document attachments in request prompt sections", () => {
618
+ const normalized = normalizeLlmContextPayloads({
619
+ createdAt: 1_742_400_000_009,
620
+ requestPayload: {
621
+ model: "claude-sonnet",
622
+ messages: [
623
+ {
624
+ role: "user",
625
+ content: [
626
+ {
627
+ type: "document",
628
+ title: "agenda.pdf",
629
+ source: {
630
+ type: "base64",
631
+ media_type: "application/pdf",
632
+ data: "JVBERi0xLjQK",
633
+ },
634
+ },
635
+ ],
636
+ },
637
+ ],
638
+ },
639
+ responsePayload: undefined,
640
+ });
641
+
642
+ expect(normalized.summary).toEqual({
643
+ provider: "anthropic",
644
+ model: "claude-sonnet",
645
+ inputTokens: undefined,
646
+ outputTokens: undefined,
647
+ cacheCreationInputTokens: undefined,
648
+ cacheReadInputTokens: undefined,
649
+ stopReason: undefined,
650
+ requestMessageCount: 1,
651
+ requestToolCount: 0,
652
+ responseMessageCount: undefined,
653
+ responseToolCallCount: undefined,
654
+ responsePreview: undefined,
655
+ toolCallNames: undefined,
656
+ });
657
+ expect(normalized.requestSections).toEqual([
658
+ {
659
+ kind: "message",
660
+ label: "User message 1",
661
+ role: "user",
662
+ text: "[document: agenda.pdf]",
663
+ },
664
+ ]);
665
+ });
666
+
667
+ test("normalizes Anthropic web_search_tool_result blocks", () => {
668
+ const normalized = normalizeLlmContextPayloads({
669
+ createdAt: 1_742_400_000_004,
670
+ requestPayload: {
671
+ model: "claude-sonnet",
672
+ messages: [
673
+ {
674
+ role: "user",
675
+ content: [
676
+ {
677
+ type: "web_search_tool_result",
678
+ tool_use_id: "stu_req_1",
679
+ content: [
680
+ {
681
+ type: "web_search_result",
682
+ url: "https://example.com",
683
+ title: "Example result",
684
+ encrypted_content: "enc_123",
685
+ },
686
+ ],
687
+ },
688
+ ],
689
+ },
690
+ ],
691
+ },
692
+ responsePayload: {
693
+ model: "claude-sonnet-4-6",
694
+ stop_reason: "end_turn",
695
+ usage: {
696
+ input_tokens: 12,
697
+ output_tokens: 8,
698
+ },
699
+ content: [
700
+ {
701
+ type: "web_search_tool_result",
702
+ tool_use_id: "stu_resp_1",
703
+ content: [
704
+ {
705
+ type: "web_search_result",
706
+ url: "https://example.org",
707
+ title: "Another result",
708
+ encrypted_content: "enc_456",
709
+ },
710
+ ],
711
+ },
712
+ ],
713
+ },
714
+ });
715
+
716
+ expect(normalized.summary).toEqual({
717
+ provider: "anthropic",
718
+ model: "claude-sonnet-4-6",
719
+ inputTokens: 12,
720
+ outputTokens: 8,
721
+ cacheCreationInputTokens: undefined,
722
+ cacheReadInputTokens: undefined,
723
+ stopReason: "end_turn",
724
+ requestMessageCount: 1,
725
+ requestToolCount: 0,
726
+ responseMessageCount: undefined,
727
+ responseToolCallCount: undefined,
728
+ responsePreview: undefined,
729
+ toolCallNames: undefined,
730
+ });
731
+ expect(normalized.requestSections).toEqual([
732
+ {
733
+ kind: "tool_result",
734
+ label: "User message 1 tool result",
735
+ role: "user",
736
+ toolName: "stu_req_1",
737
+ data: {
738
+ type: "web_search_tool_result",
739
+ tool_use_id: "stu_req_1",
740
+ content: [
741
+ {
742
+ type: "web_search_result",
743
+ url: "https://example.com",
744
+ title: "Example result",
745
+ },
746
+ ],
747
+ },
748
+ text: "[Web search results]",
749
+ },
750
+ ]);
751
+ expect(normalized.responseSections).toEqual([
752
+ {
753
+ kind: "tool_result",
754
+ label: "Assistant response tool result",
755
+ role: "assistant",
756
+ toolName: "stu_resp_1",
757
+ data: {
758
+ type: "web_search_tool_result",
759
+ tool_use_id: "stu_resp_1",
760
+ content: [
761
+ {
762
+ type: "web_search_result",
763
+ url: "https://example.org",
764
+ title: "Another result",
765
+ },
766
+ ],
767
+ },
768
+ text: "[Web search results]",
769
+ },
770
+ ]);
771
+ });
772
+
773
+ test("normalizes Gemini request and response payloads", () => {
774
+ const normalized = normalizeLlmContextPayloads({
775
+ createdAt: 1_742_400_000_002,
776
+ requestPayload: {
777
+ model: "gemini-3-flash",
778
+ contents: [
779
+ {
780
+ role: "user",
781
+ parts: [
782
+ { text: "Summarize this file." },
783
+ {
784
+ functionResponse: {
785
+ id: "call_req_1",
786
+ name: "read_file",
787
+ response: { output: "Long file body" },
788
+ },
789
+ },
790
+ ],
791
+ },
792
+ {
793
+ role: "model",
794
+ parts: [
795
+ { text: "I can do that." },
796
+ {
797
+ functionCall: {
798
+ id: "call_req_2",
799
+ name: "search_notes",
800
+ args: { query: "summary" },
801
+ },
802
+ },
803
+ ],
804
+ },
805
+ ],
806
+ config: {
807
+ systemInstruction: "Answer briefly.",
808
+ temperature: 0.4,
809
+ responseMimeType: "application/json",
810
+ tools: [
811
+ {
812
+ functionDeclarations: [
813
+ { name: "read_file", description: "Read a file" },
814
+ { name: "search_notes", description: "Search notes" },
815
+ ],
816
+ },
817
+ ],
818
+ },
819
+ },
820
+ responsePayload: {
821
+ model: "gemini-3-flash-001",
822
+ text: "Here is the summary.",
823
+ functionCalls: [
824
+ {
825
+ id: "call_resp_1",
826
+ name: "save_note",
827
+ args: { title: "brief" },
828
+ },
829
+ ],
830
+ finishReason: "STOP",
831
+ usageMetadata: {
832
+ promptTokenCount: 200,
833
+ candidatesTokenCount: 31,
834
+ },
835
+ },
836
+ });
837
+
838
+ expect(normalized.summary).toEqual({
839
+ provider: "gemini",
840
+ model: "gemini-3-flash-001",
841
+ inputTokens: 200,
842
+ outputTokens: 31,
843
+ stopReason: "STOP",
844
+ requestMessageCount: 2,
845
+ requestToolCount: 2,
846
+ responseMessageCount: 1,
847
+ responseToolCallCount: 1,
848
+ responsePreview: "Here is the summary.",
849
+ toolCallNames: ["save_note"],
850
+ });
851
+ expect(normalized.requestSections).toEqual([
852
+ {
853
+ kind: "system",
854
+ label: "System instruction",
855
+ role: "system",
856
+ text: "Answer briefly.",
857
+ },
858
+ {
859
+ kind: "message",
860
+ label: "User message 1",
861
+ role: "user",
862
+ text: "Summarize this file.",
863
+ },
864
+ {
865
+ kind: "function_response",
866
+ label: "User message 1 function response",
867
+ role: "user",
868
+ toolName: "read_file",
869
+ data: { output: "Long file body" },
870
+ text: '{"output":"Long file body"}',
871
+ },
872
+ {
873
+ kind: "message",
874
+ label: "Model message 2",
875
+ role: "model",
876
+ text: "I can do that.",
877
+ },
878
+ {
879
+ kind: "function_call",
880
+ label: "Model message 2 function call",
881
+ role: "model",
882
+ toolName: "search_notes",
883
+ data: { query: "summary" },
884
+ text: '{"query":"summary"}',
885
+ },
886
+ {
887
+ kind: "tool_definitions",
888
+ label: "Available tools",
889
+ data: {
890
+ tools: [
891
+ {
892
+ functionDeclarations: [
893
+ { name: "read_file", description: "Read a file" },
894
+ { name: "search_notes", description: "Search notes" },
895
+ ],
896
+ },
897
+ ],
898
+ },
899
+ language: "json",
900
+ },
901
+ {
902
+ kind: "settings",
903
+ label: "Generation config",
904
+ data: {
905
+ model: "gemini-3-flash",
906
+ config: {
907
+ temperature: 0.4,
908
+ responseMimeType: "application/json",
909
+ },
910
+ },
911
+ language: "json",
912
+ },
913
+ ]);
914
+ expect(normalized.responseSections).toEqual([
915
+ {
916
+ kind: "message",
917
+ label: "Assistant response",
918
+ role: "model",
919
+ text: "Here is the summary.",
920
+ },
921
+ {
922
+ kind: "function_call",
923
+ label: "Response function call 1",
924
+ role: "model",
925
+ toolName: "save_note",
926
+ data: { title: "brief" },
927
+ text: '{"title":"brief"}',
928
+ },
929
+ ]);
930
+ });
931
+
932
+ test("normalizes an OpenAI request with object tool_choice even when the response payload is malformed", () => {
933
+ const normalized = normalizeLlmContextPayloads({
934
+ createdAt: 1_742_400_000_005,
935
+ requestPayload: {
936
+ model: "gpt-4.1",
937
+ tool_choice: {
938
+ type: "function",
939
+ function: { name: "lookup" },
940
+ },
941
+ messages: [
942
+ {
943
+ role: "system",
944
+ content: "Line 1\n Line 2",
945
+ },
946
+ {
947
+ role: "user",
948
+ content: [
949
+ { type: "text", text: "First paragraph" },
950
+ { type: "text", text: " Second line\n third line" },
951
+ ],
952
+ },
953
+ ],
954
+ tools: [
955
+ {
956
+ type: "function",
957
+ function: {
958
+ name: "lookup",
959
+ description: "Lookup a record",
960
+ parameters: { type: "object" },
961
+ },
962
+ },
963
+ ],
964
+ },
965
+ responsePayload: "not-json",
966
+ });
967
+
968
+ expect(normalized.summary).toEqual({
969
+ provider: "openai",
970
+ model: "gpt-4.1",
971
+ inputTokens: undefined,
972
+ outputTokens: undefined,
973
+ cacheCreationInputTokens: undefined,
974
+ cacheReadInputTokens: undefined,
975
+ stopReason: undefined,
976
+ requestMessageCount: 2,
977
+ requestToolCount: 1,
978
+ responseMessageCount: undefined,
979
+ responseToolCallCount: undefined,
980
+ responsePreview: undefined,
981
+ toolCallNames: undefined,
982
+ });
983
+ expect(normalized.requestSections).toEqual([
984
+ {
985
+ kind: "system",
986
+ label: "System prompt",
987
+ role: "system",
988
+ text: "Line 1\n Line 2",
989
+ },
990
+ {
991
+ kind: "message",
992
+ label: "User message 2",
993
+ role: "user",
994
+ text: "First paragraph\n\n Second line\n third line",
995
+ },
996
+ {
997
+ kind: "tool_definitions",
998
+ label: "Available tools",
999
+ data: {
1000
+ tools: [
1001
+ {
1002
+ type: "function",
1003
+ function: {
1004
+ name: "lookup",
1005
+ description: "Lookup a record",
1006
+ parameters: { type: "object" },
1007
+ },
1008
+ },
1009
+ ],
1010
+ },
1011
+ language: "json",
1012
+ },
1013
+ {
1014
+ kind: "settings",
1015
+ label: "Request settings",
1016
+ data: {
1017
+ model: "gpt-4.1",
1018
+ tool_choice: {
1019
+ type: "function",
1020
+ function: { name: "lookup" },
1021
+ },
1022
+ },
1023
+ language: "json",
1024
+ },
1025
+ ]);
1026
+ expect(normalized.responseSections).toBeUndefined();
1027
+ });
1028
+
1029
+ test("normalizes an OpenAI response even when the request payload is malformed", () => {
1030
+ const normalized = normalizeLlmContextPayloads({
1031
+ createdAt: 1_742_400_000_006,
1032
+ requestPayload: "not-json",
1033
+ responsePayload: {
1034
+ model: "gpt-4.1-2026-03-01",
1035
+ choices: [
1036
+ {
1037
+ finish_reason: "stop",
1038
+ message: {
1039
+ role: "assistant",
1040
+ content: "First line\n Second line\n\n Third line",
1041
+ },
1042
+ },
1043
+ ],
1044
+ usage: {
1045
+ prompt_tokens: 18,
1046
+ completion_tokens: 9,
1047
+ },
1048
+ },
1049
+ });
1050
+
1051
+ expect(normalized.summary).toEqual({
1052
+ provider: "openai",
1053
+ model: "gpt-4.1-2026-03-01",
1054
+ inputTokens: 18,
1055
+ outputTokens: 9,
1056
+ cacheCreationInputTokens: undefined,
1057
+ cacheReadInputTokens: undefined,
1058
+ stopReason: "stop",
1059
+ requestMessageCount: undefined,
1060
+ requestToolCount: undefined,
1061
+ responseMessageCount: 1,
1062
+ responseToolCallCount: undefined,
1063
+ responsePreview: "First line Second line Third line",
1064
+ toolCallNames: undefined,
1065
+ });
1066
+ expect(normalized.requestSections).toBeUndefined();
1067
+ expect(normalized.responseSections).toEqual([
1068
+ {
1069
+ kind: "message",
1070
+ label: "Assistant response",
1071
+ role: "assistant",
1072
+ text: "First line\n Second line\n\n Third line",
1073
+ },
1074
+ ]);
1075
+ });
1076
+
1077
+ test("does not mix request and response from different providers", () => {
1078
+ const normalized = normalizeLlmContextPayloads({
1079
+ createdAt: 1_742_400_000_007,
1080
+ requestPayload: {
1081
+ model: "gpt-4.1",
1082
+ tool_choice: "auto",
1083
+ messages: [{ role: "user", content: "Hello" }],
1084
+ tools: [
1085
+ {
1086
+ type: "function",
1087
+ function: {
1088
+ name: "lookup",
1089
+ description: "Lookup a record",
1090
+ parameters: { type: "object" },
1091
+ },
1092
+ },
1093
+ ],
1094
+ },
1095
+ responsePayload: {
1096
+ model: "claude-sonnet-4-6",
1097
+ content: [{ type: "text", text: "Hello back." }],
1098
+ stop_reason: "end_turn",
1099
+ },
1100
+ });
1101
+
1102
+ expect(normalized).toEqual({});
1103
+ });
1104
+
1105
+ test("omits normalized fields for malformed or unknown payloads", () => {
1106
+ const malformed = normalizeLlmContextPayloads({
1107
+ createdAt: 1_742_400_000_003,
1108
+ requestPayload: "not-json",
1109
+ responsePayload: { foo: "bar" },
1110
+ });
1111
+
1112
+ expect(malformed.summary).toBeUndefined();
1113
+ expect(malformed.requestSections).toBeUndefined();
1114
+ expect(malformed.responseSections).toBeUndefined();
1115
+ });
1116
+ });