@vellumai/assistant 0.5.1 → 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 (338) 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__/attachments-store.test.ts +169 -21
  14. package/src/__tests__/attachments.test.ts +115 -1
  15. package/src/__tests__/btw-routes.test.ts +1 -0
  16. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  17. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  18. package/src/__tests__/checker.test.ts +54 -0
  19. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  21. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  22. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  23. package/src/__tests__/config-schema.test.ts +1 -1
  24. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  25. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  26. package/src/__tests__/conversation-attachments.test.ts +17 -19
  27. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  28. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  29. package/src/__tests__/conversation-error.test.ts +1 -1
  30. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  31. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  32. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  33. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  34. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  35. package/src/__tests__/conversation-queue.test.ts +36 -1
  36. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  37. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  38. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  39. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  40. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  41. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  42. package/src/__tests__/conversation-store.test.ts +24 -21
  43. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  44. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  45. package/src/__tests__/conversation-title-service.test.ts +137 -0
  46. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  47. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  48. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  50. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  52. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  53. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  54. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  55. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  56. package/src/__tests__/diagnostics-export.test.ts +70 -1
  57. package/src/__tests__/first-greeting.test.ts +80 -0
  58. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  59. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  60. package/src/__tests__/history-repair.test.ts +32 -10
  61. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  62. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  63. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  64. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  65. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  66. package/src/__tests__/media-generate-image.test.ts +47 -94
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  68. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  69. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  70. package/src/__tests__/migration-export-http.test.ts +3 -1
  71. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  72. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  73. package/src/__tests__/mime-builder.test.ts +3 -2
  74. package/src/__tests__/non-member-access-request.test.ts +12 -1
  75. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  76. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  77. package/src/__tests__/oauth-store.test.ts +115 -0
  78. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  79. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  80. package/src/__tests__/recording-handler.test.ts +17 -0
  81. package/src/__tests__/registry.test.ts +3 -8
  82. package/src/__tests__/relay-server.test.ts +1 -1
  83. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  84. package/src/__tests__/schema-transforms.test.ts +165 -5
  85. package/src/__tests__/server-history-render.test.ts +2 -2
  86. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  87. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  88. package/src/__tests__/starter-task-flow.test.ts +1 -0
  89. package/src/__tests__/suggestion-routes.test.ts +443 -0
  90. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  91. package/src/__tests__/swarm-recursion.test.ts +1 -0
  92. package/src/__tests__/swarm-tool.test.ts +1 -0
  93. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  94. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  95. package/src/__tests__/top-level-renderer.test.ts +22 -0
  96. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  97. package/src/__tests__/web-fetch.test.ts +6 -2
  98. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  99. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  100. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  101. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  102. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  103. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  104. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  105. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  106. package/src/agent/attachments.ts +27 -1
  107. package/src/agent/loop.ts +29 -1
  108. package/src/avatar/traits-png-sync.ts +80 -25
  109. package/src/bundler/app-bundler.ts +4 -4
  110. package/src/calls/call-domain.ts +1 -0
  111. package/src/calls/voice-session-bridge.ts +1 -0
  112. package/src/cli/commands/auth.ts +92 -0
  113. package/src/cli/commands/avatar.ts +7 -6
  114. package/src/cli/commands/config.ts +2 -0
  115. package/src/cli/commands/oauth/providers.ts +29 -0
  116. package/src/cli/program.ts +12 -0
  117. package/src/cli.ts +15 -48
  118. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  119. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  120. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  121. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  135. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  136. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  137. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  138. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  139. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  140. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  141. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  142. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  143. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  144. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  145. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  146. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  147. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  148. package/src/config/bundled-tool-registry.ts +2 -14
  149. package/src/config/feature-flag-registry.json +8 -0
  150. package/src/config/loader.ts +64 -0
  151. package/src/config/raw-config-utils.ts +30 -0
  152. package/src/config/schema-utils.ts +28 -7
  153. package/src/config/schema.ts +8 -0
  154. package/src/config/schemas/elevenlabs.ts +18 -0
  155. package/src/config/schemas/memory-lifecycle.ts +4 -2
  156. package/src/config/schemas/memory-storage.ts +1 -1
  157. package/src/config/schemas/services.ts +8 -6
  158. package/src/contacts/contact-store.ts +13 -6
  159. package/src/contacts/contacts-write.ts +0 -1
  160. package/src/context/window-manager.ts +13 -2
  161. package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
  162. package/src/daemon/conversation-agent-loop.ts +56 -19
  163. package/src/daemon/conversation-attachments.ts +18 -36
  164. package/src/daemon/conversation-error.ts +2 -1
  165. package/src/daemon/conversation-history.ts +18 -4
  166. package/src/daemon/conversation-lifecycle.ts +39 -15
  167. package/src/daemon/conversation-messaging.ts +70 -26
  168. package/src/daemon/conversation-process.ts +58 -34
  169. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  170. package/src/daemon/conversation-slash.ts +121 -256
  171. package/src/daemon/conversation-surfaces.ts +143 -20
  172. package/src/daemon/conversation-tool-setup.ts +0 -6
  173. package/src/daemon/conversation-workspace.ts +21 -1
  174. package/src/daemon/conversation.ts +51 -29
  175. package/src/daemon/first-greeting.ts +35 -0
  176. package/src/daemon/handlers/config-embeddings.ts +148 -0
  177. package/src/daemon/handlers/config-model.ts +71 -26
  178. package/src/daemon/handlers/conversations.ts +0 -23
  179. package/src/daemon/handlers/recording.ts +26 -21
  180. package/src/daemon/host-cu-proxy.ts +2 -2
  181. package/src/daemon/lifecycle.ts +106 -64
  182. package/src/daemon/message-protocol.ts +3 -0
  183. package/src/daemon/message-types/conversations.ts +19 -0
  184. package/src/daemon/message-types/messages.ts +1 -0
  185. package/src/daemon/message-types/shared.ts +2 -0
  186. package/src/daemon/message-types/surfaces.ts +2 -0
  187. package/src/daemon/message-types/upgrades.ts +23 -0
  188. package/src/daemon/server.ts +83 -12
  189. package/src/daemon/shutdown-handlers.ts +8 -5
  190. package/src/daemon/startup-error.ts +9 -0
  191. package/src/daemon/tool-side-effects.ts +11 -28
  192. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  193. package/src/instrument.ts +0 -4
  194. package/src/media/app-icon-generator.ts +2 -2
  195. package/src/memory/app-git-service.ts +28 -16
  196. package/src/memory/app-store.ts +230 -41
  197. package/src/memory/attachments-store.ts +558 -130
  198. package/src/memory/conversation-attention-store.ts +70 -0
  199. package/src/memory/conversation-crud.ts +442 -3
  200. package/src/memory/conversation-directories.ts +125 -0
  201. package/src/memory/conversation-disk-view.ts +390 -0
  202. package/src/memory/conversation-key-store.ts +17 -5
  203. package/src/memory/conversation-queries.ts +5 -1
  204. package/src/memory/conversation-title-service.ts +21 -49
  205. package/src/memory/db-init.ts +28 -0
  206. package/src/memory/embedding-backend.ts +42 -53
  207. package/src/memory/embedding-gemini.test.ts +4 -4
  208. package/src/memory/embedding-local.ts +1 -3
  209. package/src/memory/embedding-ollama.ts +1 -3
  210. package/src/memory/embedding-openai.ts +1 -3
  211. package/src/memory/indexer.ts +9 -7
  212. package/src/memory/items-extractor.ts +42 -13
  213. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  214. package/src/memory/job-handlers/embedding.test.ts +1 -4
  215. package/src/memory/llm-request-log-store.ts +100 -1
  216. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  217. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  218. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  219. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  220. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  221. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  222. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  223. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  224. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  225. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  226. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  227. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  228. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  229. package/src/memory/migrations/index.ts +7 -0
  230. package/src/memory/migrations/registry.ts +13 -0
  231. package/src/memory/retriever.test.ts +601 -2
  232. package/src/memory/retriever.ts +85 -9
  233. package/src/memory/schema/conversations.ts +6 -0
  234. package/src/memory/schema/infrastructure.ts +13 -7
  235. package/src/memory/schema/oauth.ts +6 -0
  236. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  237. package/src/notifications/copy-composer.ts +26 -0
  238. package/src/notifications/decision-engine.ts +14 -1
  239. package/src/notifications/emit-signal.ts +1 -1
  240. package/src/notifications/signal.ts +36 -0
  241. package/src/oauth/byo-connection.test.ts +1 -45
  242. package/src/oauth/byo-connection.ts +2 -8
  243. package/src/oauth/connect-orchestrator.ts +15 -11
  244. package/src/oauth/connection-resolver.test.ts +191 -0
  245. package/src/oauth/connection-resolver.ts +66 -38
  246. package/src/oauth/connection.ts +0 -1
  247. package/src/oauth/oauth-store.ts +97 -47
  248. package/src/oauth/platform-connection.test.ts +0 -1
  249. package/src/oauth/platform-connection.ts +11 -3
  250. package/src/oauth/seed-providers.ts +78 -3
  251. package/src/oauth/token-persistence.ts +16 -10
  252. package/src/permissions/checker.ts +71 -8
  253. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  254. package/src/providers/anthropic/client.ts +8 -1
  255. package/src/providers/failover.ts +4 -1
  256. package/src/providers/gemini/client.ts +50 -0
  257. package/src/providers/model-catalog.ts +92 -0
  258. package/src/providers/model-intents.ts +29 -20
  259. package/src/providers/openai/client.ts +49 -0
  260. package/src/providers/types.ts +2 -0
  261. package/src/runtime/access-request-helper.ts +16 -7
  262. package/src/runtime/auth/credential-service.ts +3 -1
  263. package/src/runtime/auth/route-policy.ts +14 -1
  264. package/src/runtime/btw-sidechain.ts +101 -0
  265. package/src/runtime/channel-reply-delivery.ts +17 -1
  266. package/src/runtime/http-router.ts +3 -1
  267. package/src/runtime/http-server.ts +196 -141
  268. package/src/runtime/http-types.ts +1 -0
  269. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  270. package/src/runtime/routes/access-request-decision.ts +41 -0
  271. package/src/runtime/routes/app-management-routes.ts +6 -3
  272. package/src/runtime/routes/app-routes.ts +7 -3
  273. package/src/runtime/routes/approval-routes.ts +1 -0
  274. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  275. package/src/runtime/routes/attachment-routes.ts +45 -15
  276. package/src/runtime/routes/btw-routes.ts +21 -61
  277. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  278. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  279. package/src/runtime/routes/conversation-routes.ts +222 -28
  280. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  281. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  282. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  283. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  284. package/src/runtime/routes/log-export-routes.ts +3 -0
  285. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  286. package/src/runtime/routes/memory-item-routes.ts +4 -0
  287. package/src/runtime/routes/migration-routes.ts +4 -1
  288. package/src/runtime/routes/oauth-apps.ts +291 -0
  289. package/src/runtime/routes/secret-routes.ts +28 -1
  290. package/src/runtime/routes/settings-routes.ts +14 -0
  291. package/src/runtime/routes/trace-event-routes.ts +4 -1
  292. package/src/schedule/schedule-store.ts +9 -21
  293. package/src/security/secure-keys.ts +21 -0
  294. package/src/signals/bash.ts +1 -1
  295. package/src/swarm/backend-claude-code.ts +3 -6
  296. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  297. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  298. package/src/tools/AGENTS.md +6 -10
  299. package/src/tools/apps/executors.ts +17 -232
  300. package/src/tools/claude-code/claude-code.ts +2 -3
  301. package/src/tools/credentials/vault.ts +7 -12
  302. package/src/tools/host-filesystem/read.ts +13 -10
  303. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  304. package/src/tools/schedule/list.ts +2 -7
  305. package/src/tools/schema-transforms.ts +5 -0
  306. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  307. package/src/tools/skills/execute.ts +1 -1
  308. package/src/tools/tool-manifest.ts +0 -6
  309. package/src/tools/ui-surface/definitions.ts +2 -2
  310. package/src/util/device-id.ts +28 -5
  311. package/src/util/platform.ts +6 -0
  312. package/src/util/pricing.ts +1 -0
  313. package/src/util/retry.ts +1 -3
  314. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  315. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  316. package/src/workspace/migrations/006-services-config.ts +5 -0
  317. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  318. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  319. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  320. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  321. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  322. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  323. package/src/workspace/migrations/registry.ts +10 -0
  324. package/src/workspace/top-level-renderer.ts +12 -0
  325. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  326. package/src/__tests__/asset-search-tool.test.ts +0 -536
  327. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  328. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  329. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  330. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  331. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  332. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  333. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  334. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  335. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  336. package/src/daemon/media-visibility-policy.ts +0 -59
  337. package/src/tools/assets/materialize.ts +0 -248
  338. package/src/tools/assets/search.ts +0 -400
