@vellumai/assistant 0.12.2 → 0.12.3-staging.1

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 (298) hide show
  1. package/.env.example +2 -0
  2. package/AGENTS.md +1 -1
  3. package/docs/architecture/integrations.md +7 -0
  4. package/docs/architecture/memory.md +17 -2
  5. package/docs/credential-execution-service.md +1 -1
  6. package/docs/desktop-browser-cli.md +7 -3
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
  10. package/node_modules/@vellumai/environments/src/shell.ts +24 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  13. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  15. package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  16. package/openapi.yaml +54 -5
  17. package/package.json +1 -1
  18. package/scripts/smoke-desktop-browser-cli.ts +1 -0
  19. package/scripts/sync-llm-catalog.ts +3 -0
  20. package/scripts/voice-ttft-spike.ts +2 -2
  21. package/src/__tests__/agent-loop.test.ts +259 -0
  22. package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
  23. package/src/__tests__/anthropic-provider.test.ts +55 -0
  24. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  25. package/src/__tests__/attachments-store.test.ts +22 -3
  26. package/src/__tests__/channel-approval.test.ts +9 -14
  27. package/src/__tests__/channel-reply-delivery.test.ts +56 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +23 -0
  29. package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
  30. package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +257 -6
  34. package/src/__tests__/conversation-attachments.test.ts +106 -0
  35. package/src/__tests__/conversation-fork-crud.test.ts +191 -0
  36. package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
  37. package/src/__tests__/credential-execution-client.test.ts +70 -53
  38. package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
  39. package/src/__tests__/events-tail-route.test.ts +33 -0
  40. package/src/__tests__/history-repair.test.ts +53 -0
  41. package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
  42. package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
  43. package/src/__tests__/llm-catalog-parity.test.ts +27 -4
  44. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  45. package/src/__tests__/oauth-commands-routes.test.ts +114 -101
  46. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  47. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  48. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  49. package/src/__tests__/persist-media-references.test.ts +50 -0
  50. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  51. package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
  52. package/src/__tests__/schedule-routes.test.ts +20 -0
  53. package/src/__tests__/scheduler-result-notification.test.ts +23 -4
  54. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  55. package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
  56. package/src/__tests__/secret-routes-scrub.test.ts +22 -27
  57. package/src/__tests__/secret-scanner.test.ts +20 -0
  58. package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
  59. package/src/__tests__/secure-keys.test.ts +7 -3
  60. package/src/__tests__/server-tool-pairing.test.ts +107 -0
  61. package/src/__tests__/skills.test.ts +5 -4
  62. package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
  63. package/src/__tests__/terminal-tools.test.ts +8 -0
  64. package/src/__tests__/tool-result-follow-up.test.ts +99 -0
  65. package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
  66. package/src/__tests__/unicode.test.ts +36 -0
  67. package/src/agent/loop.ts +37 -12
  68. package/src/agent/tool-result-follow-up.ts +70 -0
  69. package/src/api/attachment-provenance.test.ts +68 -0
  70. package/src/api/computer-use-tool.test.ts +55 -0
  71. package/src/api/computer-use-tool.ts +27 -0
  72. package/src/api/events/assistant-outbound-attachment.ts +3 -0
  73. package/src/api/events/desktop-activity-changed.ts +10 -0
  74. package/src/api/events/question-request.ts +1 -0
  75. package/src/api/index.ts +11 -0
  76. package/src/api/responses/conversation-message.ts +3 -0
  77. package/src/approvals/approval-primitive.ts +5 -2
  78. package/src/approvals/scoped-approval-grants.ts +6 -2
  79. package/src/browser/virtual-desktop-target.ts +1 -2
  80. package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
  81. package/src/cli/commands/__tests__/plugins.test.ts +20 -1
  82. package/src/cli/commands/__tests__/schedules.test.ts +14 -0
  83. package/src/cli/commands/bash.help.ts +4 -3
  84. package/src/cli/commands/browser.help.ts +5 -1
  85. package/src/cli/commands/credentials.help.ts +3 -3
  86. package/src/cli/commands/oauth/index.help.ts +10 -0
  87. package/src/cli/commands/oauth/providers.ts +12 -0
  88. package/src/cli/commands/oauth/request.test.ts +179 -2
  89. package/src/cli/commands/oauth/request.ts +41 -10
  90. package/src/cli/commands/plugins.ts +12 -5
  91. package/src/cli/commands/schedules.ts +2 -0
  92. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  93. package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
  94. package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
  95. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
  96. package/src/cli/lib/bundled-marketplace.json +855 -0
  97. package/src/cli/lib/bundled-plugin-packages.json +783 -1
  98. package/src/cli/lib/inspect-plugin.ts +11 -4
  99. package/src/cli/lib/upgrade-plugin.ts +19 -11
  100. package/src/cli/output.ts +18 -4
  101. package/src/config/__tests__/default-provider.test.ts +4 -0
  102. package/src/config/bundled-skills/schedule/SKILL.md +6 -6
  103. package/src/config/loader.ts +9 -17
  104. package/src/config/platform-identity.ts +8 -8
  105. package/src/config/profile-text-generation.test.ts +51 -0
  106. package/src/config/profile-text-generation.ts +51 -0
  107. package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
  108. package/src/config/schemas/llm.ts +7 -2
  109. package/src/config/schemas/mcp.ts +5 -1
  110. package/src/config/schemas/memory-v3.ts +12 -0
  111. package/src/credential-execution/ces-runtime.ts +2 -2
  112. package/src/credential-execution/executable-discovery.ts +36 -134
  113. package/src/credential-execution/process-manager.test.ts +20 -24
  114. package/src/credential-execution/process-manager.ts +24 -19
  115. package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
  116. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
  117. package/src/daemon/assistant-attachments.ts +20 -12
  118. package/src/daemon/chat-credential-redaction.ts +16 -1
  119. package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
  120. package/src/daemon/conversation-agent-loop.ts +12 -0
  121. package/src/daemon/conversation-attachments.ts +93 -12
  122. package/src/daemon/conversation-tool-setup.ts +71 -3
  123. package/src/daemon/conversation-turn-finalize.ts +26 -13
  124. package/src/daemon/conversation.ts +72 -17
  125. package/src/daemon/daemon-control.ts +3 -7
  126. package/src/daemon/lifecycle.ts +4 -4
  127. package/src/daemon/mcp-reload-service.ts +1 -1
  128. package/src/daemon/message-types/shared.ts +1 -0
  129. package/src/daemon/orphan-reaper.ts +4 -3
  130. package/src/daemon/persist-media-references.ts +20 -3
  131. package/src/daemon/tool-setup-types.ts +6 -0
  132. package/src/daemon/wake-conversation-ops.ts +50 -15
  133. package/src/desktop/desktop-automation-lease.test.ts +230 -0
  134. package/src/desktop/desktop-automation-lease.ts +90 -6
  135. package/src/desktop/desktop-help.ts +15 -0
  136. package/src/desktop/desktop-stream-bridge.test.ts +65 -10
  137. package/src/desktop/desktop-stream-bridge.ts +3 -3
  138. package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
  139. package/src/desktop/desktop-wallpaper-worker.ts +17 -0
  140. package/src/desktop/desktop-wallpaper.test.ts +48 -7
  141. package/src/desktop/desktop-wallpaper.ts +34 -102
  142. package/src/desktop/virtual-desktop-feature.ts +1 -1
  143. package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
  144. package/src/mcp/__tests__/startup.test.ts +30 -7
  145. package/src/mcp/__tests__/tool-caps.test.ts +107 -0
  146. package/src/mcp/manager.ts +168 -101
  147. package/src/mcp/startup.ts +33 -13
  148. package/src/mcp/tool-caps.ts +176 -0
  149. package/src/messaging/provider-message-metadata.ts +3 -3
  150. package/src/notifications/__tests__/copy-composer.test.ts +70 -0
  151. package/src/notifications/copy-composer.ts +11 -3
  152. package/src/notifications/schedule-result-producer.ts +4 -6
  153. package/src/oauth/AGENTS.md +2 -0
  154. package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
  155. package/src/oauth/identity-verifier.ts +24 -0
  156. package/src/oauth/oauth-store.ts +11 -1
  157. package/src/oauth/provider-serializer.ts +1 -0
  158. package/src/oauth/seed-providers.ts +12 -0
  159. package/src/permissions/confirmation-guardian-request.test.ts +16 -0
  160. package/src/permissions/confirmation-guardian-request.ts +2 -3
  161. package/src/permissions/question-prompter.test.ts +31 -0
  162. package/src/permissions/question-prompter.ts +2 -0
  163. package/src/persistence/attachments-store.ts +84 -44
  164. package/src/persistence/conversation-crud.ts +111 -20
  165. package/src/persistence/conversation-plugin-facade.ts +30 -0
  166. package/src/persistence/conversation-tool-surface.ts +135 -0
  167. package/src/persistence/conversation-types.test.ts +32 -0
  168. package/src/persistence/conversation-types.ts +26 -4
  169. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
  170. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
  171. package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
  172. package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
  173. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
  174. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
  175. package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
  176. package/src/persistence/schema/index.ts +1 -0
  177. package/src/persistence/schema/oauth.ts +1 -0
  178. package/src/persistence/steps.ts +24 -3
  179. package/src/plugin-api/conversation-turn.ts +31 -7
  180. package/src/plugin-api/index.ts +11 -1
  181. package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
  182. package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
  183. package/src/plugins/defaults/memory/AGENTS.md +39 -5
  184. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
  185. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
  186. package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
  187. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
  188. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
  189. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
  190. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
  191. package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
  192. package/src/plugins/defaults/memory/buffer-file.ts +354 -0
  193. package/src/plugins/defaults/memory/buffer-format.ts +40 -0
  194. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
  195. package/src/plugins/defaults/memory/context-search/format.ts +2 -1
  196. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
  197. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
  198. package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
  199. package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
  200. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
  201. package/src/plugins/defaults/memory/host-utils.ts +0 -10
  202. package/src/plugins/defaults/memory/injectors.ts +4 -3
  203. package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
  204. package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
  205. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
  206. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
  207. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
  208. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
  209. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
  210. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
  211. package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
  212. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
  213. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  214. package/src/plugins/defaults/memory/tools.ts +1 -1
  215. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  216. package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
  217. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  218. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
  219. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
  220. package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
  221. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
  222. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
  223. package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
  224. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
  225. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
  226. package/src/plugins/defaults/memory/v3/card.ts +2 -1
  227. package/src/plugins/defaults/memory/v3/injector.ts +212 -178
  228. package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
  229. package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
  230. package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
  231. package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
  232. package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
  233. package/src/plugins/defaults/memory/v3/sections.ts +2 -1
  234. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
  235. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
  236. package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
  237. package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
  238. package/src/prompts/delegation-gate.ts +57 -0
  239. package/src/prompts/system-prompt.ts +14 -32
  240. package/src/providers/inference/adapter-factory.ts +6 -0
  241. package/src/providers/jev/client.test.ts +260 -0
  242. package/src/providers/jev/client.ts +518 -0
  243. package/src/providers/model-catalog.ts +58 -3
  244. package/src/providers/server-tool-pairing.ts +16 -7
  245. package/src/runtime/AGENTS.md +2 -2
  246. package/src/runtime/__tests__/agent-wake.test.ts +94 -1
  247. package/src/runtime/agent-wake.ts +31 -4
  248. package/src/runtime/guardian-action-service.ts +2 -17
  249. package/src/runtime/guardian-reply-router.ts +1 -8
  250. package/src/runtime/http-server.ts +2 -2
  251. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
  252. package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
  253. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
  254. package/src/runtime/routes/channel-route-shared.ts +1 -9
  255. package/src/runtime/routes/conversation-routes.ts +29 -4
  256. package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
  257. package/src/runtime/routes/desktop-setup-routes.ts +8 -5
  258. package/src/runtime/routes/guardian-approval-interception.ts +24 -0
  259. package/src/runtime/routes/inbound-message-handler.ts +2 -3
  260. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
  261. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
  262. package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
  263. package/src/runtime/routes/inference-profiles-routes.ts +9 -0
  264. package/src/runtime/routes/oauth-commands-routes.ts +57 -54
  265. package/src/runtime/routes/oauth-providers.ts +4 -0
  266. package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
  267. package/src/runtime/routes/oauth-request-hints.ts +149 -0
  268. package/src/runtime/routes/schedule-routes.ts +2 -0
  269. package/src/runtime/routes/secret-routes.ts +99 -119
  270. package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
  271. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
  272. package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
  273. package/src/schedule/run-script.ts +2 -2
  274. package/src/schedule/scheduler.ts +47 -11
  275. package/src/schedule/tool-surface-readiness.ts +65 -0
  276. package/src/schedule/worker-mcp.ts +85 -0
  277. package/src/schedule/worker.ts +9 -31
  278. package/src/security/secure-keys.ts +60 -48
  279. package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
  280. package/src/tools/ask-question/ask-question-tool.ts +119 -23
  281. package/src/tools/browser/browser-execution.ts +7 -1
  282. package/src/tools/host-terminal/host-shell.ts +12 -6
  283. package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
  284. package/src/tools/shared/shell-output.test.ts +10 -0
  285. package/src/tools/shared/shell-output.ts +14 -2
  286. package/src/tools/skills/sandbox-runner.ts +13 -2
  287. package/src/tools/skills/scaffold-managed.ts +2 -1
  288. package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
  289. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
  290. package/src/tools/terminal/safe-env.ts +35 -24
  291. package/src/tools/terminal/sanitized-bash.ts +15 -2
  292. package/src/tools/terminal/shell-launch.test.ts +162 -0
  293. package/src/tools/terminal/shell.test.ts +29 -0
  294. package/src/tools/terminal/shell.ts +13 -7
  295. package/src/util/browser-human-verification.ts +14 -0
  296. package/src/util/host-process.test.ts +17 -1
  297. package/src/util/host-process.ts +24 -0
  298. package/src/util/unicode.ts +29 -0
