@vellumai/assistant 0.11.1 → 0.11.2-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 (318) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +1 -1
  3. package/eslint.config.mjs +28 -8
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  12. package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  13. package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
  14. package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
  15. package/openapi.yaml +292 -5
  16. package/package.json +1 -1
  17. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  18. package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
  19. package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
  20. package/src/__tests__/compaction-events.test.ts +139 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +35 -3
  22. package/src/__tests__/config-schema.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +180 -7
  25. package/src/__tests__/conversation-error.test.ts +211 -4
  26. package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
  27. package/src/__tests__/conversation-queue.test.ts +126 -0
  28. package/src/__tests__/conversation-retry-route.test.ts +46 -7
  29. package/src/__tests__/conversation-slash.test.ts +4 -2
  30. package/src/__tests__/conversation-summarize-route.test.ts +7 -1
  31. package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
  32. package/src/__tests__/document-create-dedupe.test.ts +2 -1
  33. package/src/__tests__/document-find-replace.test.ts +2 -1
  34. package/src/__tests__/document-tool-security.test.ts +2 -1
  35. package/src/__tests__/document-update-default-surface.test.ts +2 -1
  36. package/src/__tests__/document-workspace-file.test.ts +467 -0
  37. package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
  38. package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
  39. package/src/__tests__/http-user-message-parity.test.ts +66 -0
  40. package/src/__tests__/list-messages-attachments.test.ts +87 -0
  41. package/src/__tests__/list-messages-provider-error.test.ts +143 -0
  42. package/src/__tests__/list-messages-system-card.test.ts +100 -0
  43. package/src/__tests__/llm-resolver.test.ts +19 -9
  44. package/src/__tests__/managed-profile-guard.test.ts +23 -0
  45. package/src/__tests__/notification-platform-adapter.test.ts +130 -2
  46. package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
  47. package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
  48. package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
  49. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  50. package/src/__tests__/provider-error-scenarios.test.ts +140 -0
  51. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  52. package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
  53. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
  54. package/src/__tests__/skills.test.ts +27 -0
  55. package/src/__tests__/slack-channels-routes.test.ts +0 -2
  56. package/src/__tests__/slack-share-routes.test.ts +0 -3
  57. package/src/__tests__/slack-users-routes.test.ts +0 -2
  58. package/src/__tests__/subagent-tools.test.ts +11 -0
  59. package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
  60. package/src/__tests__/tool-result-spool.test.ts +5 -4
  61. package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
  62. package/src/__tests__/turn-events-store.test.ts +26 -0
  63. package/src/__tests__/ui-visual-surface.test.ts +695 -0
  64. package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
  65. package/src/__tests__/visible-app-context.test.ts +189 -0
  66. package/src/__tests__/workspace-git-service.test.ts +173 -33
  67. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
  68. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  69. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
  70. package/src/agent/loop.ts +36 -7
  71. package/src/api/events/context-window-usage.ts +31 -0
  72. package/src/api/events/notification-intent.ts +8 -0
  73. package/src/api/events/ui-surface-pending.ts +35 -0
  74. package/src/api/index.ts +14 -0
  75. package/src/api/responses/conversation-message.ts +25 -4
  76. package/src/api/surfaces.ts +90 -1
  77. package/src/approvals/guardian-card-withdrawal.ts +66 -31
  78. package/src/approvals/guardian-decision-primitive.ts +4 -0
  79. package/src/calls/__tests__/call-setup-router.test.ts +156 -26
  80. package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
  81. package/src/calls/call-setup-router.ts +80 -36
  82. package/src/calls/voice-session-bridge.ts +173 -26
  83. package/src/cli/commands/inference.help.ts +3 -3
  84. package/src/cli/commands/notifications.help.ts +3 -2
  85. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
  86. package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
  87. package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
  88. package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
  89. package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
  90. package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
  91. package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
  92. package/src/cli/commands/platform/index.help.ts +92 -0
  93. package/src/cli/commands/platform/index.ts +7 -0
  94. package/src/cli/commands/platform/invoices.ts +132 -0
  95. package/src/cli/commands/usage.help.ts +1 -1
  96. package/src/cli/lib/list-installed-plugins.ts +2 -1
  97. package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
  98. package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
  99. package/src/config/assistant-feature-flags.ts +7 -2
  100. package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
  101. package/src/config/bundled-skills/subagent/SKILL.md +3 -1
  102. package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
  103. package/src/config/bundled-skills/visualize/SKILL.md +163 -0
  104. package/src/config/call-site-defaults.ts +3 -2
  105. package/src/config/default-profile-catalog.ts +75 -70
  106. package/src/config/default-profile-names.ts +13 -28
  107. package/src/config/env-registry.ts +1 -0
  108. package/src/config/feature-flag-registry.json +16 -0
  109. package/src/config/llm-resolver.ts +21 -5
  110. package/src/config/loader.ts +18 -11
  111. package/src/config/schemas/llm.ts +1 -9
  112. package/src/config/schemas/memory-retrospective.ts +9 -0
  113. package/src/config/schemas/monitoring.ts +28 -2
  114. package/src/config/schemas/workspace-git.ts +15 -0
  115. package/src/config/seed-inference-profiles.ts +19 -5
  116. package/src/context/post-turn-tool-result-truncation.ts +2 -2
  117. package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
  118. package/src/conversations/message-consolidation.ts +17 -15
  119. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
  120. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
  121. package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
  122. package/src/daemon/conversation-agent-loop.ts +107 -22
  123. package/src/daemon/conversation-error.ts +169 -48
  124. package/src/daemon/conversation-messaging.ts +87 -4
  125. package/src/daemon/conversation-process.ts +57 -45
  126. package/src/daemon/conversation-runtime-assembly.ts +57 -0
  127. package/src/daemon/conversation-store.ts +36 -1
  128. package/src/daemon/conversation-surfaces.ts +68 -6
  129. package/src/daemon/conversation-turn-finalize.ts +76 -18
  130. package/src/daemon/conversation.ts +122 -27
  131. package/src/daemon/lifecycle.ts +9 -0
  132. package/src/daemon/message-types/conversations.ts +8 -0
  133. package/src/daemon/message-types/surfaces.ts +3 -0
  134. package/src/documents/document-store.ts +247 -10
  135. package/src/home/__tests__/feed-types.test.ts +43 -8
  136. package/src/home/__tests__/feed-writer.test.ts +59 -0
  137. package/src/home/feed-types.ts +34 -7
  138. package/src/home/feed-writer.ts +15 -6
  139. package/src/live-voice/__tests__/activity-label.test.ts +95 -0
  140. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  141. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
  142. package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
  143. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
  144. package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
  145. package/src/live-voice/activity-label.ts +169 -0
  146. package/src/live-voice/live-activity-reporter.ts +35 -6
  147. package/src/live-voice/live-voice-session.ts +282 -41
  148. package/src/live-voice/protocol.ts +35 -0
  149. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
  150. package/src/messaging/providers/slack/adapter.ts +86 -19
  151. package/src/messaging/providers/slack/api.test.ts +85 -1
  152. package/src/messaging/providers/slack/api.ts +121 -288
  153. package/src/messaging/providers/slack/client.ts +20 -251
  154. package/src/messaging/providers/slack/send.test.ts +4 -9
  155. package/src/messaging/providers/slack/send.ts +1 -1
  156. package/src/messaging/providers/slack/types.ts +5 -0
  157. package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
  158. package/src/messaging/providers/slack/web-api-transport.ts +367 -0
  159. package/src/messaging/providers/slack/withdraw.ts +6 -14
  160. package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
  161. package/src/messaging/providers/telegram-bot/send.ts +23 -2
  162. package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
  163. package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
  164. package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
  165. package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
  166. package/src/monitoring/file-descriptors.ts +262 -0
  167. package/src/monitoring/process-memory.ts +4 -11
  168. package/src/monitoring/resource-sampler.ts +5 -0
  169. package/src/monitoring/worker.ts +12 -0
  170. package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
  171. package/src/notifications/__tests__/broadcaster.test.ts +347 -7
  172. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  173. package/src/notifications/__tests__/decision-engine.test.ts +349 -29
  174. package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
  175. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  176. package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
  177. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  178. package/src/notifications/adapters/macos.ts +3 -0
  179. package/src/notifications/adapters/platform.ts +90 -14
  180. package/src/notifications/adapters/telegram.ts +6 -4
  181. package/src/notifications/assistant-reply-producer.ts +219 -0
  182. package/src/notifications/broadcaster.ts +523 -317
  183. package/src/notifications/copy-composer.ts +27 -6
  184. package/src/notifications/decision-engine.ts +154 -77
  185. package/src/notifications/deterministic-checks.ts +6 -7
  186. package/src/notifications/edit-notification.ts +8 -4
  187. package/src/notifications/emit-signal.ts +49 -16
  188. package/src/notifications/guardian-delivery-recorder.ts +8 -2
  189. package/src/notifications/home-feed-side-effect.ts +43 -6
  190. package/src/notifications/notification-utils.ts +39 -4
  191. package/src/notifications/signal.ts +10 -0
  192. package/src/notifications/types.ts +17 -0
  193. package/src/persistence/bookmark-crud.ts +18 -9
  194. package/src/persistence/conversation-attention-store.ts +26 -0
  195. package/src/persistence/conversation-crud.ts +79 -32
  196. package/src/persistence/conversation-queries.ts +30 -15
  197. package/src/persistence/conversation-title-service.ts +5 -170
  198. package/src/persistence/conversation-types.ts +192 -1
  199. package/src/persistence/db-init.ts +14 -1
  200. package/src/persistence/db-maintenance.ts +5 -4
  201. package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
  202. package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
  203. package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
  204. package/src/persistence/embeddings/embedding-local.ts +2 -1
  205. package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
  206. package/src/persistence/embeddings/plugin-index.ts +84 -12
  207. package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
  208. package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
  209. package/src/persistence/planner-statistics.ts +14 -0
  210. package/src/persistence/schema/documents.ts +26 -11
  211. package/src/persistence/steps.ts +2 -0
  212. package/src/platform/client.test.ts +172 -2
  213. package/src/platform/client.ts +178 -55
  214. package/src/plugin-api/conversation-turn.ts +13 -0
  215. package/src/plugin-api/model-profiles.test.ts +6 -2
  216. package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
  217. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
  218. package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
  219. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
  220. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
  224. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
  225. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
  226. package/src/plugins/defaults/memory/host-utils.ts +5 -0
  227. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
  228. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
  229. package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
  230. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
  231. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
  232. package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
  233. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
  234. package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
  235. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
  236. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
  237. package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
  238. package/src/plugins/defaults/memory/v2/injection.ts +48 -3
  239. package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
  240. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
  241. package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
  242. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  243. package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
  244. package/src/plugins/types.ts +17 -0
  245. package/src/prompts/templates/system-sections.ts +2 -2
  246. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  247. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  248. package/src/providers/call-site-routing.ts +77 -14
  249. package/src/providers/connection-resolution.ts +42 -2
  250. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
  251. package/src/providers/inference/adapter-factory.ts +70 -12
  252. package/src/providers/model-catalog.ts +0 -11
  253. package/src/providers/model-intents.ts +11 -0
  254. package/src/providers/registry.ts +8 -0
  255. package/src/providers/retry.ts +85 -17
  256. package/src/providers/types.ts +8 -1
  257. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  258. package/src/runtime/agent-wake.ts +132 -9
  259. package/src/runtime/channel-approval-types.ts +25 -0
  260. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
  261. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
  262. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
  263. package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
  264. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
  265. package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
  266. package/src/runtime/routes/consolidation-routes.ts +1 -1
  267. package/src/runtime/routes/conversation-management-routes.ts +18 -6
  268. package/src/runtime/routes/conversation-query-routes.ts +14 -23
  269. package/src/runtime/routes/conversation-routes.ts +113 -8
  270. package/src/runtime/routes/credential-routes.ts +1 -5
  271. package/src/runtime/routes/documents-routes.ts +207 -12
  272. package/src/runtime/routes/identity-routes.ts +3 -93
  273. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
  274. package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
  275. package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
  276. package/src/runtime/routes/notification-routes.ts +4 -1
  277. package/src/runtime/routes/platform-routes.ts +238 -3
  278. package/src/runtime/routes/playground/guard.ts +1 -2
  279. package/src/runtime/routes/slack-channel-routes.ts +2 -4
  280. package/src/runtime/routes/workspace-commit-routes.ts +49 -2
  281. package/src/runtime/routes/workspace-routes.ts +11 -12
  282. package/src/runtime/routes/workspace-utils.ts +49 -2
  283. package/src/runtime/services/conversation-serializer.ts +2 -4
  284. package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
  285. package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
  286. package/src/subagent/__tests__/consult-context.test.ts +84 -0
  287. package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
  288. package/src/subagent/consult-context.ts +410 -0
  289. package/src/subagent/consult-prompt.ts +38 -5
  290. package/src/subagent/manager.ts +9 -4
  291. package/src/subagent/types.ts +8 -0
  292. package/src/telemetry/telemetry-event-sources.ts +7 -0
  293. package/src/telemetry/telemetry-wire-source.json +1 -1
  294. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  295. package/src/telemetry/turn-events-store.ts +30 -1
  296. package/src/telemetry/types.ts +24 -0
  297. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  298. package/src/tools/browser/pinned-tabs.ts +3 -1
  299. package/src/tools/subagent/spawn.ts +23 -1
  300. package/src/tools/terminal/safe-env.ts +1 -0
  301. package/src/tools/ui-surface/definitions.ts +39 -1
  302. package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
  303. package/src/tools/ui-surface/visual-validation.ts +787 -0
  304. package/src/tools/workflows/run-workflow.test.ts +1 -0
  305. package/src/util/__tests__/short-title.test.ts +229 -0
  306. package/src/util/__tests__/worker-compute.test.ts +65 -0
  307. package/src/util/cgroup-cpu.ts +93 -0
  308. package/src/util/errors.ts +27 -0
  309. package/src/util/process-tree.ts +19 -0
  310. package/src/util/short-title.ts +189 -0
  311. package/src/util/worker-compute.ts +83 -0
  312. package/src/workspace/byok-default-profile-ensure.ts +25 -15
  313. package/src/workspace/git-service.ts +79 -30
  314. package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
  315. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  316. package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
  317. package/src/workspace/migrations/registry.ts +6 -0
  318. package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
