@vellumai/assistant 0.11.4 → 0.11.5-staging.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 (367) hide show
  1. package/AGENTS.md +2 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  16. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  17. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
  19. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/openapi.yaml +193 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/anthropic-provider.test.ts +10 -3
  26. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  27. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  28. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  29. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  30. package/src/__tests__/channel-reply-delivery.test.ts +26 -13
  31. package/src/__tests__/checker.test.ts +20 -314
  32. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  33. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  34. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  35. package/src/__tests__/config-loader-backfill.test.ts +19 -10
  36. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  37. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  38. package/src/__tests__/conversation-attachments.test.ts +0 -1
  39. package/src/__tests__/conversation-error.test.ts +17 -0
  40. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  41. package/src/__tests__/conversation-pairing.test.ts +186 -0
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  43. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  44. package/src/__tests__/credential-routes.test.ts +22 -3
  45. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  46. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  47. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  48. package/src/__tests__/dm-backfill.test.ts +63 -0
  49. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  50. package/src/__tests__/edit-propagation.test.ts +107 -4
  51. package/src/__tests__/events-client-registration.test.ts +28 -0
  52. package/src/__tests__/file-write-tool.test.ts +4 -2
  53. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  54. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  55. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  56. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  57. package/src/__tests__/host-shell-tool.test.ts +23 -4
  58. package/src/__tests__/image-conversion.test.ts +143 -1
  59. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  60. package/src/__tests__/input-repairs.test.ts +207 -0
  61. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  62. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  63. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  64. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  65. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  66. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  67. package/src/__tests__/mtime-cache.test.ts +2 -0
  68. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  69. package/src/__tests__/openai-provider.test.ts +22 -0
  70. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  71. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  72. package/src/__tests__/platform.test.ts +16 -1
  73. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  74. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  75. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  76. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  77. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  78. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  79. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  80. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  81. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  82. package/src/__tests__/reaction-persistence.test.ts +200 -7
  83. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  84. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  85. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  86. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  87. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  88. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  89. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  90. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  91. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  92. package/src/__tests__/tool-executor.test.ts +63 -38
  93. package/src/__tests__/tool-policy.test.ts +97 -1
  94. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  95. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  96. package/src/__tests__/validate-input.test.ts +243 -7
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  99. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  100. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  101. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  102. package/src/acp/acp-claude-oauth.ts +38 -21
  103. package/src/acp/prepare-agent-env.ts +146 -55
  104. package/src/agent/loop-tool-dedup.test.ts +161 -0
  105. package/src/agent/loop.ts +68 -22
  106. package/src/api/constants/app-tools.ts +34 -0
  107. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  108. package/src/api/index.ts +19 -0
  109. package/src/api/responses/resource-pressure-status.ts +25 -0
  110. package/src/approvals/guardian-channel-delivery.ts +70 -6
  111. package/src/approvals/guardian-request-resolvers.ts +32 -66
  112. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  113. package/src/calls/voice-session-bridge.ts +60 -6
  114. package/src/channels/__tests__/message-audience.test.ts +49 -0
  115. package/src/channels/__tests__/types.test.ts +17 -3
  116. package/src/channels/message-audience.ts +40 -0
  117. package/src/channels/types.ts +16 -12
  118. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  119. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  120. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  121. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  122. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  123. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  124. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  125. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  126. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  127. package/src/cli/commands/conversations.help.ts +34 -0
  128. package/src/cli/commands/conversations.ts +125 -0
  129. package/src/cli/commands/inference-providers.ts +9 -4
  130. package/src/cli/commands/inference.help.ts +9 -4
  131. package/src/cli/commands/keys.help.ts +13 -1
  132. package/src/cli/commands/keys.ts +26 -15
  133. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  134. package/src/cli/commands/memory/index.help.ts +41 -27
  135. package/src/cli/commands/memory/index.ts +2 -0
  136. package/src/cli/commands/memory/memory-v2.ts +58 -54
  137. package/src/cli/commands/memory/memory-validate.ts +18 -0
  138. package/src/cli/commands/monitoring.ts +1 -0
  139. package/src/cli/commands/plugins.help.ts +28 -31
  140. package/src/cli/commands/plugins.ts +10 -0
  141. package/src/cli/commands/skills.help.ts +13 -16
  142. package/src/cli/commands/trust.ts +3 -14
  143. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  144. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  145. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  146. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  147. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  148. package/src/cli/lib/bundled-marketplace.json +13 -0
  149. package/src/cli/lib/daemon-credential-client.ts +25 -3
  150. package/src/cli/lib/install-from-github.ts +35 -24
  151. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  152. package/src/cli/lib/plugin-surfaces.ts +27 -0
  153. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  154. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  155. package/src/config/default-profile-catalog.ts +5 -6
  156. package/src/config/feature-flag-registry.json +32 -32
  157. package/src/config/webhook-routing.ts +8 -0
  158. package/src/context/compactor.ts +31 -2
  159. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  160. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  161. package/src/daemon/conversation-error.ts +13 -0
  162. package/src/daemon/conversation-messaging.ts +22 -2
  163. package/src/daemon/conversation-process.ts +11 -0
  164. package/src/daemon/conversation-store.ts +10 -0
  165. package/src/daemon/conversation-surfaces.ts +21 -8
  166. package/src/daemon/conversation.ts +12 -10
  167. package/src/daemon/lifecycle.ts +6 -2
  168. package/src/daemon/message-types/conversations.ts +5 -7
  169. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  170. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  171. package/src/daemon/resource-pressure-guard.ts +387 -0
  172. package/src/daemon/shutdown-handlers.ts +2 -0
  173. package/src/daemon/startup-error.ts +16 -0
  174. package/src/daemon/trust-context.ts +14 -10
  175. package/src/daemon/unsendable-image-notice.ts +76 -0
  176. package/src/hooks/registry.ts +65 -51
  177. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  178. package/src/ipc/gateway-client.test.ts +1 -1
  179. package/src/ipc/gateway-client.ts +14 -21
  180. package/src/ipc/socket-cleanup.ts +2 -11
  181. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  182. package/src/messaging/provider-message-metadata.ts +128 -0
  183. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  184. package/src/messaging/providers/channel-transport.ts +90 -13
  185. package/src/messaging/providers/discord/send.ts +16 -0
  186. package/src/messaging/providers/discord/transport.ts +11 -1
  187. package/src/messaging/providers/index.ts +109 -16
  188. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  189. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  190. package/src/messaging/providers/slack/send.test.ts +6 -9
  191. package/src/messaging/providers/slack/send.ts +40 -28
  192. package/src/messaging/providers/slack/transport.ts +22 -26
  193. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  194. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  195. package/src/messaging/read-provider-metadata.test.ts +157 -0
  196. package/src/messaging/read-provider-metadata.ts +51 -0
  197. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  198. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  199. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  200. package/src/notifications/adapters/macos.ts +55 -0
  201. package/src/notifications/adapters/slack.ts +10 -5
  202. package/src/notifications/assistant-reply-producer.ts +37 -4
  203. package/src/notifications/conversation-pairing.ts +111 -21
  204. package/src/notifications/edit-notification.ts +38 -8
  205. package/src/notifications/emit-signal.ts +12 -14
  206. package/src/notifications/home-feed-side-effect.ts +218 -15
  207. package/src/notifications/types.ts +5 -0
  208. package/src/permissions/AGENTS.md +16 -0
  209. package/src/permissions/checker.test.ts +75 -122
  210. package/src/permissions/checker.ts +44 -560
  211. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  212. package/src/permissions/confirmation-guardian-request.ts +5 -1
  213. package/src/persistence/conversation-crud.ts +23 -20
  214. package/src/persistence/conversation-queries.ts +14 -1
  215. package/src/persistence/conversation-types.ts +22 -0
  216. package/src/persistence/delivery-crud.ts +122 -15
  217. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  218. package/src/plugin-api/conversation-turn.ts +27 -0
  219. package/src/plugin-api/credential-scope.test.ts +15 -0
  220. package/src/plugin-api/credential-scope.ts +14 -0
  221. package/src/plugin-api/index.ts +19 -1
  222. package/src/plugin-api/resolve-credential.ts +15 -10
  223. package/src/plugin-api/store-credential.ts +148 -0
  224. package/src/plugin-api/system-card.ts +36 -0
  225. package/src/plugin-api/vision-support.test.ts +82 -0
  226. package/src/plugin-api/vision-support.ts +14 -2
  227. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  228. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  229. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  230. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  231. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  232. package/src/plugins/defaults/main.ts +15 -0
  233. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  234. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  235. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  237. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  238. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  239. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  240. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  241. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  242. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  243. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  244. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  245. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  246. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  247. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  248. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  249. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  250. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  251. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  252. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  253. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  254. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  255. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  256. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  257. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  258. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  259. package/src/plugins/mtime-cache.ts +1 -1
  260. package/src/plugins/pipeline.ts +14 -7
  261. package/src/plugins/plugin-execution-context.ts +35 -11
  262. package/src/plugins/plugin-tree-walk.ts +63 -4
  263. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  264. package/src/providers/anthropic/client.ts +332 -241
  265. package/src/providers/connection-resolution.ts +2 -1
  266. package/src/providers/content-blocks.ts +22 -0
  267. package/src/providers/gemini/client.ts +5 -2
  268. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  269. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  270. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  271. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  272. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  273. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  274. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  275. package/src/providers/inference/adapter-factory.ts +33 -2
  276. package/src/providers/inference/auth.ts +13 -2
  277. package/src/providers/inference/credential-usage.ts +37 -0
  278. package/src/providers/inference/missing-credential-guard.ts +110 -0
  279. package/src/providers/inference/resolve-auth.ts +7 -7
  280. package/src/providers/media-resolve.ts +176 -15
  281. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  282. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  283. package/src/providers/openai/chat-completions-provider.ts +322 -42
  284. package/src/routes/route-host-protocol.ts +7 -0
  285. package/src/routes/worker.ts +31 -10
  286. package/src/runtime/AGENTS.md +35 -0
  287. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  288. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  289. package/src/runtime/assistant-event-hub.ts +38 -0
  290. package/src/runtime/channel-reply-delivery.ts +54 -31
  291. package/src/runtime/channel-retry-sweep.ts +6 -4
  292. package/src/runtime/effective-capabilities.test.ts +19 -0
  293. package/src/runtime/effective-capabilities.ts +25 -0
  294. package/src/runtime/guardian-reply-router.ts +6 -2
  295. package/src/runtime/http-errors.ts +1 -0
  296. package/src/runtime/http-server.ts +20 -5
  297. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  298. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  299. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  300. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  301. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  302. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  303. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  304. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  305. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  306. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  307. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  308. package/src/runtime/routes/approval-routes.ts +37 -4
  309. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  310. package/src/runtime/routes/canned-message-complete.ts +68 -29
  311. package/src/runtime/routes/client-routes.ts +113 -0
  312. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  313. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  314. package/src/runtime/routes/conversation-routes.ts +36 -22
  315. package/src/runtime/routes/credential-in-use.ts +85 -0
  316. package/src/runtime/routes/credential-routes.ts +55 -85
  317. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  318. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  319. package/src/runtime/routes/identity-routes.ts +5 -121
  320. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  321. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  322. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  323. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  324. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  325. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  326. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  327. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  328. package/src/runtime/routes/index.ts +2 -0
  329. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  330. package/src/runtime/routes/monitoring-routes.ts +9 -1
  331. package/src/runtime/routes/plugins-routes.ts +17 -5
  332. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  333. package/src/runtime/routes/secret-routes.ts +39 -8
  334. package/src/runtime/routes/settings-routes.ts +6 -6
  335. package/src/runtime/routes/surface-action-routes.ts +20 -19
  336. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  337. package/src/runtime/routes/user-route-resolution.ts +18 -1
  338. package/src/runtime/slack-reply-session.test.ts +23 -13
  339. package/src/runtime/slack-reply-session.ts +38 -55
  340. package/src/runtime/web-presence.ts +90 -0
  341. package/src/skills/validate-input.ts +177 -40
  342. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  343. package/src/subagent/consult-context.ts +7 -23
  344. package/src/tools/credentials/broker.ts +24 -79
  345. package/src/tools/credentials/ref-parse.ts +35 -0
  346. package/src/tools/credentials/resolve.ts +4 -10
  347. package/src/tools/credentials/store.ts +168 -0
  348. package/src/tools/credentials/tool-policy.ts +67 -0
  349. package/src/tools/executor.ts +53 -15
  350. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  351. package/src/tools/network/url-safety.ts +9 -16
  352. package/src/tools/network/web-search.ts +21 -3
  353. package/src/tools/permission-checker.ts +43 -52
  354. package/src/tools/schema-transforms.ts +40 -5
  355. package/src/tools/shared/input-repairs.ts +160 -0
  356. package/src/tools/skills/skill-tool-factory.ts +18 -4
  357. package/src/tools/subagent/spawn.ts +0 -1
  358. package/src/tools/tool-approval-handler.ts +16 -10
  359. package/src/tools/tool-types.ts +26 -13
  360. package/src/tools/types.ts +6 -6
  361. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  362. package/src/util/cgroup-memory.ts +3 -0
  363. package/src/util/container-cpu-sampler.ts +250 -0
  364. package/src/util/image-conversion.ts +178 -14
  365. package/src/util/platform.ts +93 -10
  366. package/src/permissions/ipc-risk-types.ts +0 -143
  367. package/src/permissions/risk-types.ts +0 -76