@@ -6,52 +6,7 @@
6
6
  * This module is side-effect free: importing it does not register any plugin.
7
7
  */
8
8
 
9
- const HIGH_SURROGATE_START = 0xd800;
10
- const HIGH_SURROGATE_END = 0xdbff;
11
- const LOW_SURROGATE_START = 0xdc00;
12
- const LOW_SURROGATE_END = 0xdfff;
13
-
14
- function isHighSurrogate(code: number): boolean {
15
- return code >= HIGH_SURROGATE_START && code <= HIGH_SURROGATE_END;
16
- }
17
-
18
- function isLowSurrogate(code: number): boolean {
19
- return code >= LOW_SURROGATE_START && code <= LOW_SURROGATE_END;
20
- }
21
-
22
- /**
23
- * Slice a string by code-unit indices without splitting a surrogate pair, so
24
- * the result never ends or begins mid-emoji. Indices are clamped to the
25
- * string bounds; a cut that would land between a high/low surrogate is nudged
26
- * inward to the nearest whole code point.
27
- */
28
- function safeStringSlice(
29
- str: string,
30
- start = 0,
31
- end: number = str.length,
32
- ): string {
33
- let safeStart = Math.max(0, Math.min(str.length, start));
34
- let safeEnd = Math.max(safeStart, Math.min(str.length, end));
35
-
36
- if (safeEnd < str.length && safeEnd > safeStart) {
37
- const lastCode = str.charCodeAt(safeEnd - 1);
38
- if (isHighSurrogate(lastCode)) {
39
- safeEnd--;
40
- }
41
- }
42
-
43
- if (safeStart > 0 && safeStart < str.length) {
44
- const firstCode = str.charCodeAt(safeStart);
45
- if (isLowSurrogate(firstCode)) {
46
- safeStart++;
47
- if (safeStart > safeEnd) {
48
- safeEnd = safeStart;
49
- }
50
- }
51
- }
52
-
53
- return str.slice(safeStart, safeEnd);
54
- }
9
+ import { safeStringSlice } from "@vellumai/plugin-api";
55
10
 