@@ -0,0 +1,1199 @@
1
+ export interface LlmContextNormalizationInput {
2
+ requestPayload: unknown;
3
+ responsePayload: unknown;
4
+ createdAt: number;
5
+ }
6
+
7
+ export interface LlmContextSummary {
8
+ provider: "openai" | "anthropic" | "gemini";
9
+ model?: string;
10
+ inputTokens?: number;
11
+ outputTokens?: number;
12
+ cacheCreationInputTokens?: number;
13
+ cacheReadInputTokens?: number;
14
+ stopReason?: string;
15
+ requestMessageCount?: number;
16
+ requestToolCount?: number;
17
+ responseMessageCount?: number;
18
+ responseToolCallCount?: number;
19
+ responsePreview?: string;
20
+ toolCallNames?: string[];
21
+ }
22
+
23
+ export interface LlmContextSection {
24
+ kind:
25
+ | "system"
26
+ | "message"
27
+ | "reasoning"
28
+ | "settings"
29
+ | "tool_definitions"
30
+ | "tool_use"
31
+ | "tool_result"
32
+ | "function_call"
33
+ | "function_response";
34
+ label: string;
35
+ role?: string;
36
+ text?: string;
37
+ toolName?: string;
38
+ data?: unknown;
39
+ language?: string;
40
+ }
41
+
42
+ export interface LlmContextNormalizationResult {
43
+ summary?: LlmContextSummary;
44
+ requestSections?: LlmContextSection[];
45
+ responseSections?: LlmContextSection[];
46
+ }
47
+
48
+ interface NormalizedPayloadCandidate {
49
+ provider: LlmContextSummary["provider"];
50
+ summary: LlmContextSummary;
51
+ requestSections?: LlmContextSection[];
52
+ responseSections?: LlmContextSection[];
53
+ }
54
+
55
+ export function normalizeLlmContextPayloads(
56
+ input: LlmContextNormalizationInput,
57
+ ): LlmContextNormalizationResult {
58
+ const requestCandidates = [
59
+ normalizeOpenAiRequestPayload(input.requestPayload),
60
+ normalizeAnthropicRequestPayload(input.requestPayload),
61
+ normalizeGeminiRequestPayload(input.requestPayload),
62
+ ].filter((candidate): candidate is NormalizedPayloadCandidate =>
63
+ Boolean(candidate),
64
+ );
65
+ const responseCandidates = [
66
+ normalizeOpenAiResponsePayload(input.responsePayload),
67
+ normalizeAnthropicResponsePayload(input.responsePayload),
68
+ normalizeGeminiResponsePayload(input.responsePayload),
69
+ ].filter((candidate): candidate is NormalizedPayloadCandidate =>
70
+ Boolean(candidate),
71
+ );
72
+
73
+ if (requestCandidates.length > 1 || responseCandidates.length > 1) {
74
+ return {};
75
+ }
76
+
77
+ const requestCandidate = requestCandidates[0];
78
+ const responseCandidate = responseCandidates[0];
79
+
80
+ if (requestCandidate && responseCandidate) {
81
+ if (requestCandidate.provider !== responseCandidate.provider) {
82
+ return {};
83
+ }
84
+
85
+ return mergeNormalizedCandidates(requestCandidate, responseCandidate);
86
+ }
87
+
88
+ if (requestCandidate) {
89
+ return requestCandidate as LlmContextNormalizationResult;
90
+ }
91
+
92
+ if (responseCandidate) {
93
+ const requestCandidate = normalizeCompatibleRequestPayload(
94
+ input.requestPayload,
95
+ responseCandidate.provider,
96
+ );
97
+ if (
98
+ requestCandidate &&
99
+ requestCandidate.provider !== responseCandidate.provider
100
+ ) {
101
+ return {};
102
+ }
103
+
104
+ return mergeNormalizedCandidates(requestCandidate, responseCandidate);
105
+ }
106
+
107
+ return {};
108
+ }
109
+
110
+ function normalizeOpenAiRequestPayload(
111
+ requestPayload: unknown,
112
+ allowPlainText = false,
113
+ ): NormalizedPayloadCandidate | null {
114
+ const request = asRecord(requestPayload);
115
+ if (!request) {
116
+ return null;
117
+ }
118
+
119
+ const messages = asRecordArray(request.messages);
120
+ if (!messages) {
121
+ return null;
122
+ }
123
+
124
+ const requestToolNames = extractOpenAiRequestToolNames(request.tools);
125
+ const hasOpenAiSignal =
126
+ requestToolNames.length > 0 ||
127
+ asString(request.tool_choice) !== undefined ||
128
+ (request.parallel_tool_calls !== undefined &&
129
+ typeof request.parallel_tool_calls === "boolean") ||
130
+ messages.some((message) => Boolean(asRecordArray(message.tool_calls)));
131
+ if (!allowPlainText && !hasOpenAiSignal) {
132
+ return null;
133
+ }
134
+
135
+ const requestSections: LlmContextSection[] = [];
136
+ for (const [index, message] of messages.entries()) {
137
+ const role = asString(message.role) ?? "unknown";
138
+ const messageText = extractOpenAiContentText(message.content);
139
+ if (messageText !== undefined) {
140
+ requestSections.push({
141
+ kind:
142
+ role === "system"
143
+ ? "system"
144
+ : role === "tool"
145
+ ? "tool_result"
146
+ : "message",
147
+ label: buildMessageLabel(role, index + 1),
148
+ role,
149
+ text: messageText,
150
+ });
151
+ }
152
+
153
+ for (const toolCallSection of openAiToolCallSections(
154
+ message.tool_calls,
155
+ "Request tool call",
156
+ )) {
157
+ requestSections.push(toolCallSection);
158
+ }
159
+ }
160
+
161
+ if (requestToolNames.length > 0) {
162
+ requestSections.push({
163
+ kind: "tool_definitions",
164
+ label: "Available tools",
165
+ data: {
166
+ tools: asRecordArray(request.tools) ?? request.tools,
167
+ },
168
+ language: "json",
169
+ });
170
+ }
171
+
172
+ const requestSettings = omitRecordKeys(request, ["messages", "tools"]);
173
+ if (hasMeaningfulRequestSettings(requestSettings)) {
174
+ requestSections.push(
175
+ structuredJsonSection("settings", "Request settings", requestSettings),
176
+ );
177
+ }
178
+
179
+ return {
180
+ provider: "openai",
181
+ summary: {
182
+ provider: "openai",
183
+ model: asString(request.model),
184
+ inputTokens: undefined,
185
+ outputTokens: undefined,
186
+ cacheCreationInputTokens: undefined,
187
+ cacheReadInputTokens: undefined,
188
+ stopReason: undefined,
189
+ requestMessageCount: messages.length,
190
+ requestToolCount: requestToolNames.length,
191
+ responseMessageCount: undefined,
192
+ responseToolCallCount: undefined,
193
+ responsePreview: undefined,
194
+ toolCallNames: undefined,
195
+ },
196
+ requestSections: requestSections.length > 0 ? requestSections : undefined,
197
+ };
198
+ }
199
+
200
+ function normalizeOpenAiResponsePayload(
201
+ responsePayload: unknown,
202
+ ): NormalizedPayloadCandidate | null {
203
+ const response = asRecord(responsePayload);
204
+ if (!response) {
205
+ return null;
206
+ }
207
+
208
+ const choices = asRecordArray(response.choices);
209
+ if (!choices) {
210
+ return null;
211
+ }
212
+
213
+ const firstChoice = choices[0];
214
+ const responseMessage = asRecord(firstChoice?.message);
215
+ const responseText = extractOpenAiContentText(responseMessage?.content);
216
+ const responseSections: LlmContextSection[] = [];
217
+ if (responseText !== undefined) {
218
+ responseSections.push({
219
+ kind: "message",
220
+ label: "Assistant response",
221
+ role: asString(responseMessage?.role) ?? "assistant",
222
+ text: responseText,
223
+ });
224
+ }
225
+ const responseToolSections = openAiToolCallSections(
226
+ responseMessage?.tool_calls,
227
+ "Response tool call",
228
+ );
229
+ responseSections.push(...responseToolSections);
230
+
231
+ const usage = asRecord(response.usage);
232
+ const toolCallNames = responseToolSections
233
+ .map((section) => section.toolName)
234
+ .filter((name): name is string => typeof name === "string");
235
+
236
+ return {
237
+ provider: "openai",
238
+ summary: {
239
+ provider: "openai",
240
+ model: asString(response.model),
241
+ inputTokens: asNumber(usage?.prompt_tokens),
242
+ outputTokens: asNumber(usage?.completion_tokens),
243
+ cacheCreationInputTokens: undefined,
244
+ cacheReadInputTokens: undefined,
245
+ stopReason: asString(firstChoice?.finish_reason),
246
+ requestMessageCount: undefined,
247
+ requestToolCount: undefined,
248
+ responseMessageCount:
249
+ responseText !== undefined || responseToolSections.length > 0
250
+ ? 1
251
+ : undefined,
252
+ responseToolCallCount:
253
+ responseToolSections.length > 0
254
+ ? responseToolSections.length
255
+ : undefined,
256
+ responsePreview: responseText ? truncateText(responseText) : undefined,
257
+ toolCallNames: toolCallNames.length > 0 ? toolCallNames : undefined,
258
+ },
259
+ responseSections:
260
+ responseSections.length > 0 ? responseSections : undefined,
261
+ };
262
+ }
263
+
264
+ function normalizeAnthropicRequestPayload(
265
+ requestPayload: unknown,
266
+ allowPlainText = false,
267
+ ): NormalizedPayloadCandidate | null {
268
+ const request = asRecord(requestPayload);
269
+ if (!request) {
270
+ return null;
271
+ }
272
+
273
+ const messages = asRecordArray(request.messages);
274
+ if (!messages) {
275
+ return null;
276
+ }
277
+
278
+ const requestToolNames = extractAnthropicToolNames(request.tools);
279
+ const hasAnthropicContentSignal = messages.some((message) =>
280
+ (asRecordArray(message.content) ?? []).some((block) => {
281
+ const type = asString(block.type);
282
+ return (
283
+ type === "document" ||
284
+ type === "tool_use" ||
285
+ type === "server_tool_use" ||
286
+ type === "tool_result" ||
287
+ type === "web_search_tool_result" ||
288
+ type === "thinking" ||
289
+ type === "redacted_thinking"
290
+ );
291
+ }),
292
+ );
293
+ const hasAnthropicSignal =
294
+ request.system !== undefined ||
295
+ requestToolNames.length > 0 ||
296
+ isAnthropicToolChoice(request.tool_choice) ||
297
+ hasAnthropicContentSignal;
298
+ if (!allowPlainText && !hasAnthropicSignal) {
299
+ return null;
300
+ }
301
+
302
+ const requestSections: LlmContextSection[] = [];
303
+ const systemSections = anthropicSystemSections(request.system);
304
+ requestSections.push(...systemSections);
305
+
306
+ for (const [index, message] of messages.entries()) {
307
+ requestSections.push(
308
+ ...anthropicMessageSections(
309
+ message,
310
+ buildMessageLabel(asString(message.role) ?? "unknown", index + 1),
311
+ ),
312
+ );
313
+ }
314
+
315
+ if (requestToolNames.length > 0) {
316
+ requestSections.push({
317
+ kind: "tool_definitions",
318
+ label: "Available tools",
319
+ data: {
320
+ tools: asRecordArray(request.tools) ?? request.tools,
321
+ },
322
+ language: "json",
323
+ });
324
+ }
325
+
326
+ const requestSettings = omitRecordKeys(request, [
327
+ "system",
328
+ "messages",
329
+ "tools",
330
+ ]);
331
+ if (hasMeaningfulRequestSettings(requestSettings)) {
332
+ requestSections.push(
333
+ structuredJsonSection("settings", "Request settings", requestSettings),
334
+ );
335
+ }
336
+
337
+ return {
338
+ provider: "anthropic",
339
+ summary: {
340
+ provider: "anthropic",
341
+ model: asString(request.model),
342
+ inputTokens: undefined,
343
+ outputTokens: undefined,
344
+ cacheCreationInputTokens: undefined,
345
+ cacheReadInputTokens: undefined,
346
+ stopReason: undefined,
347
+ requestMessageCount: messages.length,
348
+ requestToolCount: requestToolNames.length,
349
+ responseMessageCount: undefined,
350
+ responseToolCallCount: undefined,
351
+ responsePreview: undefined,
352
+ toolCallNames: undefined,
353
+ },
354
+ requestSections: requestSections.length > 0 ? requestSections : undefined,
355
+ };
356
+ }
357
+
358
+ function normalizeAnthropicResponsePayload(
359
+ responsePayload: unknown,
360
+ ): NormalizedPayloadCandidate | null {
361
+ const response = asRecord(responsePayload);
362
+ if (!response) {
363
+ return null;
364
+ }
365
+
366
+ const content = asRecordArray(response.content);
367
+ if (!content) {
368
+ return null;
369
+ }
370
+
371
+ const responseSections = anthropicContentSections(
372
+ content,
373
+ "Assistant response",
374
+ );
375
+ const responseText = collectAnthropicPreviewText(content);
376
+ const responseToolNames = content
377
+ .map((block) =>
378
+ isAnthropicToolUseType(asString(block.type))
379
+ ? asString(block.name)
380
+ : undefined,
381
+ )
382
+ .filter((name): name is string => typeof name === "string");
383
+ const hasAnthropicResponseMessage = responseSections.some(
384
+ (section) =>
385
+ section.kind === "message" ||
386
+ section.kind === "tool_use" ||
387
+ section.kind === "reasoning",
388
+ );
389
+
390
+ const usage = asRecord(response.usage);
391
+ return {
392
+ provider: "anthropic",
393
+ summary: {
394
+ provider: "anthropic",
395
+ model: asString(response.model),
396
+ inputTokens: asNumber(usage?.input_tokens),
397
+ outputTokens: asNumber(usage?.output_tokens),
398
+ cacheCreationInputTokens: asNumber(usage?.cache_creation_input_tokens),
399
+ cacheReadInputTokens: asNumber(usage?.cache_read_input_tokens),
400
+ stopReason: asString(response.stop_reason),
401
+ requestMessageCount: undefined,
402
+ requestToolCount: undefined,
403
+ responseMessageCount: hasAnthropicResponseMessage ? 1 : undefined,
404
+ responseToolCallCount:
405
+ responseToolNames.length > 0 ? responseToolNames.length : undefined,
406
+ responsePreview: responseText ? truncateText(responseText) : undefined,
407
+ toolCallNames:
408
+ responseToolNames.length > 0 ? responseToolNames : undefined,
409
+ },
410
+ responseSections:
411
+ responseSections.length > 0 ? responseSections : undefined,
412
+ };
413
+ }
414
+
415
+ function normalizeGeminiRequestPayload(
416
+ requestPayload: unknown,
417
+ ): NormalizedPayloadCandidate | null {
418
+ const request = asRecord(requestPayload);
419
+ if (!request) {
420
+ return null;
421
+ }
422
+
423
+ const contents = asRecordArray(request.contents);
424
+ if (!contents) {
425
+ return null;
426
+ }
427
+
428
+ const requestSections: LlmContextSection[] = [];
429
+ const config = asRecord(request.config);
430
+ const systemText = extractGeminiSystemInstructionText(
431
+ config?.systemInstruction,
432
+ );
433
+ if (systemText !== undefined) {
434
+ requestSections.push({
435
+ kind: "system",
436
+ label: "System instruction",
437
+ role: "system",
438
+ text: systemText,
439
+ });
440
+ }
441
+
442
+ for (const [index, content] of contents.entries()) {
443
+ requestSections.push(...geminiContentSections(content, index + 1));
444
+ }
445
+
446
+ const requestToolNames = extractGeminiToolNames(config?.tools);
447
+ if (requestToolNames.length > 0) {
448
+ requestSections.push({
449
+ kind: "tool_definitions",
450
+ label: "Available tools",
451
+ data: {
452
+ tools: asRecordArray(config?.tools) ?? config?.tools,
453
+ },
454
+ language: "json",
455
+ });
456
+ }
457
+
458
+ const requestSettings = buildGeminiRequestSettings(request, config);
459
+ if (hasMeaningfulRequestSettings(requestSettings)) {
460
+ requestSections.push(
461
+ structuredJsonSection("settings", "Generation config", requestSettings),
462
+ );
463
+ }
464
+
465
+ return {
466
+ provider: "gemini",
467
+ summary: {
468
+ provider: "gemini",
469
+ model: asString(request.model),
470
+ inputTokens: undefined,
471
+ outputTokens: undefined,
472
+ cacheCreationInputTokens: undefined,
473
+ cacheReadInputTokens: undefined,
474
+ stopReason: undefined,
475
+ requestMessageCount: contents.length,
476
+ requestToolCount: requestToolNames.length,
477
+ responseMessageCount: undefined,
478
+ responseToolCallCount: undefined,
479
+ responsePreview: undefined,
480
+ toolCallNames: undefined,
481
+ },
482
+ requestSections: requestSections.length > 0 ? requestSections : undefined,
483
+ };
484
+ }
485
+
486
+ function normalizeGeminiResponsePayload(
487
+ responsePayload: unknown,
488
+ ): NormalizedPayloadCandidate | null {
489
+ const response = asRecord(responsePayload);
490
+ if (!response) {
491
+ return null;
492
+ }
493
+
494
+ const responseText = asString(response.text);
495
+ const responseFunctionSections = geminiFunctionCallSections(
496
+ response.functionCalls,
497
+ "Response function call",
498
+ );
499
+ const usage = asRecord(response.usageMetadata);
500
+ if (responseText === undefined && responseFunctionSections.length === 0) {
501
+ return null;
502
+ }
503
+
504
+ const responseSections: LlmContextSection[] = [];
505
+ if (responseText !== undefined) {
506
+ responseSections.push({
507
+ kind: "message",
508
+ label: "Assistant response",
509
+ role: "model",
510
+ text: responseText,
511
+ });
512
+ }
513
+ responseSections.push(...responseFunctionSections);
514
+
515
+ const toolCallNames = responseFunctionSections
516
+ .map((section) => section.toolName)
517
+ .filter((name): name is string => typeof name === "string");
518
+
519
+ return {
520
+ provider: "gemini",
521
+ summary: {
522
+ provider: "gemini",
523
+ model: asString(response.model),
524
+ inputTokens: asNumber(usage?.promptTokenCount),
525
+ outputTokens: asNumber(usage?.candidatesTokenCount),
526
+ cacheCreationInputTokens: undefined,
527
+ cacheReadInputTokens: undefined,
528
+ stopReason: asString(response.finishReason),
529
+ requestMessageCount: undefined,
530
+ requestToolCount: undefined,
531
+ responseMessageCount:
532
+ responseText !== undefined || responseFunctionSections.length > 0
533
+ ? 1
534
+ : undefined,
535
+ responseToolCallCount:
536
+ responseFunctionSections.length > 0
537
+ ? responseFunctionSections.length
538
+ : undefined,
539
+ responsePreview: responseText ? truncateText(responseText) : undefined,
540
+ toolCallNames: toolCallNames.length > 0 ? toolCallNames : undefined,
541
+ },
542
+ responseSections:
543
+ responseSections.length > 0 ? responseSections : undefined,
544
+ };
545
+ }
546
+
547
+ function anthropicSystemSections(system: unknown): LlmContextSection[] {
548
+ const text = extractAnthropicSystemText(system);
549
+ if (!text) {
550
+ return [];
551
+ }
552
+ return [
553
+ {
554
+ kind: "system",
555
+ label: "System prompt",
556
+ role: "system",
557
+ text,
558
+ },
559
+ ];
560
+ }
561
+
562
+ function anthropicMessageSections(
563
+ message: Record<string, unknown>,
564
+ label: string,
565
+ ): LlmContextSection[] {
566
+ const role = asString(message.role) ?? "unknown";
567
+ const content = message.content;
568
+ const sections: LlmContextSection[] = [];
569
+ const text = collectAnthropicMessageText(content);
570
+ if (text) {
571
+ sections.push({
572
+ kind: "message",
573
+ label,
574
+ role,
575
+ text,
576
+ });
577
+ }
578
+
579
+ for (const block of asRecordArray(content) ?? []) {
580
+ const type = asString(block.type);
581
+ if (type === "thinking" || type === "redacted_thinking") {
582
+ sections.push({
583
+ kind: "reasoning",
584
+ label: `${label} reasoning`,
585
+ role,
586
+ text: collectAnthropicReasoningText(block),
587
+ });
588
+ continue;
589
+ }
590
+
591
+ if (isAnthropicToolUseType(type)) {
592
+ sections.push({
593
+ kind: "tool_use",
594
+ label: `${label} tool use`,
595
+ role,
596
+ toolName: asString(block.name),
597
+ data: asRecord(block.input) ?? block.input,
598
+ text: previewStructuredValue(block.input),
599
+ });
600
+ continue;
601
+ }
602
+
603
+ if (isAnthropicToolResultType(type)) {
604
+ sections.push({
605
+ kind: "tool_result",
606
+ label: `${label} tool result`,
607
+ role,
608
+ toolName: asString(block.name) ?? asString(block.tool_use_id),
609
+ data:
610
+ type === "web_search_tool_result"
611
+ ? sanitizeAnthropicWebSearchToolResultData(block)
612
+ : undefined,
613
+ text: collectAnthropicToolResultText(block),
614
+ });
615
+ }
616
+ }
617
+
618
+ return sections;
619
+ }
620
+
621
+ function anthropicContentSections(
622
+ content: Record<string, unknown>[],
623
+ label: string,
624
+ ): LlmContextSection[] {
625
+ return anthropicMessageSections(
626
+ {
627
+ role: "assistant",
628
+ content,
629
+ },
630
+ label,
631
+ );
632
+ }
633
+
634
+ function geminiContentSections(
635
+ content: Record<string, unknown>,
636
+ index: number,
637
+ ): LlmContextSection[] {
638
+ const role = asString(content.role) ?? "unknown";
639
+ const parts = asRecordArray(content.parts) ?? [];
640
+ const sections: LlmContextSection[] = [];
641
+ const textParts: string[] = [];
642
+
643
+ for (const part of parts) {
644
+ const text = asString(part.text);
645
+ if (text) {
646
+ textParts.push(text);
647
+ continue;
648
+ }
649
+
650
+ const inlineData = asRecord(part.inlineData);
651
+ if (inlineData) {
652
+ const mimeType =
653
+ asString(inlineData.mimeType) ?? "application/octet-stream";
654
+ textParts.push(`[inline data: ${mimeType}]`);
655
+ continue;
656
+ }
657
+
658
+ const functionCall = asRecord(part.functionCall);
659
+ if (functionCall) {
660
+ sections.push({
661
+ kind: "function_call",
662
+ label: `${buildMessageLabel(role, index)} function call`,
663
+ role,
664
+ toolName: asString(functionCall.name),
665
+ data: asRecord(functionCall.args) ?? functionCall.args,
666
+ text: previewStructuredValue(functionCall.args),
667
+ });
668
+ continue;
669
+ }
670
+
671
+ const functionResponse = asRecord(part.functionResponse);
672
+ if (functionResponse) {
673
+ sections.push({
674
+ kind: "function_response",
675
+ label: `${buildMessageLabel(role, index)} function response`,
676
+ role,
677
+ toolName: asString(functionResponse.name),
678
+ data: asRecord(functionResponse.response) ?? functionResponse.response,
679
+ text: previewStructuredValue(functionResponse.response),
680
+ });
681
+ }
682
+ }
683
+
684
+ const text = joinTextParts(textParts);
685
+ if (text) {
686
+ sections.unshift({
687
+ kind: "message",
688
+ label: buildMessageLabel(role, index),
689
+ role,
690
+ text,
691
+ });
692
+ }
693
+
694
+ return sections;
695
+ }
696
+
697
+ function openAiToolCallSections(
698
+ toolCalls: unknown,
699
+ labelPrefix: string,
700
+ ): LlmContextSection[] {
701
+ return (asRecordArray(toolCalls) ?? []).map((toolCall, index) => {
702
+ const fn = asRecord(toolCall.function);
703
+ return {
704
+ kind: "function_call",
705
+ label: `${labelPrefix} ${index + 1}`,
706
+ role: "assistant",
707
+ toolName: asString(fn?.name),
708
+ data: parseJsonValue(asString(fn?.arguments)),
709
+ text: previewStructuredValue(parseJsonValue(asString(fn?.arguments))),
710
+ };
711
+ });
712
+ }
713
+
714
+ function geminiFunctionCallSections(
715
+ functionCalls: unknown,
716
+ labelPrefix: string,
717
+ ): LlmContextSection[] {
718
+ return (asRecordArray(functionCalls) ?? []).map((call, index) => ({
719
+ kind: "function_call",
720
+ label: `${labelPrefix} ${index + 1}`,
721
+ role: "model",
722
+ toolName: asString(call.name),
723
+ data: asRecord(call.args) ?? call.args,
724
+ text: previewStructuredValue(call.args),
725
+ }));
726
+ }
727
+
728
+ function extractOpenAiRequestToolNames(tools: unknown): string[] {
729
+ return (asRecordArray(tools) ?? [])
730
+ .map((tool) => asString(asRecord(tool.function)?.name))
731
+ .filter((name): name is string => typeof name === "string");
732
+ }
733
+
734
+ function extractAnthropicToolNames(tools: unknown): string[] {
735
+ return (asRecordArray(tools) ?? [])
736
+ .map((tool) => asString(tool.name))
737
+ .filter((name): name is string => typeof name === "string");
738
+ }
739
+
740
+ function isAnthropicToolChoice(toolChoice: unknown): boolean {
741
+ const record = asRecord(toolChoice);
742
+ if (!record) {
743
+ return false;
744
+ }
745
+
746
+ const type = asString(record.type);
747
+ return (
748
+ type === "auto" || type === "any" || type === "tool" || type === "none"
749
+ );
750
+ }
751
+
752
+ function extractGeminiToolNames(tools: unknown): string[] {
753
+ const toolGroups = asRecordArray(tools) ?? [];
754
+ const names: string[] = [];
755
+ for (const toolGroup of toolGroups) {
756
+ for (const declaration of asRecordArray(toolGroup.functionDeclarations) ??
757
+ []) {
758
+ const name = asString(declaration.name);
759
+ if (name) {
760
+ names.push(name);
761
+ }
762
+ }
763
+ }
764
+ return names;
765
+ }
766
+
767
+ function extractOpenAiContentText(content: unknown): string | undefined {
768
+ if (typeof content === "string") {
769
+ return hasMeaningfulText(content) ? content : undefined;
770
+ }
771
+
772
+ const parts = asRecordArray(content);
773
+ if (!parts) {
774
+ return undefined;
775
+ }
776
+
777
+ const textParts: string[] = [];
778
+ for (const part of parts) {
779
+ const type = asString(part.type);
780
+ if (type === "text" || type === "input_text" || type === "output_text") {
781
+ const text = asString(part.text);
782
+ if (text) {
783
+ textParts.push(text);
784
+ }
785
+ continue;
786
+ }
787
+
788
+ if (type === "image_url" || type === "input_image") {
789
+ textParts.push("[image]");
790
+ continue;
791
+ }
792
+
793
+ if (type === "file") {
794
+ textParts.push("[file]");
795
+ }
796
+ }
797
+
798
+ return joinTextParts(textParts);
799
+ }
800
+
801
+ function extractAnthropicSystemText(system: unknown): string | undefined {
802
+ if (typeof system === "string") {
803
+ return hasMeaningfulText(system) ? system : undefined;
804
+ }
805
+
806
+ const parts = asRecordArray(system);
807
+ if (!parts) {
808
+ return undefined;
809
+ }
810
+
811
+ const textParts = parts
812
+ .map((part) => asString(part.text))
813
+ .filter((text): text is string => typeof text === "string");
814
+ return joinTextParts(textParts);
815
+ }
816
+
817
+ function extractGeminiSystemInstructionText(
818
+ systemInstruction: unknown,
819
+ ): string | undefined {
820
+ if (typeof systemInstruction === "string") {
821
+ return hasMeaningfulText(systemInstruction) ? systemInstruction : undefined;
822
+ }
823
+
824
+ const record = asRecord(systemInstruction);
825
+ if (!record) {
826
+ return undefined;
827
+ }
828
+
829
+ const parts = asRecordArray(record.parts) ?? [];
830
+ const textParts = parts
831
+ .map((part) => asString(part.text))
832
+ .filter((text): text is string => typeof text === "string");
833
+ return joinTextParts(textParts);
834
+ }
835
+
836
+ function collectAnthropicText(content: unknown): string | undefined {
837
+ if (typeof content === "string") {
838
+ return hasMeaningfulText(content) ? content : undefined;
839
+ }
840
+
841
+ const blocks = asRecordArray(content);
842
+ if (!blocks) {
843
+ return undefined;
844
+ }
845
+
846
+ const textParts: string[] = [];
847
+ for (const block of blocks) {
848
+ const text = collectAnthropicBlockText(block);
849
+ if (text) {
850
+ textParts.push(text);
851
+ }
852
+ }
853
+
854
+ return joinTextParts(textParts);
855
+ }
856
+
857
+ function collectAnthropicMessageText(content: unknown): string | undefined {
858
+ if (typeof content === "string") {
859
+ return hasMeaningfulText(content) ? content : undefined;
860
+ }
861
+
862
+ const blocks = asRecordArray(content);
863
+ if (!blocks) {
864
+ return undefined;
865
+ }
866
+
867
+ const textParts: string[] = [];
868
+ for (const block of blocks) {
869
+ const text = collectAnthropicBlockText(block);
870
+ if (text) {
871
+ textParts.push(text);
872
+ }
873
+ }
874
+
875
+ return joinTextParts(textParts);
876
+ }
877
+
878
+ function collectAnthropicPreviewText(content: unknown): string | undefined {
879
+ if (typeof content === "string") {
880
+ return hasMeaningfulText(content) ? content : undefined;
881
+ }
882
+
883
+ const blocks = asRecordArray(content);
884
+ if (!blocks) {
885
+ return undefined;
886
+ }
887
+
888
+ const textParts: string[] = [];
889
+ for (const block of blocks) {
890
+ if (asString(block.type) !== "text") {
891
+ continue;
892
+ }
893
+
894
+ const text = asString(block.text);
895
+ if (text) {
896
+ textParts.push(text);
897
+ }
898
+ }
899
+
900
+ return joinTextParts(textParts);
901
+ }
902
+
903
+ function collectAnthropicBlockText(
904
+ block: Record<string, unknown>,
905
+ ): string | undefined {
906
+ const type = asString(block.type);
907
+ if (type === "text") {
908
+ const text = asString(block.text);
909
+ return text ? text : undefined;
910
+ }
911
+
912
+ if (type === "image") {
913
+ return "[image]";
914
+ }
915
+
916
+ if (type === "document") {
917
+ const title = asString(block.title);
918
+ return title ? `[document: ${title}]` : "[document]";
919
+ }
920
+
921
+ return undefined;
922
+ }
923
+
924
+ function collectAnthropicReasoningText(
925
+ block: Record<string, unknown>,
926
+ ): string | undefined {
927
+ const type = asString(block.type);
928
+ if (type === "thinking") {
929
+ const thinking = asString(block.thinking);
930
+ return thinking ? thinking : undefined;
931
+ }
932
+
933
+ if (type === "redacted_thinking") {
934
+ return "[redacted thinking]";
935
+ }
936
+
937
+ return undefined;
938
+ }
939
+
940
+ function isAnthropicToolUseType(type: string | undefined): boolean {
941
+ return type === "tool_use" || type === "server_tool_use";
942
+ }
943
+
944
+ function isAnthropicToolResultType(type: string | undefined): boolean {
945
+ return type === "tool_result" || type === "web_search_tool_result";
946
+ }
947
+
948
+ function collectAnthropicToolResultText(
949
+ block: Record<string, unknown>,
950
+ ): string | undefined {
951
+ if (asString(block.type) === "web_search_tool_result") {
952
+ return "[Web search results]";
953
+ }
954
+ return collectAnthropicText(block.content);
955
+ }
956
+
957
+ function sanitizeAnthropicWebSearchToolResultData(
958
+ block: Record<string, unknown>,
959
+ ): unknown {
960
+ return sanitizeAnthropicStructuredValue(block);
961
+ }
962
+
963
+ function sanitizeAnthropicStructuredValue(value: unknown): unknown {
964
+ if (Array.isArray(value)) {
965
+ return value.map((entry) => sanitizeAnthropicStructuredValue(entry));
966
+ }
967
+
968
+ const record = asRecord(value);
969
+ if (!record) {
970
+ return value;
971
+ }
972
+
973
+ return Object.fromEntries(
974
+ Object.entries(record)
975
+ .filter(
976
+ ([key, entryValue]) =>
977
+ key !== "encrypted_content" && entryValue !== undefined,
978
+ )
979
+ .map(([key, entryValue]) => [
980
+ key,
981
+ sanitizeAnthropicStructuredValue(entryValue),
982
+ ]),
983
+ );
984
+ }
985
+
986
+ function buildMessageLabel(role: string, index: number): string {
987
+ const capitalizedRole =
988
+ role.length > 0 ? role[0]!.toUpperCase() + role.slice(1) : "Message";
989
+ if (role === "system") {
990
+ return "System prompt";
991
+ }
992
+ return `${capitalizedRole} message ${index}`;
993
+ }
994
+
995
+ function buildGeminiRequestSettings(
996
+ request: Record<string, unknown>,
997
+ config: Record<string, unknown> | null,
998
+ ): Record<string, unknown> | undefined {
999
+ const topLevelSettings = omitRecordKeys(request, ["contents", "config"]);
1000
+ const configSettings = omitRecordKeys(config, ["systemInstruction", "tools"]);
1001
+
1002
+ if (!topLevelSettings && !configSettings) {
1003
+ return undefined;
1004
+ }
1005
+
1006
+ return {
1007
+ ...(topLevelSettings ?? {}),
1008
+ ...(configSettings ? { config: configSettings } : {}),
1009
+ };
1010
+ }
1011
+
1012
+ function structuredJsonSection(
1013
+ kind: LlmContextSection["kind"],
1014
+ label: string,
1015
+ data: unknown,
1016
+ ): LlmContextSection {
1017
+ return {
1018
+ kind,
1019
+ label,
1020
+ data,
1021
+ language: "json",
1022
+ };
1023
+ }
1024
+
1025
+ function hasMeaningfulRequestSettings(
1026
+ settings: Record<string, unknown> | undefined,
1027
+ ): settings is Record<string, unknown> {
1028
+ if (!settings) {
1029
+ return false;
1030
+ }
1031
+
1032
+ const keys = Object.keys(settings);
1033
+ return !(keys.length === 1 && keys[0] === "model");
1034
+ }
1035
+
1036
+ function omitRecordKeys(
1037
+ record: Record<string, unknown> | null,
1038
+ omittedKeys: string[],
1039
+ ): Record<string, unknown> | undefined {
1040
+ if (!record) {
1041
+ return undefined;
1042
+ }
1043
+
1044
+ const filteredEntries = Object.entries(record).filter(
1045
+ ([key, value]) => !omittedKeys.includes(key) && value !== undefined,
1046
+ );
1047
+ if (filteredEntries.length === 0) {
1048
+ return undefined;
1049
+ }
1050
+
1051
+ return Object.fromEntries(filteredEntries);
1052
+ }
1053
+
1054
+ function previewStructuredValue(value: unknown): string | undefined {
1055
+ if (value === undefined) {
1056
+ return undefined;
1057
+ }
1058
+ if (typeof value === "string") {
1059
+ return truncateText(value);
1060
+ }
1061
+ try {
1062
+ return truncateText(JSON.stringify(value));
1063
+ } catch {
1064
+ return undefined;
1065
+ }
1066
+ }
1067
+
1068
+ function parseJsonValue(value: string | undefined): unknown {
1069
+ if (!value) {
1070
+ return undefined;
1071
+ }
1072
+ try {
1073
+ return JSON.parse(value);
1074
+ } catch {
1075
+ return value;
1076
+ }
1077
+ }
1078
+
1079
+ function joinTextParts(parts: string[]): string | undefined {
1080
+ if (parts.length === 0) {
1081
+ return undefined;
1082
+ }
1083
+ const text = parts.join("\n\n");
1084
+ return hasMeaningfulText(text) ? text : undefined;
1085
+ }
1086
+
1087
+ function truncateText(text: string, maxLength = 280): string {
1088
+ const normalized = normalizeText(text);
1089
+ if (normalized.length <= maxLength) {
1090
+ return normalized;
1091
+ }
1092
+ return `${normalized.slice(0, maxLength - 3).trimEnd()}...`;
1093
+ }
1094
+
1095
+ function normalizeText(text: string): string {
1096
+ const normalized = text.replace(/\s+/g, " ").trim();
1097
+ return normalized.length > 0 ? normalized : "";
1098
+ }
1099
+
1100
+ function hasMeaningfulText(text: string): boolean {
1101
+ return text.trim().length > 0;
1102
+ }
1103
+
1104
+ function mergeSummaryFragments(
1105
+ requestSummary: LlmContextSummary | undefined,
1106
+ responseSummary: LlmContextSummary | undefined,
1107
+ ): LlmContextSummary | undefined {
1108
+ if (!requestSummary && !responseSummary) {
1109
+ return undefined;
1110
+ }
1111
+
1112
+ const summary = {
1113
+ ...(requestSummary ?? responseSummary)!,
1114
+ } as LlmContextSummary;
1115
+
1116
+ if (!requestSummary || !responseSummary) {
1117
+ return summary;
1118
+ }
1119
+
1120
+ for (const [key, value] of Object.entries(responseSummary) as [
1121
+ keyof LlmContextSummary,
1122
+ LlmContextSummary[keyof LlmContextSummary],
1123
+ ][]) {
1124
+ if (value !== undefined) {
1125
+ summary[key] = value as never;
1126
+ }
1127
+ }
1128
+
1129
+ return summary;
1130
+ }
1131
+
1132
+ function mergeNormalizedCandidates(
1133
+ requestCandidate: NormalizedPayloadCandidate | null | undefined,
1134
+ responseCandidate: NormalizedPayloadCandidate | null | undefined,
1135
+ ): LlmContextNormalizationResult {
1136
+ if (!requestCandidate && !responseCandidate) {
1137
+ return {};
1138
+ }
1139
+
1140
+ const requestSections = [
1141
+ ...(requestCandidate?.requestSections ?? []),
1142
+ ...(responseCandidate?.requestSections ?? []),
1143
+ ];
1144
+ const responseSections = [
1145
+ ...(requestCandidate?.responseSections ?? []),
1146
+ ...(responseCandidate?.responseSections ?? []),
1147
+ ];
1148
+
1149
+ return {
1150
+ summary: mergeSummaryFragments(
1151
+ requestCandidate?.summary,
1152
+ responseCandidate?.summary,
1153
+ ),
1154
+ requestSections: requestSections.length > 0 ? requestSections : undefined,
1155
+ responseSections:
1156
+ responseSections.length > 0 ? responseSections : undefined,
1157
+ };
1158
+ }
1159
+
1160
+ function normalizeCompatibleRequestPayload(
1161
+ requestPayload: unknown,
1162
+ provider: LlmContextSummary["provider"],
1163
+ ): NormalizedPayloadCandidate | null {
1164
+ switch (provider) {
1165
+ case "openai":
1166
+ return normalizeOpenAiRequestPayload(requestPayload, true);
1167
+ case "anthropic":
1168
+ return normalizeAnthropicRequestPayload(requestPayload, true);
1169
+ case "gemini":
1170
+ return normalizeGeminiRequestPayload(requestPayload);
1171
+ }
1172
+ }
1173
+
1174
+ function asRecord(value: unknown): Record<string, unknown> | null {
1175
+ if (typeof value !== "object" || value == null || Array.isArray(value)) {
1176
+ return null;
1177
+ }
1178
+ return value as Record<string, unknown>;
1179
+ }
1180
+
1181
+ function asRecordArray(value: unknown): Record<string, unknown>[] | null {
1182
+ if (!Array.isArray(value)) {
1183
+ return null;
1184
+ }
1185
+ return value.filter(
1186
+ (entry): entry is Record<string, unknown> =>
1187
+ typeof entry === "object" && entry != null && !Array.isArray(entry),
1188
+ );
1189
+ }
1190
+
1191
+ function asString(value: unknown): string | undefined {
1192
+ return typeof value === "string" ? value : undefined;
1193
+ }
1194
+
1195
+ function asNumber(value: unknown): number | undefined {
1196
+ return typeof value === "number" && Number.isFinite(value)
1197
+ ? value
1198
+ : undefined;
1199
+ }