@@ -120,6 +120,7 @@ import type { AssistantSurface } from "./conversation-agent-loop.js";
120
120
  import {
121
121
  buildConversationErrorMessage,
122
122
  classifyConversationError,
123
+ type ConversationErrorAttribution,
123
124
  maxTokensReachedClassification,
124
125
  } from "./conversation-error.js";
125
126
  import { buildDeferredFinalizeEffect } from "./conversation-turn-finalize.js";
@@ -224,6 +225,13 @@ export interface EventHandlerState {
224
225
  * path.
225
226
  */
226
227
  providerErrorCode: string | null;
228
+ /**
229
+ * Classified category of the most recent provider error
230
+ * (`classifyConversationError(...).errorCategory`). Stamped onto the
231
+ * synthetic error row's metadata alongside {@link providerErrorCode} when
232
+ * the loop persists the failure as an assistant message.
233
+ */
234
+ providerErrorCategory: string | null;
227
235
  persistProviderErrorAsAssistantMessage: boolean;
228
236
  lastAssistantMessageId: string | undefined;
229
237
  /**
@@ -487,6 +495,14 @@ export interface EventHandlerState {
487
495
  * subprocess env override can never rewrite.
488
496
  */
489
497
  readonly stagedRevealIdentities: Set<string>;
498
+ /**
499
+ * `ui_show` tool calls whose streaming input has already been classified
500
+ * for a surface placeholder (see {@link handleInputJsonDelta}). Holds both
501
+ * outcomes, the ones that emitted `ui_surface_pending` and the ones whose
502
+ * prefix proved they are not a `visual`, so the scan runs once per call
503
+ * rather than on every delta.
504
+ */
505
+ readonly surfacePendingScannedToolUseIds: Set<string>;
490
506
  /**
491
507
  * In-flight priming of {@link liveRevealGuardEntries}. The dispatcher
492
508
  * awaits this before processing a `text_delta` (and before the
@@ -540,6 +556,8 @@ export interface EventHandlerDeps {
540
556
  * degrades gracefully when it's absent.
541
557
  */
542
558
  readonly latencyTracker?: TurnLatencyTracker;
559
+ /** Best-effort resolved route metadata for provider error classification. */
560
+ readonly errorAttribution?: () => ConversationErrorAttribution;
543
561
  }