56
11
  /**
57
12
  * Minimum number of characters to preserve when truncating.
@@ -103,6 +103,31 @@ describe("parallel-tasks system prompt section", () => {
103
103
  }
104
104
  });
105
105
 
106
+ test("an explicit rendered state replaces the derivation", () => {
107
+ // A fork wake replaying its source's recorded surface renders what the
108
+ // source's live turn rendered, whatever its own scope could spawn.
109
+ expect(
110
+ buildSystemPrompt({
111
+ canSpawnSubagents: false,
112
+ delegateIndependentTasks: true,
113
+ }),
114
+ ).toContain(HEADING);
115
+ expect(
116
+ buildSystemPrompt({
117
+ canSpawnSubagents: true,
118
+ delegateIndependentTasks: false,
119
+ }),
120
+ ).not.toContain(HEADING);
121
+ // It is the rendered state, not a capability claim: the channel gate has
122
+ // already been applied by the turn that recorded it.
123
+ expect(
124
+ buildSystemPrompt({
125
+ channelCapabilities: channel("slack"),
126
+ delegateIndependentTasks: true,
127
+ }),
128
+ ).toContain(HEADING);
129
+ });
130
+
106
131
  test("delegates only independent tasks and keeps small requests inline", () => {
107
132
  const prompt = buildSystemPrompt({ canSpawnSubagents: true });
108
133
  expect(prompt).toContain("several independent things at once");
@@ -0,0 +1,57 @@
1
+ import type { ChannelCapabilities } from "../daemon/conversation-runtime-assembly.js";
2
+
3
+ /**
4
+ * Whether this turn arrived on an external messaging surface (Slack, Telegram,
5
+ * email, a plugin channel) rather than the app itself.
6
+ *
7
+ * `resolveChannelCapabilities` folds every first-party client (macOS, web,
8
+ * iOS, CLI, the HTTP API) onto the `vellum` channel, and a prompt built
9
+ * outside a turn carries no capabilities at all, so those two cases are the
10
+ * app and everything else is a channel the gateway delivers to over a reply
11
+ * callback.
12
+ */
13
+ export function isExternalChannelTurn(
14
+ capabilities: ChannelCapabilities | undefined,
15
+ ): boolean {
16
+ const channel = capabilities?.channel;
17
+ return channel !== undefined && channel !== "vellum";
18
+ }
19
+
20
+ /** The two inputs the delegation section's gate reads. */
21
+ export interface DelegationGateInputs {
22
+ /**
23
+ * Whether the turn can actually spawn subagents, read off its resolved
24
+ * tool surface (`canSpawnSubagentsForTurn`). Absent means no.
25
+ */
26
+ canSpawnSubagents?: boolean;
27
+ channelCapabilities?: ChannelCapabilities;
28
+ }
29
+
30
+ /**
31
+ * The delegation section's gate: the turn can spawn AND a subagent's answer
32
+ * would reach whoever asked.
33
+ *
34
+ * Off unless a caller states it can spawn, and the caller that states it
35
+ * derives the answer from the turn's resolved tool surface rather than
36
+ * assuming (`canSpawnSubagentsForTurn`). Guidance about handing work to
37
+ * subagents is worth nothing to a turn that cannot spawn, and worse than
38
+ * nothing when it makes that turn defer work it has to do inline.
39
+ *
40
+ * Off on an external channel for the second reason: a subagent's terminal
41
+ * summary reaches the parent through the conversation's event sink, which an
42
+ * app client reads and a channel does not, so a delegated answer would never
43
+ * be delivered to the person who asked for it.
44
+ *
45
+ * Its own module so the conversation's prompt build and the prompt builder
46
+ * share one derivation: the conversation resolves the state once, hands it to
47
+ * the builder, and keeps it for the wire-surface recorder, so the prompt the
48
+ * provider receives and the state a fork replays are the same value.
49
+ */
50
+ export function resolveDelegateIndependentTasks(
51
+ options: DelegationGateInputs | undefined,
52
+ ): boolean {
53
+ return (
54
+ options?.canSpawnSubagents === true &&
55
+ !isExternalChannelTurn(options?.channelCapabilities)
56
+ );
57
+ }
@@ -26,6 +26,7 @@ import {
26
26
  import { stripCommentLines } from "../util/strip-comment-lines.js";
27
27
  import { cleanupBootstrapFiles } from "./bootstrap-cleanup.js";
28
28
  import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "./cache-boundary.js";
29
+ import { resolveDelegateIndependentTasks } from "./delegation-gate.js";
29
30
  import { resolveGuardianPersona, resolveUserSlug } from "./persona-resolver.js";
30
31
  import { renderWorkspaceSections } from "./sections.js";
31
32
  import { isTemplateContent } from "./template-detection.js";
@@ -440,23 +441,15 @@ export interface BuildSystemPromptOptions {
440
441
  * pass), rather than from a caller's assumption.
441
442
  */
442
443
  canSpawnSubagents?: boolean;
443
- }
444
-
445
- /**
446
- * Whether this turn arrived on an external messaging surface (Slack, Telegram,
447
- * email, a plugin channel) rather than the app itself.
448
- *
449
- * `resolveChannelCapabilities` folds every first-party client (macOS, web,
450
- * iOS, CLI, the HTTP API) onto the `vellum` channel, and a prompt built
451
- * outside a turn carries no capabilities at all, so those two cases are the
452
- * app and everything else is a channel the gateway delivers to over a reply
453
- * callback.
454
- */
455
- function isExternalChannelTurn(
456
- capabilities: ChannelCapabilities | undefined,
457
- ): boolean {
458
- const channel = capabilities?.channel;
459
- return channel !== undefined && channel !== "vellum";
444
+ /**
445
+ * The delegation section's rendered state, replacing the derivation from
446
+ * `canSpawnSubagents` and `channelCapabilities` when set. A fork wake
447
+ * replaying its source conversation's recorded surface passes the state
448
+ * the source's live turn rendered (`ConversationToolSurface`), so the
449
+ * fork's system prompt matches the source's byte for byte even though the
450
+ * wake's own scope cannot spawn; a spawn is still rejected at execution.
451
+ */
452
+ delegateIndependentTasks?: boolean;
460
453
  }