@@ -294,6 +294,53 @@ describe("OpenAIChatCompletionsProvider reasoning parsing", () => {
294
294
  });
295
295
  });
296
296
 
297
+ test("round-trips reasoning_content on assistant messages that carry tool_calls", async () => {
298
+ const { provider, requests } = stubProvider(
299
+ [
300
+ {
301
+ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
302
+ usage: { prompt_tokens: 2, completion_tokens: 1 },
303
+ },
304
+ ],
305
+ { assistantReasoningField: "reasoning_content" },
306
+ );
307
+
308
+ await provider.sendMessage([
309
+ {
310
+ role: "assistant",
311
+ content: [
312
+ {
313
+ type: "thinking",
314
+ thinking: "need the search tool",
315
+ signature: "",
316
+ },
317
+ { type: "tool_use", id: "call_1", name: "search", input: { q: "x" } },
318
+ ],
319
+ },
320
+ ]);
321
+
322
+ const params = requests[0] as {
323
+ messages: Array<{
324
+ role: string;
325
+ content: string | null;
326
+ reasoning_content?: string;
327
+ tool_calls?: unknown;
328
+ }>;
329
+ };
330
+ expect(params.messages[0]).toEqual({
331
+ role: "assistant",
332
+ content: null,
333
+ reasoning_content: "need the search tool",
334
+ tool_calls: [
335
+ {
336
+ id: "call_1",
337
+ type: "function",
338
+ function: { name: "search", arguments: JSON.stringify({ q: "x" }) },
339
+ },
340
+ ],
341
+ });
342
+ });
343
+
297
344
  test("uses reasoning field for OpenRouter-style round-trip", async () => {
298
345
  const { provider, requests } = stubProvider(
299
346
  [
@@ -438,18 +485,88 @@ describe("OpenAIChatCompletionsProvider reasoning parsing", () => {
438
485
  };
439
486
  const assistantMsg = params.messages.find((m) => m.role === "assistant")!;
440
487
  // Backfill defaults off, so providers that tolerate null assistant content
441
- // (e.g. OpenAI proper) are unaffected by the OpenRouter-specific guard.
488
+ // (e.g. Fireworks, Together) are unaffected unless they opt in.
442
489
  expect(assistantMsg.content).toBeNull();
443
490
  });
444
491
 
445
- test("does not backfill content when tool calls are present", async () => {
446
- const { provider, requests } = stubProvider([
492
+ test("backfills placeholder when thinking is dropped and no text was emitted", async () => {
493
+ // Custom openai-compatible endpoints do not set assistantReasoningField, so
494
+ // a Stop during thinking serializes to { role: "assistant", content: null }
495
+ // unless the backfill guard runs.
496
+ const { provider, requests } = stubProvider(
497
+ [
498
+ {
499
+ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
500
+ usage: { prompt_tokens: 2, completion_tokens: 1 },
501
+ },
502
+ ],
503
+ { backfillEmptyAssistantContent: true },
504
+ );
505
+
506
+ await provider.sendMessage([
507
+ { role: "user", content: [{ type: "text", text: "question" }] },
447
508
  {
448
- choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
449
- usage: { prompt_tokens: 2, completion_tokens: 1 },
509
+ role: "assistant",
510
+ content: [
511
+ {
512
+ type: "thinking",
513
+ thinking: "aborted mid-thought",
514
+ signature: "",
515
+ },
516
+ ],
450
517
  },
451
518
  ]);
452
519
 
520
+ const params = requests[0] as {
521
+ messages: Array<{
522
+ role: string;
523
+ content: string | null;
524
+ reasoning?: string;
525
+ reasoning_content?: string;
526
+ tool_calls?: unknown;
527
+ }>;
528
+ };
529
+ const assistantMsg = params.messages.find((m) => m.role === "assistant")!;
530
+ expect(assistantMsg.content).toBe(EMPTY_ASSISTANT_TURN_PLACEHOLDER);
531
+ expect(assistantMsg.tool_calls).toBeUndefined();
532
+ expect(assistantMsg.reasoning).toBeUndefined();
533
+ expect(assistantMsg.reasoning_content).toBeUndefined();
534
+ });
535
+
536
+ test("backfills placeholder for an empty aborted assistant turn", async () => {
537
+ const { provider, requests } = stubProvider(
538
+ [
539
+ {
540
+ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
541
+ usage: { prompt_tokens: 2, completion_tokens: 1 },
542
+ },
543
+ ],
544
+ { backfillEmptyAssistantContent: true },
545
+ );
546
+
547
+ await provider.sendMessage([
548
+ { role: "user", content: [{ type: "text", text: "question" }] },
549
+ { role: "assistant", content: [] },
550
+ ]);
551
+
552
+ const params = requests[0] as {
553
+ messages: Array<{ role: string; content: string | null }>;
554
+ };
555
+ const assistantMsg = params.messages.find((m) => m.role === "assistant")!;
556
+ expect(assistantMsg.content).toBe(EMPTY_ASSISTANT_TURN_PLACEHOLDER);
557
+ });
558
+
559
+ test("does not backfill content when tool calls are present", async () => {
560
+ const { provider, requests } = stubProvider(
561
+ [
562
+ {
563
+ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
564
+ usage: { prompt_tokens: 2, completion_tokens: 1 },
565
+ },
566
+ ],
567
+ { backfillEmptyAssistantContent: true },
568
+ );
569
+
453
570
  await provider.sendMessage([
454
571
  {
455
572
  role: "assistant",
@@ -460,12 +577,18 @@ describe("OpenAIChatCompletionsProvider reasoning parsing", () => {
460
577
  ]);
461
578
 
462
579
  const params = requests[0] as {
463
- messages: Array<{ role: string; content: string | null }>;
580
+ messages: Array<{
581
+ role: string;
582
+ content: string | null;
583
+ reasoning_content?: string;
584
+ }>;
464
585
  };
465
586
  // Tool-call-only assistant messages keep null content (preferred by
466
587
  // Anthropic-proxy/Bedrock backends); the placeholder is only for the
467
- // neither-content-nor-tool_calls case.
588
+ // neither-content-nor-tool_calls case. The reasoning field stays omitted
589
+ // when assistantReasoningField is unset.
468
590
  expect(params.messages[0].content).toBeNull();
591
+ expect(params.messages[0].reasoning_content).toBeUndefined();
469
592
  });
470
593
 
471
594
  test("forwards config.top_p onto the request body", async () => {
@@ -535,52 +658,153 @@ describe("OpenAIChatCompletionsProvider reasoning parsing", () => {
535
658
  };
536
659
  expect(params.messages[0].reasoning_content).toBe("deepseek thinking");
537
660
  });
538
- });
539
661
 
540
- describe("reasoning opt-out rejection fallback", () => {
541
- function stubProviderWithErrors(
542
- errors: unknown[],
543
- chunks: MockChunk[],
544
- ): { provider: OpenAIChatCompletionsProvider; requests: unknown[] } {
545
- const provider = new OpenAIChatCompletionsProvider(
546
- "test-key",
547
- "test-model",
548
- );
549
- const requests: unknown[] = [];
550
- const pending = [...errors];
551
- (provider as unknown as { client: unknown }).client = {
552
- chat: {
553
- completions: {
554
- create: async (params: unknown) => {
555
- // Snapshot: the fallback mutates `params` between attempts.
556
- requests.push(JSON.parse(JSON.stringify(params)));
557
- const error = pending.shift();
558
- if (error !== undefined) {
559
- throw error;
560
- }
561
- return makeStream(chunks);
562
- },
662
+ test("omits empty reasoning_content on tool-call turns even when the field is set", async () => {
663
+ const { provider, requests } = stubProvider(
664
+ [
665
+ {
666
+ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
667
+ usage: { prompt_tokens: 2, completion_tokens: 1 },
563
668
  },
669
+ ],
670
+ { assistantReasoningField: "reasoning_content" },
671
+ );
672
+
673
+ await provider.sendMessage([
674
+ {
675
+ role: "assistant",
676
+ content: [
677
+ { type: "tool_use", id: "call_1", name: "search", input: { q: "x" } },
678
+ ],
564
679
  },
680
+ ]);
681
+
682
+ const params = requests[0] as {
683
+ messages: Array<{
684
+ role: string;
685
+ content: string | null;
686
+ tool_calls?: unknown;
687
+ reasoning_content?: string;
688
+ }>;
565
689
  };
566
- return { provider, requests };
567
- }
690
+ const assistantMsg = params.messages[0];
691
+ expect(assistantMsg.tool_calls).toEqual([
692
+ {
693
+ id: "call_1",
694
+ type: "function",
695
+ function: { name: "search", arguments: JSON.stringify({ q: "x" }) },
696
+ },
697
+ ]);
698
+ expect(assistantMsg.reasoning_content).toBeUndefined();
699
+ });
568
700
 
569
- const okChunks: MockChunk[] = [
570
- {
571
- choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
572
- usage: { prompt_tokens: 1, completion_tokens: 1 },
701
+ test("omits empty reasoning on tool-call turns for the OpenRouter-style field", async () => {
702
+ const { provider, requests } = stubProvider(
703
+ [
704
+ {
705
+ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
706
+ usage: { prompt_tokens: 2, completion_tokens: 1 },
707
+ },
708
+ ],
709
+ { assistantReasoningField: "reasoning" },
710
+ );
711
+
712
+ await provider.sendMessage([
713
+ {
714
+ role: "assistant",
715
+ content: [
716
+ { type: "tool_use", id: "call_1", name: "search", input: { q: "x" } },
717
+ ],
718
+ },
719
+ ]);
720
+
721
+ const params = requests[0] as {
722
+ messages: Array<{
723
+ role: string;
724
+ reasoning?: string;
725
+ reasoning_content?: string;
726
+ }>;
727
+ };
728
+ expect(params.messages[0].reasoning).toBeUndefined();
729
+ expect(params.messages[0].reasoning_content).toBeUndefined();
730
+ });
731
+
732
+ test("omits empty reasoning_content on text-only turns even when the field is set", async () => {
733
+ const { provider, requests } = stubProvider(
734
+ [
735
+ {
736
+ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
737
+ usage: { prompt_tokens: 2, completion_tokens: 1 },
738
+ },
739
+ ],
740
+ { assistantReasoningField: "reasoning_content" },
741
+ );
742
+
743
+ await provider.sendMessage([
744
+ {
745
+ role: "assistant",
746
+ content: [{ type: "text", text: "plain reply" }],
747
+ },
748
+ ]);
749
+
750
+ const params = requests[0] as {
751
+ messages: Array<{
752
+ role: string;
753
+ content: string | null;
754
+ reasoning_content?: string;
755
+ }>;
756
+ };
757
+ expect(params.messages[0].content).toBe("plain reply");
758
+ expect(params.messages[0].reasoning_content).toBeUndefined();
759
+ });
760
+ });
761
+
762
+ function stubProviderWithErrors(
763
+ errors: unknown[],
764
+ chunks: MockChunk[],
765
+ options?: OpenAIChatCompletionsProviderOptions,
766
+ ): { provider: OpenAIChatCompletionsProvider; requests: unknown[] } {
767
+ const provider = new OpenAIChatCompletionsProvider(
768
+ "test-key",
769
+ "test-model",
770
+ options,
771
+ );
772
+ const requests: unknown[] = [];
773
+ const pending = [...errors];
774
+ (provider as unknown as { client: unknown }).client = {
775
+ chat: {
776
+ completions: {
777
+ create: async (params: unknown) => {
778
+ // Snapshot: the fallback mutates `params` between attempts.
779
+ requests.push(JSON.parse(JSON.stringify(params)));
780
+ const error = pending.shift();
781
+ if (error !== undefined) {
782
+ throw error;
783
+ }
784
+ return makeStream(chunks);
785
+ },
786
+ },
573
787
  },
574
- ];
788
+ };
789
+ return { provider, requests };
790
+ }
791
+
792
+ const OK_CHUNKS: MockChunk[] = [
793
+ {
794
+ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
795
+ usage: { prompt_tokens: 1, completion_tokens: 1 },
796
+ },
797
+ ];
575
798
 
576
- function rejection(message: string, status = 400): Error {
577
- return Object.assign(new Error(message), { status });
578
- }
799
+ function rejection(message: string, status = 400): Error {
800
+ return Object.assign(new Error(message), { status });
801
+ }
579
802
 
803
+ describe("reasoning opt-out rejection fallback", () => {
580
804
  test("retries once without reasoning params when a model rejects the explicit opt-out", async () => {
581
805
  const { provider, requests } = stubProviderWithErrors(
582
806
  [rejection("reasoning_effort 'none' is not supported for this model")],
583
- okChunks,
807
+ OK_CHUNKS,
584
808
  );
585
809
 
586
810
  const response = await provider.sendMessage(
@@ -626,7 +850,7 @@ describe("reasoning opt-out rejection fallback", () => {
626
850
  // can only fire off the normalized upstream detail.
627
851
  expect(/reasoning/i.test(wrapped.message)).toBe(false);
628
852
 
629
- const { provider, requests } = stubProviderWithErrors([wrapped], okChunks);
853
+ const { provider, requests } = stubProviderWithErrors([wrapped], OK_CHUNKS);
630
854
 
631
855
  const response = await provider.sendMessage(
632
856
  [{ role: "user", content: [{ type: "text", text: "hi" }] }],
@@ -651,7 +875,7 @@ describe("reasoning opt-out rejection fallback", () => {
651
875
  test("does not retry when the request did not opt out of reasoning", async () => {
652
876
  const { provider, requests } = stubProviderWithErrors(
653
877
  [rejection("reasoning_effort is invalid")],
654
- okChunks,
878
+ OK_CHUNKS,
655
879
  );
656
880
 
657
881
  await expect(
@@ -666,7 +890,7 @@ describe("reasoning opt-out rejection fallback", () => {
666
890
  test("does not retry a 4xx that does not name reasoning", async () => {
667
891
  const { provider, requests } = stubProviderWithErrors(
668
892
  [rejection("invalid api key")],
669
- okChunks,
893
+ OK_CHUNKS,
670
894
  );
671
895
 
672
896
  await expect(
@@ -681,7 +905,7 @@ describe("reasoning opt-out rejection fallback", () => {
681
905
  test("does not retry server errors", async () => {
682
906
  const { provider, requests } = stubProviderWithErrors(
683
907
  [rejection("reasoning backend unavailable", 500)],
684
- okChunks,
908
+ OK_CHUNKS,
685
909
  );
686
910
 
687
911
  await expect(
@@ -694,6 +918,312 @@ describe("reasoning opt-out rejection fallback", () => {
694
918
  });
695
919
  });
696
920
 
921
+ describe("thinking-mode tool_choice rejection fallback", () => {
922
+ test("retries once without tool_choice when thinking mode rejects it", async () => {
923
+ const { provider, requests } = stubProviderWithErrors(
924
+ [rejection("Thinking mode does not support this tool_choice")],
925
+ OK_CHUNKS,
926
+ );
927
+
928
+ const response = await provider.sendMessage(
929
+ [{ role: "user", content: [{ type: "text", text: "hi" }] }],
930
+ {
931
+ tools: [
932
+ {
933
+ name: "bash",
934
+ description: "Run a shell command",
935
+ input_schema: { type: "object", properties: {} },
936
+ },
937
+ ],
938
+ config: { tool_choice: { type: "none" }, effort: "high" },
939
+ },
940
+ );
941
+
942
+ expect(requests).toHaveLength(2);
943
+ const first = requests[0] as {
944
+ tool_choice?: string;
945
+ reasoning_effort?: string;
946
+ };
947
+ const second = requests[1] as {
948
+ tool_choice?: string;
949
+ reasoning_effort?: string;
950
+ };
951
+ expect(first.tool_choice).toBe("none");
952
+ expect(first.reasoning_effort).toBe("high");
953
+ expect(second.tool_choice).toBeUndefined();
954
+ expect(second.reasoning_effort).toBe("high");
955
+ const text = response.content.find((b) => b.type === "text") as
956
+ | { type: "text"; text: string }
957
+ | undefined;
958
+ expect(text?.text).toBe("ok");
959
+ });
960
+
961
+ test("retries once for an OpenRouter-wrapped thinking-mode tool_choice rejection", async () => {
962
+ const wrapped = new OpenAI.APIError(
963
+ 400,
964
+ {
965
+ code: 400,
966
+ message: "Provider returned error",
967
+ metadata: {
968
+ raw: "[invalid_request_error] Thinking mode does not support this tool_choice",
969
+ provider_name: "deepseek",
970
+ },
971
+ },
972
+ undefined,
973
+ new Headers(),
974
+ );
975
+ expect(/tool_choice/i.test(wrapped.message)).toBe(false);
976
+
977
+ const { provider, requests } = stubProviderWithErrors([wrapped], OK_CHUNKS);
978
+
979
+ await provider.sendMessage(
980
+ [{ role: "user", content: [{ type: "text", text: "hi" }] }],
981
+ {
982
+ tools: [
983
+ {
984
+ name: "bash",
985
+ description: "Run a shell command",
986
+ input_schema: { type: "object", properties: {} },
987
+ },
988
+ ],
989
+ config: { tool_choice: { type: "none" }, effort: "high" },
990
+ },
991
+ );
992
+
993
+ expect(requests).toHaveLength(2);
994
+ expect((requests[0] as { tool_choice?: string }).tool_choice).toBe("none");
995
+ expect((requests[1] as { tool_choice?: string }).tool_choice).toBeUndefined();
996
+ });
997
+
998
+ test("does not retry a 4xx that does not name tool_choice", async () => {
999
+ const { provider, requests } = stubProviderWithErrors(
1000
+ [rejection("invalid api key")],
1001
+ OK_CHUNKS,
1002
+ );
1003
+
1004
+ await expect(
1005
+ provider.sendMessage(
1006
+ [{ role: "user", content: [{ type: "text", text: "hi" }] }],
1007
+ {
1008
+ tools: [
1009
+ {
1010
+ name: "bash",
1011
+ description: "Run a shell command",
1012
+ input_schema: { type: "object", properties: {} },
1013
+ },
1014
+ ],
1015
+ config: { tool_choice: { type: "none" }, effort: "high" },
1016
+ },
1017
+ ),
1018
+ ).rejects.toThrow();
1019
+ expect(requests).toHaveLength(1);
1020
+ });
1021
+
1022
+ test("does not retry thinking-mode tool_choice 500s", async () => {
1023
+ const { provider, requests } = stubProviderWithErrors(
1024
+ [rejection("Thinking mode does not support this tool_choice", 500)],
1025
+ OK_CHUNKS,
1026
+ );
1027
+
1028
+ await expect(
1029
+ provider.sendMessage(
1030
+ [{ role: "user", content: [{ type: "text", text: "hi" }] }],
1031
+ {
1032
+ tools: [
1033
+ {
1034
+ name: "bash",
1035
+ description: "Run a shell command",
1036
+ input_schema: { type: "object", properties: {} },
1037
+ },
1038
+ ],
1039
+ config: { tool_choice: { type: "none" }, effort: "high" },
1040
+ },
1041
+ ),
1042
+ ).rejects.toThrow();
1043
+ expect(requests).toHaveLength(1);
1044
+ });
1045
+ });
1046
+
1047
+ describe("missing reasoning_content rejection fallback", () => {
1048
+ const toolCallHistory = [
1049
+ {
1050
+ role: "assistant" as const,
1051
+ content: [
1052
+ {
1053
+ type: "tool_use" as const,
1054
+ id: "call_1",
1055
+ name: "search",
1056
+ input: { q: "x" },
1057
+ },
1058
+ ],
1059
+ },
1060
+ ];
1061
+
1062
+ test("retries once with empty reasoning_content when thinking mode requires it", async () => {
1063
+ const { provider, requests } = stubProviderWithErrors(
1064
+ [
1065
+ rejection(
1066
+ "The reasoning_content in the thinking mode must be passed back to the API",
1067
+ ),
1068
+ ],
1069
+ OK_CHUNKS,
1070
+ { assistantReasoningField: "reasoning_content" },
1071
+ );
1072
+
1073
+ const response = await provider.sendMessage(toolCallHistory);
1074
+
1075
+ expect(requests).toHaveLength(2);
1076
+ const first = requests[0] as {
1077
+ messages: Array<{ reasoning_content?: string }>;
1078
+ };
1079
+ const second = requests[1] as {
1080
+ messages: Array<{ reasoning_content?: string }>;
1081
+ };
1082
+ expect(first.messages[0].reasoning_content).toBeUndefined();
1083
+ expect(second.messages[0].reasoning_content).toBe("");
1084
+ const text = response.content.find((b) => b.type === "text") as
1085
+ | { type: "text"; text: string }
1086
+ | undefined;
1087
+ expect(text?.text).toBe("ok");
1088
+ });
1089
+
1090
+ test("retries once for an OpenRouter-wrapped missing reasoning_content rejection", async () => {
1091
+ const wrapped = new OpenAI.APIError(
1092
+ 400,
1093
+ {
1094
+ code: 400,
1095
+ message: "Provider returned error",
1096
+ metadata: {
1097
+ raw: "[invalid_request_error] The reasoning_content in the thinking mode must be passed back to the API",
1098
+ provider_name: "deepseek",
1099
+ },
1100
+ },
1101
+ undefined,
1102
+ new Headers(),
1103
+ );
1104
+ expect(/reasoning_content/i.test(wrapped.message)).toBe(false);
1105
+
1106
+ const { provider, requests } = stubProviderWithErrors([wrapped], OK_CHUNKS, {
1107
+ assistantReasoningField: "reasoning_content",
1108
+ });
1109
+
1110
+ await provider.sendMessage(toolCallHistory);
1111
+
1112
+ expect(requests).toHaveLength(2);
1113
+ expect(
1114
+ (requests[0] as { messages: Array<{ reasoning_content?: string }> })
1115
+ .messages[0].reasoning_content,
1116
+ ).toBeUndefined();
1117
+ expect(
1118
+ (requests[1] as { messages: Array<{ reasoning_content?: string }> })
1119
+ .messages[0].reasoning_content,
1120
+ ).toBe("");
1121
+ });
1122
+
1123
+ test("does not retry a 4xx that does not require reasoning_content round-trip", async () => {
1124
+ const { provider, requests } = stubProviderWithErrors(
1125
+ [rejection("invalid api key")],
1126
+ OK_CHUNKS,
1127
+ { assistantReasoningField: "reasoning_content" },
1128
+ );
1129
+
1130
+ await expect(provider.sendMessage(toolCallHistory)).rejects.toThrow();
1131
+ expect(requests).toHaveLength(1);
1132
+ });
1133
+
1134
+ test("does not retry missing reasoning_content 500s", async () => {
1135
+ const { provider, requests } = stubProviderWithErrors(
1136
+ [
1137
+ rejection(
1138
+ "The reasoning_content in the thinking mode must be passed back to the API",
1139
+ 500,
1140
+ ),
1141
+ ],
1142
+ OK_CHUNKS,
1143
+ { assistantReasoningField: "reasoning_content" },
1144
+ );
1145
+
1146
+ await expect(provider.sendMessage(toolCallHistory)).rejects.toThrow();
1147
+ expect(requests).toHaveLength(1);
1148
+ });
1149
+ });
1150
+
1151
+ describe("unknown assistant reasoning field rejection fallback", () => {
1152
+ const thinkingHistory = [
1153
+ {
1154
+ role: "assistant" as const,
1155
+ content: [
1156
+ { type: "thinking" as const, thinking: "hidden chain", signature: "" },
1157
+ { type: "text" as const, text: "answer" },
1158
+ ],
1159
+ },
1160
+ ];
1161
+
1162
+ test("retries once without reasoning_content when a strict schema rejects it", async () => {
1163
+ const { provider, requests } = stubProviderWithErrors(
1164
+ [
1165
+ rejection(
1166
+ "Additional properties are not allowed ('reasoning_content' was unexpected)",
1167
+ ),
1168
+ ],
1169
+ OK_CHUNKS,
1170
+ { assistantReasoningField: "reasoning_content" },
1171
+ );
1172
+
1173
+ const response = await provider.sendMessage(thinkingHistory);
1174
+
1175
+ expect(requests).toHaveLength(2);
1176
+ const first = requests[0] as {
1177
+ messages: Array<{ reasoning_content?: string; content: string | null }>;
1178
+ };
1179
+ const second = requests[1] as {
1180
+ messages: Array<{ reasoning_content?: string; content: string | null }>;
1181
+ };
1182
+ expect(first.messages[0].reasoning_content).toBe("hidden chain");
1183
+ expect(second.messages[0].reasoning_content).toBeUndefined();
1184
+ expect(second.messages[0].content).toBe("answer");
1185
+ const text = response.content.find((b) => b.type === "text") as
1186
+ | { type: "text"; text: string }
1187
+ | undefined;
1188
+ expect(text?.text).toBe("ok");
1189
+ });
1190
+
1191
+ test("does not strip reasoning_content on a must-be-passed-back error", async () => {
1192
+ const { provider, requests } = stubProviderWithErrors(
1193
+ [
1194
+ rejection(
1195
+ "The reasoning_content in the thinking mode must be passed back to the API",
1196
+ ),
1197
+ ],
1198
+ OK_CHUNKS,
1199
+ { assistantReasoningField: "reasoning_content" },
1200
+ );
1201
+
1202
+ await expect(provider.sendMessage(thinkingHistory)).rejects.toThrow();
1203
+ expect(requests).toHaveLength(1);
1204
+ expect(
1205
+ (requests[0] as { messages: Array<{ reasoning_content?: string }> })
1206
+ .messages[0].reasoning_content,
1207
+ ).toBe("hidden chain");
1208
+ });
1209
+
1210
+ test("does not retry unknown-field 500s", async () => {
1211
+ const { provider, requests } = stubProviderWithErrors(
1212
+ [
1213
+ rejection(
1214
+ "Additional properties are not allowed ('reasoning_content' was unexpected)",
1215
+ 500,
1216
+ ),
1217
+ ],
1218
+ OK_CHUNKS,
1219
+ { assistantReasoningField: "reasoning_content" },
1220
+ );
1221
+
1222
+ await expect(provider.sendMessage(thinkingHistory)).rejects.toThrow();
1223
+ expect(requests).toHaveLength(1);
1224
+ });
1225
+ });
1226
+
697
1227
  describe("OpenAIChatCompletionsProvider cache usage parsing", () => {
698
1228
  test("maps prompt_tokens_details cache fields into usage", async () => {
699
1229
  // prompt_tokens is the inclusive total; the cached subset surfaces as