544
562
 
545
563
  // ── Factory ──────────────────────────────────────────────────────────
@@ -560,6 +578,7 @@ export function createEventHandlerState(): EventHandlerState {
560
578
  model: "",
561
579
  providerErrorUserMessage: null,
562
580
  providerErrorCode: null,
581
+ providerErrorCategory: null,
563
582
  persistProviderErrorAsAssistantMessage: false,
564
583
  lastAssistantMessageId: undefined,
565
584
  assistantRowAwaitingFinalization: false,
@@ -606,6 +625,7 @@ export function createEventHandlerState(): EventHandlerState {
606
625
  candidateRemintAuthorities: [],
607
626
  forChatMintWatermark: currentForChatMintWatermark(),
608
627
  stagedRevealIdentities: new Set(),
628
+ surfacePendingScannedToolUseIds: new Set(),
609
629
  liveRevealGuardPriming: undefined,
610
630
  };
611
631
  }
@@ -628,7 +648,7 @@ export function createEventHandlerState(): EventHandlerState {
628
648
  async function chatRevealCandidates(
629
649
  state: EventHandlerState,
630
650
  ): Promise<readonly ResolvedRevealCandidate[] | undefined> {
631
- if (!isAssistantFeatureFlagEnabled("chat-credential-reveal", getConfig())) {
651
+ if (!isAssistantFeatureFlagEnabled("chat-credential-reveal")) {
632
652
  return undefined;
633
653
  }
634
654
  return resolvedRevealCandidatesForState(state);
@@ -1169,6 +1189,25 @@ function schedulePartialFlush(
1169
1189
  // tools the client discards it (extractCodePreview only handles app tools),
1170
1190
  // so we skip forwarding entirely to avoid transport/decode overhead.
1171
1191
  const APP_TOOL_NAMES = new Set(["app_create"]);
1192
+
1193
+ // ── Surface Placeholder Detection ────────────────────────────────────
1194
+ // A `visual` ui_show streams the longest tool input the model produces, and
1195
+ // nothing is on screen while it does: the ui_show chip is suppressed because
1196
+ // the surface renders in its place, and the surface only exists once the call
1197
+ // closes. Sniffing `surface_type` off the accumulated prefix lets the client
1198
+ // hold a placeholder for that gap. The input itself is never forwarded: the
1199
+ // fragment is large and the client has no use for a partial one.
1200
+ const UI_SHOW_TOOL_NAME = "ui_show";
1201
+
1202
+ /**
1203
+ * How much of the accumulated input the surface-type sniff reads. `ui_show`
1204
+ * declares `surface_type` first, so a real one lands in the first few dozen
1205
+ * characters; past this window the call is classified as "not a visual" and
1206
+ * never re-scanned.
1207
+ */
1208
+ const SURFACE_TYPE_SCAN_CHARS = 400;
1209
+
1210
+ const VISUAL_SURFACE_TYPE_PATTERN = /"surface_type"\s*:\s*"visual"/;
1172
1211
  const MAX_TOKENS_CONTINUE_PROMPT =
1173
1212
  "Continue from where you stopped. Do not repeat content you've already sent.";
1174
1213
  const MAX_TOKENS_SURFACE_COMPLETION_SUMMARY = "Continue";
@@ -1734,11 +1773,48 @@ function handleToolOutputChunk(
1734
1773
  });
1735
1774
  }
1736
1775
 
1776
+ /**
1777
+ * Announce a still-streaming `ui_show` that is going to produce a `visual`
1778
+ * surface, so the client can hold a placeholder for it.
1779
+ *
1780
+ * Runs at most once per tool call: the first delta whose prefix matches emits,
1781
+ * and a call whose prefix grows past the scan window without matching is
1782
+ * recorded as classified so later deltas cost nothing.
1783
+ */
1784
+ function announcePendingVisualSurface(
1785
+ state: EventHandlerState,
1786
+ deps: EventHandlerDeps,
1787
+ event: Extract<AgentEvent, { type: "input_json_delta" }>,
1788
+ ): void {
1789
+ if (state.surfacePendingScannedToolUseIds.has(event.toolUseId)) {
1790
+ return;
1791
+ }
1792
+ const prefix = event.accumulatedJson.slice(0, SURFACE_TYPE_SCAN_CHARS);
1793
+ if (!VISUAL_SURFACE_TYPE_PATTERN.test(prefix)) {
1794
+ if (event.accumulatedJson.length >= SURFACE_TYPE_SCAN_CHARS) {
1795
+ state.surfacePendingScannedToolUseIds.add(event.toolUseId);
1796
+ }
1797
+ return;
1798
+ }
1799
+ state.surfacePendingScannedToolUseIds.add(event.toolUseId);
1800
+ deps.onEvent({
1801
+ type: "ui_surface_pending",
1802
+ surfaceType: "visual",
1803
+ conversationId: deps.ctx.conversationId,
1804
+ toolUseId: event.toolUseId,
1805
+ messageId: state.lastAssistantMessageId,
1806
+ });
1807
+ }
1808
+
1737
1809
  export function handleInputJsonDelta(
1738
1810
  state: EventHandlerState,
1739
1811
  deps: EventHandlerDeps,
1740
1812
  event: Extract<AgentEvent, { type: "input_json_delta" }>,
1741
1813
  ): void {
1814
+ if (event.toolName === UI_SHOW_TOOL_NAME) {
1815
+ announcePendingVisualSurface(state, deps, event);
1816
+ return;
1817
+ }
1742
1818
  // Only forward input deltas for app tools — the client only uses this
1743
1819
  // stream for app_create code previews. Non-app tools would send large
1744
1820
  // cumulative JSON on every delta with no benefit.
@@ -2555,9 +2631,7 @@ function handleError(
2555
2631
  deps: EventHandlerDeps,
2556
2632
  event: Extract<AgentEvent, { type: "error" }>,
2557
2633
  ): void {
2558
- const classified = classifyConversationError(event.error, {
2559
- phase: "agent_loop",
2560
- });
2634
+ const classified = classifyAgentLoopError(event.error, deps);
2561
2635
  if (classified.errorCategory === "provider_api_error") {
2562
2636
  log.error(
2563
2637
  {
@@ -2582,6 +2656,7 @@ function handleError(
2582
2656
  );
2583
2657
  state.providerErrorUserMessage = classified.userMessage;
2584
2658
  state.providerErrorCode = classified.code;
2659
+ state.providerErrorCategory = classified.errorCategory;
2585
2660
  state.persistProviderErrorAsAssistantMessage =
2586
2661
  shouldPersistProviderErrorAsAssistantMessage(classified);
2587
2662
  }
@@ -3066,9 +3141,7 @@ function handleProviderError(
3066
3141
  deps: EventHandlerDeps,
3067
3142
  event: Extract<AgentEvent, { type: "provider_error" }>,
3068
3143
  ): void {
3069
- const classified = classifyConversationError(event.error, {
3070
- phase: "agent_loop",
3071
- });
3144
+ const classified = classifyAgentLoopError(event.error, deps);
3072
3145
  if (!shouldPersistProviderErrorAsAssistantMessage(classified)) {
3073
3146
  return;
3074
3147
  }
@@ -3090,6 +3163,16 @@ function handleProviderError(
3090
3163
  }
3091
3164
  }
3092
3165
 
3166
+ function classifyAgentLoopError(
3167
+ error: Error,
3168
+ deps: EventHandlerDeps,
3169
+ ): ReturnType<typeof classifyConversationError> {
3170
+ return classifyConversationError(error, {
3171
+ phase: "agent_loop",
3172
+ ...deps.errorAttribution?.(),
3173
+ });
3174
+ }
3175
+
3093
3176
  // ── Dispatcher ───────────────────────────────────────────────────────
3094
3177
 
3095
3178
  /** Routes an AgentEvent to the appropriate handler. */
@@ -30,7 +30,7 @@ import {
30
30
  } from "../config/llm-context-resolution.js";
31
31
  import {
32
32
  resolveCallSiteConfig,
33
- resolveEffectiveProfileKey,
33
+ resolveCallSiteConfigWithProfile,
34
34
  resolveProfilelessModelKey,
35
35
  selectWinningProfile,
36
36
  } from "../config/llm-resolver.js";
@@ -47,6 +47,7 @@ import {
47
47
  getLastUserTimestampBefore,
48
48
  getMessageById,
49
49
  provenanceFromTrustContext,
50
+ PROVIDER_ERROR_MESSAGE_KIND,
50
51
  resolveOverrideProfile,
51
52
  updateConversationContextWindow,
52
53
  updateConversationSlackContextWatermark,
@@ -60,6 +61,11 @@ import { HOOKS } from "../plugin-api/constants.js";
60
61
  import type { ConversationGraphMemory } from "../plugins/defaults/memory/graph/conversation-graph-memory.js";
61
62
  import { enqueueMemoryRetrospectiveOnCompaction } from "../plugins/defaults/memory/memory-retrospective-enqueue.js";
62
63
  import { runHook } from "../plugins/pipeline.js";
64
+ import { isManagedConnectionRoute } from "../providers/connection-resolution.js";
65
+ import {
66
+ ConnectionResolutionError,
67
+ resolveRoutingIdentity,
68
+ } from "../providers/routing-identity.js";
63
69
  import type { ContentBlock, Message } from "../providers/types.js";
64
70
  import type { Provider } from "../providers/types.js";
65
71
  import { resolveCapabilities } from "../runtime/capabilities.js";
@@ -94,6 +100,7 @@ import {
94
100
  budgetYieldUnrecoveredClassification,
95
101
  buildConversationErrorMessage,
96
102
  classifyConversationError,
103
+ type ConversationErrorAttribution,
97
104
  isUserCancellation,
98
105
  } from "./conversation-error.js";
99
106
  import { raceWithTimeout } from "./conversation-media-retry.js";
@@ -132,6 +139,19 @@ const log = getLogger("conversation-agent-loop");
132
139
  const DISK_PRESSURE_ERROR_CODE = "DISK_SPACE_CRITICAL" as const;
133
140
  const DISK_PRESSURE_ERROR_CATEGORY = "disk_pressure";
134
141
 
142
+ /**
143
+ * Assistant-voice wording persisted as the synthetic assistant row when a
144
+ * turn terminates because managed credits ran out. The shared classification
145
+ * (`managedBalanceClassification` in conversation-error.ts) keeps its
146
+ * `userMessage` context-neutral because it also feeds the non-terminal
147
+ * memory-v3 degraded notice, where a normal reply still follows; here the
148
+ * turn truly ends with no reply, so first-person copy is accurate. Phrased
149
+ * about the past failure, not the current balance, because the row stays in
150
+ * the transcript (and renders as a plain bubble) after the balance recovers.
151
+ */
152
+ const OUT_OF_CREDITS_ASSISTANT_REPLY =
153
+ "I couldn't reply because you ran out of credits. Add credits in Settings → Billing and we can pick up where we left off.";
154
+
135
155
  /** Title-cased friendly labels for tool names, used in confirmation chips. */
136
156
  const TOOL_FRIENDLY_LABEL: Record<string, string> = {
137
157
  bash: "Run Command",
@@ -254,6 +274,22 @@ export async function runAgentLoopImpl(
254
274
  * the turn.
255
275
  */
256
276
  isHiddenPrompt?: boolean;
277
+ /**
278
+ * Row the end-of-turn reply notification should treat as the prompt this
279
+ * turn answers. Defaults to `userMessageId`; a coalesced batch overrides it
280
+ * with its last push-eligible member (see
281
+ * `isReplyPushIneligibleUserMessage`), whose reply the user is actually
282
+ * waiting on, rather than a suppressed row that merely landed last.
283
+ */
284
+ notifyUserMessageId?: string;
285
+ /**
286
+ * True when this run's reply streams to the app and nowhere else, so the
287
+ * end-of-turn reply notification ignores the initiating row's
288
+ * channel/voice delivery markers (see
289
+ * `isReplyPushIneligibleUserMessage`). Set by the retry route, which
290
+ * re-runs a stored anchor row without its original delivery orchestration.
291
+ */
292
+ replyDeliveredInAppOnly?: boolean;
257
293
  /**
258
294
  * LLM call-site identifier threaded into the per-call provider config.
259
295
  * Adapter callers (heartbeat, filing, scheduler, etc.) pass their own
@@ -392,10 +428,7 @@ export async function runAgentLoopImpl(
392
428
  // connection and profile so credential/connection errors point at the
393
429
  // exact slot to fix instead of a generic banner. Resolution can itself
394
430
  // throw on a broken config — attribution must never mask the real error.
395
- const turnErrorAttribution = (): {
396
- connectionName?: string;
397
- profileName?: string;
398
- } => {
431
+ const turnErrorAttribution = (): ConversationErrorAttribution => {
399
432
  try {
400
433
  const overrideProfile = readCurrentOverrideProfile();
401
434
  const resolveOpts = {
@@ -403,21 +436,30 @@ export async function runAgentLoopImpl(
403
436
  forceOverrideProfile,
404
437
  selectionSeed: ctx.conversationId,
405
438
  };
406
- const resolved = resolveCallSiteConfig(
407
- turnCallSite,
408
- config.llm,
409
- resolveOpts,
410
- );
411
- const profileName = resolveEffectiveProfileKey(
412
- turnCallSite,
413
- config.llm,
414
- resolveOpts,
415
- );
439
+ const { config: resolved, profileName } =
440
+ resolveCallSiteConfigWithProfile(turnCallSite, config.llm, resolveOpts);
441
+ let connectionName = resolved.provider_connection;
442
+ try {
443
+ connectionName =
444
+ resolveRoutingIdentity(resolved.provider, resolved.model)
445
+ ?.connectionName ?? connectionName;
446
+ } catch (error) {
447
+ if (!(error instanceof ConnectionResolutionError)) {
448
+ throw error;
449
+ }
450
+ connectionName = error.connectionName;
451
+ }
452
+ // Managed-ness comes from the connection row, matching what dispatch
453
+ // decides. The profile's own provider can't stand in: a concrete
454
+ // provider tweak over a managed winner keeps the managed connection
455
+ // while replacing the provider (`llm-resolver.ts`).
456
+ const isManagedRoute = connectionName
457
+ ? isManagedConnectionRoute(connectionName)
458
+ : undefined;
416
459
  return {
417
- ...(resolved.provider_connection
418
- ? { connectionName: resolved.provider_connection }
419
- : {}),
460
+ ...(connectionName ? { connectionName } : {}),
420
461
  ...(profileName ? { profileName } : {}),
462
+ ...(isManagedRoute !== undefined ? { isManagedRoute } : {}),
421
463
  };
422
464
  } catch {
423
465
  return {};
@@ -620,6 +662,11 @@ export async function runAgentLoopImpl(
620
662
  // has been emitted. Guards the catch block: an error thrown afterwards
621
663
  // (deferred turn-tail bookkeeping) must not relabel a visibly-replied turn.
622
664
  let turnReplied = false;
665
+ // Narrower than `turnReplied`: true only for a `message_complete` branch that
666
+ // produced a genuine reply. A handed-off generation continues the run, and a
667
+ // provider-error turn's only assistant row is the synthetic error text, so
668
+ // the deferred tail must not treat either as a final reply.
669
+ let turnCompleted = false;
623
670
 
624
671
  const publishLoopMessagesChanged = (): void => {
625
672
  if (
@@ -914,6 +961,10 @@ export async function runAgentLoopImpl(
914
961
  // `client_os` into the in-flight turn.
915
962
  ctx.currentTurnClientOs = ctx.clientOs ?? undefined;
916
963
 
964
+ // Freeze the app the client had in view at turn start, for the same
965
+ // anti-race reason as `client_os` above.
966
+ ctx.currentTurnVisibleAppId = ctx.visibleAppId ?? undefined;
967
+
917
968
  // Resolve the effective profile key for this turn and detect changes.
918
969
  // `modelProfileKey` is the actual profile used for this turn. The
919
970
  // notice key is narrower: it only marks turns where runtime context should
@@ -1014,6 +1065,7 @@ export async function runAgentLoopImpl(
1014
1065
  turnInterfaceContext: capturedTurnInterfaceContext,
1015
1066
  applyCompaction: applySuccessfulCompaction,
1016
1067
  latencyTracker,
1068
+ errorAttribution: turnErrorAttribution,
1017
1069
  };
1018
1070
  const eventHandler = (event: AgentEvent): Promise<void> => {
1019
1071
  if (
@@ -1343,10 +1395,23 @@ export async function runAgentLoopImpl(
1343
1395
  capturedTurnInterfaceContext.userMessageInterface,
1344
1396
  assistantMessageInterface:
1345
1397
  capturedTurnInterfaceContext.assistantMessageInterface,
1398
+ // Mark the synthetic row as a provider-failure notice so clients
1399
+ // can render it as a themed card instead of persona speech
1400
+ // (`isProviderErrorMetadata` -> the wire `providerError` field).
1401
+ messageKind: PROVIDER_ERROR_MESSAGE_KIND,
1402
+ providerErrorCode: state.providerErrorCode ?? undefined,
1403
+ providerErrorCategory: state.providerErrorCategory ?? undefined,
1346
1404
  };
1347
- const errorAssistantMessage = createAssistantMessage(
1348
- state.providerErrorUserMessage,
1349
- );
1405
+ // The persisted row re-enters LLM history and is displayed as
1406
+ // assistant speech, so managed-credits exhaustion swaps the
1407
+ // context-neutral classification copy for assistant-voice wording.
1408
+ const persistedErrorText =
1409
+ state.providerErrorCode === "PROVIDER_BILLING" &&
1410
+ state.providerErrorCategory === "credits_exhausted"
1411
+ ? OUT_OF_CREDITS_ASSISTANT_REPLY
1412
+ : state.providerErrorUserMessage;
1413
+ const errorAssistantMessage =
1414
+ createAssistantMessage(persistedErrorText);
1350
1415
  const errorRow = await addMessage(
1351
1416
  ctx.conversationId,
1352
1417
  "assistant",
@@ -1506,6 +1571,7 @@ export async function runAgentLoopImpl(
1506
1571
  publishLoopMessagesChanged();
1507
1572
  } else {
1508
1573
  turnReplied = true;
1574
+ turnCompleted = !persistedErrorAssistantMessage;
1509
1575
  ctx.emitActivityState("idle", "message_complete", {
1510
1576
  anchor: "global",
1511
1577
  requestId: reqId,
@@ -1541,7 +1607,17 @@ export async function runAgentLoopImpl(
1541
1607
  // drain the deferred bookkeeping — after the SSE, before the `finally`
1542
1608
  // commits and drains the queue for the next turn.
1543
1609
  await settlePendingPartialFlush(state, deps);
1544
- await runDeferredTurnTail({ ctx, state, rlog, generationCompletedAt });
1610
+ await runDeferredTurnTail({
1611
+ ctx,
1612
+ state,
1613
+ rlog,
1614
+ generationCompletedAt,
1615
+ turnCompleted,
1616
+ userMessageId: options?.notifyUserMessageId ?? userMessageId,
1617
+ ...(options?.replyDeliveredInAppOnly
1618
+ ? { replyDeliveredInAppOnly: true }
1619
+ : {}),
1620
+ });
1545
1621
  } catch (err) {
1546
1622
  clearConversationNotices(ctx.conversationId);
1547
1623
  const errorCtx = {
@@ -1669,6 +1745,15 @@ export async function runAgentLoopImpl(
1669
1745
  // Channel command intents (e.g. Telegram /start) are single-turn metadata.
1670
1746
  // Clear at turn end so they never leak into subsequent unrelated messages.
1671
1747
  ctx.commandIntent = undefined;
1748
+ // The app on screen is view state owned by the message that reported it,
1749
+ // not durable conversation state: nothing tells the daemon when the user
1750
+ // closes an app. Clear both copies at turn end so a later turn that brings
1751
+ // no client transport (a scheduled wake, a background follow-up) cannot
1752
+ // claim the user is looking at an app they may have closed. Every inbound
1753
+ // send re-applies it from transport before its own turn, including the
1754
+ // queue drain, so live turns are unaffected.
1755
+ ctx.visibleAppId = undefined;
1756
+ ctx.currentTurnVisibleAppId = undefined;
1672
1757
  // taskRunId scopes ephemeral task-run permissions to a single turn. Clear
1673
1758
  // before drainQueue so queued/drained turns on a reused conversation can't
1674
1759
  // inherit stale in-task-run scope from the turn that just finished.