461
454
 
462
455
  /**
@@ -525,22 +518,11 @@ export function buildSystemPrompt(options?: BuildSystemPromptOptions): string {
525
518
  const ctx = {
526
519
  ...options,
527
520
  hasNoClient,
528
- // The delegation section's gate: the turn can spawn AND a subagent's
529
- // answer would reach whoever asked.
530
- //
531
- // Off unless a caller states it can spawn, and the caller that states it
532
- // derives the answer from the turn's resolved tool surface rather than
533
- // assuming (`canSpawnSubagentsForTurn`). Guidance about handing work to
534
- // subagents is worth nothing to a turn that cannot spawn, and worse than
535
- // nothing when it makes that turn defer work it has to do inline.
536
- //
537
- // Off on an external channel for the second reason: a subagent's terminal
538
- // summary reaches the parent through the conversation's event sink, which
539
- // an app client reads and a channel does not, so a delegated answer would
540
- // never be delivered to the person who asked for it.
521
+ // The delegation section's gate, or the rendered state a replaying wake
522
+ // carries from its source (see `resolveDelegateIndependentTasks`).
541
523
  delegateIndependentTasks:
542
- options?.canSpawnSubagents === true &&
543
- !isExternalChannelTurn(options?.channelCapabilities),
524
+ options?.delegateIndependentTasks ??
525
+ resolveDelegateIndependentTasks(options),
544
526
  isContainerized: getIsContainerized(),
545
527
  sendUserMessageTool: options?.sendUserMessageTool === true,
546
528
  workspaceDir: getWorkspaceDir(),
@@ -35,6 +35,7 @@ import { AtlasCloudProvider } from "../atlascloud/client.js";
35
35
  import { BasetenProvider } from "../baseten/client.js";
36
36
  import { FireworksProvider } from "../fireworks/client.js";
37
37
  import { GeminiProvider } from "../gemini/client.js";
38
+ import { JevProvider } from "../jev/client.js";
38
39
  import { MinimaxProvider } from "../minimax/client.js";
39
40
  import { PROVIDER_CATALOG } from "../model-catalog.js";
40
41
  import { OllamaProvider } from "../ollama/client.js";
@@ -205,6 +206,11 @@ const ADAPTER_FACTORIES: Record<string, AdapterFactory> = {
205
206
  streamTimeoutMs,
206
207
  ...(baseURL ? { baseURL } : {}),
207
208
  }),
209
+ jev: ({ apiKey, model, streamTimeoutMs, baseURL }) =>
210
+ new JevProvider(apiKey, model, {
211
+ streamTimeoutMs,
212
+ ...(baseURL ? { baseURL } : {}),
213
+ }),
208
214
  vellum: ({ apiKey, model, streamTimeoutMs, baseURL }) =>
209
215
  new VellumProvider(apiKey, model, {
210
216
  streamTimeoutMs,
@@ -0,0 +1,260 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+
3
+ import { ProviderError } from "../../util/errors.js";
4
+ import {
5
+ conversationToState,
6
+ DEFAULT_JEV_MODEL,
7
+ JevProvider,
8
+ parseSystemOneOverride,
9
+ validateJevApiKey,
10
+ } from "./client.js";
11
+
12
+ const originalFetch = globalThis.fetch;
13
+
14
+ afterEach(() => {
15
+ globalThis.fetch = originalFetch;
16
+ });
17
+
18
+ function jsonResponse(body: unknown, status = 200): Response {
19
+ return new Response(JSON.stringify(body), {
20
+ status,
21
+ headers: { "Content-Type": "application/json" },
22
+ });
23
+ }
24
+
25
+ function stubFetch(
26
+ impl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
27
+ ): typeof fetch {
28
+ return impl as unknown as typeof fetch;
29
+ }
30
+
31
+ describe("parseSystemOneOverride", () => {
32
+ test("reads a TypeSafe questions payload from the last user message", () => {
33
+ const parsed = parseSystemOneOverride(
34
+ JSON.stringify({
35
+ questions: {
36
+ urgency: {
37
+ type: "noul",
38
+ instructions: "Does this message express urgency?",
39
+ },
40
+ },
41
+ }),
42
+ );
43
+ expect(parsed?.questions.urgency?.type).toBe("noul");
44
+ expect(parsed?.state).toBeUndefined();
45
+ });
46
+
47
+ test("forwards an explicit state when present", () => {
48
+ const parsed = parseSystemOneOverride(
49
+ JSON.stringify({
50
+ state: "Cancel my subscription",
51
+ questions: {
52
+ intent: {
53
+ type: "choice",
54
+ instructions: "What does the user want?",
55
+ criteria: { cancel: "Cancellation", refund: "Refund" },
56
+ },
57
+ },
58
+ }),
59
+ );
60
+ expect(parsed?.state).toBe("Cancel my subscription");
61
+ expect(parsed?.questions.intent?.type).toBe("choice");
62
+ });
63
+
64
+ test("ignores ordinary chat text", () => {
65
+ expect(parseSystemOneOverride("Cancel my subscription")).toBeNull();
66
+ });
67
+ });
68
+
69
+ describe("conversationToState", () => {
70
+ test("flattens system prompt and turns into a string", () => {
71
+ const state = conversationToState(
72
+ [
73
+ {
74
+ role: "user",
75
+ content: [{ type: "text", text: "Cancel my subscription" }],
76
+ },
77
+ ],
78
+ "You are a helpful assistant.",
79
+ );
80
+ expect(state).toContain("system:\nYou are a helpful assistant.");
81
+ expect(state).toContain("user:\nCancel my subscription");
82
+ });
83
+ });
84
+
85
+ describe("validateJevApiKey", () => {
86
+ test("rejects a 401 from the models endpoint", async () => {
87
+ globalThis.fetch = stubFetch(async () =>
88
+ jsonResponse({ error: "nope" }, 401),
89
+ );
90
+ const result = await validateJevApiKey("bad-key");
91
+ expect(result).toEqual({
92
+ valid: false,
93
+ reason: "API key is invalid or expired.",
94
+ });
95
+ });
96
+
97
+ test("falls back to System One when /v1/models is unpublished", async () => {
98
+ const urls: string[] = [];
99
+ globalThis.fetch = stubFetch(async (input) => {
100
+ const url = String(input);
101
+ urls.push(url);
102
+ if (url.endsWith("/v1/models")) {
103
+ return jsonResponse({ error: "missing" }, 404);
104
+ }
105
+ return jsonResponse({
106
+ model: DEFAULT_JEV_MODEL,
107
+ answers: { probe: { type: "noul", noul: 1 } },
108
+ });
109
+ });
110
+ const result = await validateJevApiKey("sk-test");
111
+ expect(result).toEqual({ valid: true });
112
+ expect(urls.some((url) => url.endsWith("/v1/systemone"))).toBe(true);
113
+ });
114
+
115
+ test("allows the key on transient 429s", async () => {
116
+ globalThis.fetch = stubFetch(async () =>
117
+ jsonResponse({ error: "slow down" }, 429),
118
+ );
119
+ const result = await validateJevApiKey("sk-test");
120
+ expect(result).toEqual({ valid: true });
121
+ });
122
+ });
123
+
124
+ describe("JevProvider.sendMessage", () => {
125
+ test("evaluates conversation state with the default experiment noul", async () => {
126
+ let captured: { url: string; body: unknown } | undefined;
127
+ globalThis.fetch = stubFetch(async (input, init) => {
128
+ captured = {
129
+ url: String(input),
130
+ body: JSON.parse(String(init?.body ?? "{}")) as unknown,
131
+ };
132
+ return jsonResponse({
133
+ model: "jev-latest",
134
+ answers: { actionable: { type: "noul", noul: 0.91 } },
135
+ usage: { input_tokens: 12, output_tokens: 4 },
136
+ });
137
+ });
138
+
139
+ const provider = new JevProvider("sk-test", "jev-latest");
140
+ const deltas: string[] = [];
141
+ const response = await provider.sendMessage(
142
+ [
143
+ {
144
+ role: "user",
145
+ content: [{ type: "text", text: "Please cancel this charge." }],
146
+ },
147
+ ],
148
+ {
149
+ onEvent: (event) => {
150
+ if (event.type === "text_delta") {
151
+ deltas.push(event.text);
152
+ }
153
+ },
154
+ },
155
+ );
156
+
157
+ expect(captured?.url).toBe("https://api.typesafe.ai/v1/systemone");
158
+ expect(captured?.body).toMatchObject({
159
+ model: "jev-latest",
160
+ questions: {
161
+ actionable: {
162
+ type: "noul",
163
+ instructions:
164
+ "Does the latest user message ask the assistant to take a specific action?",
165
+ },
166
+ },
167
+ });
168
+ expect(String((captured?.body as { state: string }).state)).toContain(
169
+ "Please cancel this charge.",
170
+ );
171
+ expect(response.model).toBe("jev-latest");
172
+ expect(response.usage).toEqual({ inputTokens: 12, outputTokens: 4 });
173
+ expect(response.content[0]).toEqual({
174
+ type: "text",
175
+ text: JSON.stringify(
176
+ { actionable: { type: "noul", noul: 0.91 } },
177
+ null,
178
+ 2,
179
+ ),
180
+ });
181
+ expect(deltas.join("")).toBe(
182
+ response.content[0] && "text" in response.content[0]
183
+ ? response.content[0].text
184
+ : "",
185
+ );
186
+ });
187
+
188
+ test("forwards an explicit System One payload from the last user message", async () => {
189
+ let captured: unknown;
190
+ globalThis.fetch = stubFetch(async (_input, init) => {
191
+ captured = JSON.parse(String(init?.body ?? "{}")) as unknown;
192
+ return jsonResponse({
193
+ model: "jev-latest",
194
+ answers: {
195
+ department: {
196
+ type: "choice",
197
+ choice: "billing",
198
+ probabilities: { billing: 0.8, technical: 0.2 },
199
+ confidence: 0.6,
200
+ },
201
+ },
202
+ });
203
+ });
204
+
205
+ const provider = new JevProvider("sk-test", "jev-latest");
206
+ await provider.sendMessage([
207
+ {
208
+ role: "user",
209
+ content: [
210
+ {
211
+ type: "text",
212
+ text: JSON.stringify({
213
+ state: "I was charged twice.",
214
+ questions: {
215
+ department: {
216
+ type: "choice",
217
+ instructions: "Which team should handle this?",
218
+ criteria: {
219
+ billing: "Payments",
220
+ technical: "Bugs",
221
+ },
222
+ },
223
+ },
224
+ }),
225
+ },
226
+ ],
227
+ },
228
+ ]);
229
+
230
+ expect(captured).toMatchObject({
231
+ model: "jev-latest",
232
+ state: "I was charged twice.",
233
+ questions: {
234
+ department: {
235
+ type: "choice",
236
+ instructions: "Which team should handle this?",
237
+ },
238
+ },
239
+ });
240
+ });
241
+
242
+ test("throws ProviderError with invalid_credentials on 401", async () => {
243
+ globalThis.fetch = stubFetch(async () =>
244
+ jsonResponse({ error: { message: "bad key" } }, 401),
245
+ );
246
+ const provider = new JevProvider("sk-test", "jev-latest");
247
+ try {
248
+ await provider.sendMessage([
249
+ { role: "user", content: [{ type: "text", text: "hello" }] },
250
+ ]);
251
+ throw new Error("expected sendMessage to throw");
252
+ } catch (error) {
253
+ expect(error).toBeInstanceOf(ProviderError);
254
+ const providerError = error as ProviderError;
255
+ expect(providerError.statusCode).toBe(401);
256
+ expect(providerError.reason).toBe("invalid_credentials");
257
+ expect(providerError.message).toBe("bad key");
258
+ }
259
+ });
260
+ });