@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
@@ -0,0 +1,518 @@
1
+ import { isAbortReason } from "../../util/abort-reasons.js";
2
+ import { ProviderError, type ProviderErrorReason } from "../../util/errors.js";
3
+ import { getLogger } from "../../util/logger.js";
4
+ import { safeStringSlice } from "../../util/unicode.js";
5
+ import { fileBlockToProviderText } from "../file-block-text.js";
6
+ import { recordProviderRequestDiagnostics } from "../request-diagnostics.js";
7
+ import { createStreamTimeout } from "../stream-timeout.js";
8
+ import type {
9
+ ContentBlock,
10
+ Message,
11
+ Provider,
12
+ ProviderResponse,
13
+ SendMessageOptions,
14
+ } from "../types.js";
15
+
16
+ const log = getLogger("jev-client");
17
+
18
+ export const JEV_PROVIDER_ID = "jev";
19
+ export const DEFAULT_JEV_BASE_URL = "https://api.typesafe.ai";
20
+ export const DEFAULT_JEV_MODEL = "jev-latest";
21
+
22
+ const SYSTEM_ONE_PATH = "/v1/systemone";
23
+ const MODELS_PATH = "/v1/models";
24
+ const VALIDATION_TIMEOUT_MS = 10_000;
25
+ /** TypeSafe's published request budget is about 32k tokens / 150k English characters. */
26
+ const MAX_STATE_CHARS = 140_000;
27
+
28
+ export interface JevProviderOptions {
29
+ baseURL?: string;
30
+ streamTimeoutMs?: number;
31
+ }
32
+
33
+ export type ApiKeyValidationResult =
34
+ { valid: true } | { valid: false; reason: string };
35
+
36
+ export type JevJsonValue =
37
+ | string
38
+ | number
39
+ | boolean
40
+ | null
41
+ | JevJsonValue[]
42
+ | { [key: string]: JevJsonValue };
43
+
44
+ export type JevQuestion = {
45
+ type: "choice" | "score" | "noul";
46
+ instructions: string;
47
+ criteria?: JevJsonValue;
48
+ };
49
+
50
+ export type JevQuestions = Record<string, JevQuestion>;
51
+
52
+ export interface JevSystemOneRequest {
53
+ state: JevJsonValue;
54
+ model: string;
55
+ questions: JevQuestions;
56
+ }
57
+
58
+ export interface JevSystemOneResult {
59
+ model: string;
60
+ answers: Record<string, JevJsonValue>;
61
+ usage?: {
62
+ input_tokens?: number;
63
+ output_tokens?: number;
64
+ };
65
+ }
66
+
67
+ const DEFAULT_EXPERIMENT_QUESTIONS: JevQuestions = {
68
+ actionable: {
69
+ type: "noul",
70
+ instructions:
71
+ "Does the latest user message ask the assistant to take a specific action?",
72
+ },
73
+ };
74
+
75
+ function trimTrailingSlash(url: string): string {
76
+ return url.replace(/\/+$/, "");
77
+ }
78
+
79
+ function resolveBaseUrl(baseURL?: string): string {
80
+ const trimmed = baseURL?.trim();
81
+ return trimTrailingSlash(
82
+ trimmed && trimmed.length > 0 ? trimmed : DEFAULT_JEV_BASE_URL,
83
+ );
84
+ }
85
+
86
+ function isRecord(value: unknown): value is Record<string, unknown> {
87
+ return typeof value === "object" && value !== null && !Array.isArray(value);
88
+ }
89
+
90
+ function isJevQuestion(value: unknown): value is JevQuestion {
91
+ if (!isRecord(value)) {
92
+ return false;
93
+ }
94
+ const type = value.type;
95
+ if (type !== "choice" && type !== "score" && type !== "noul") {
96
+ return false;
97
+ }
98
+ if (
99
+ typeof value.instructions !== "string" ||
100
+ value.instructions.trim().length === 0
101
+ ) {
102
+ return false;
103
+ }
104
+ return true;
105
+ }
106
+
107
+ function isJevQuestions(value: unknown): value is JevQuestions {
108
+ if (!isRecord(value)) {
109
+ return false;
110
+ }
111
+ const entries = Object.entries(value);
112
+ if (entries.length === 0) {
113
+ return false;
114
+ }
115
+ return entries.every(([, question]) => isJevQuestion(question));
116
+ }
117
+
118
+ /**
119
+ * If the last user message is a TypeSafe System One payload, use it as the
120
+ * evaluation request. `state` is optional; callers fall back to the
121
+ * conversation when it is omitted.
122
+ */
123
+ export function parseSystemOneOverride(
124
+ text: string,
125
+ ): { state?: JevJsonValue; questions: JevQuestions } | null {
126
+ const trimmed = text.trim();
127
+ if (!trimmed.startsWith("{")) {
128
+ return null;
129
+ }
130
+ let parsed: unknown;
131
+ try {
132
+ parsed = JSON.parse(trimmed) as unknown;
133
+ } catch {
134
+ return null;
135
+ }
136
+ if (!isRecord(parsed) || !isJevQuestions(parsed.questions)) {
137
+ return null;
138
+ }
139
+ return {
140
+ questions: parsed.questions,
141
+ ...(parsed.state !== undefined
142
+ ? { state: parsed.state as JevJsonValue }
143
+ : {}),
144
+ };
145
+ }
146
+
147
+ function contentBlockToText(block: ContentBlock): string {
148
+ switch (block.type) {
149
+ case "text":
150
+ return block.text;
151
+ case "file":
152
+ return fileBlockToProviderText(block);
153
+ case "image":
154
+ return "[image]";
155
+ case "tool_use":
156
+ return `[tool_use ${block.name} ${JSON.stringify(block.input)}]`;
157
+ case "tool_result":
158
+ return `[tool_result ${block.tool_use_id}${
159
+ block.content ? ` ${block.content}` : ""
160
+ }]`;
161
+ default:
162
+ return "";
163
+ }
164
+ }
165
+
166
+ function messageToText(message: Message): string {
167
+ return message.content
168
+ .map((block) => contentBlockToText(block))
169
+ .filter((part) => part.length > 0)
170
+ .join("\n");
171
+ }
172
+
173
+ export function conversationToState(
174
+ messages: Message[],
175
+ systemPrompt?: string,
176
+ ): string {
177
+ const parts: string[] = [];
178
+ if (systemPrompt && systemPrompt.trim().length > 0) {
179
+ parts.push(`system:\n${systemPrompt}`);
180
+ }
181
+ for (const message of messages) {
182
+ const text = messageToText(message);
183
+ if (text.length === 0) {
184
+ continue;
185
+ }
186
+ parts.push(`${message.role}:\n${text}`);
187
+ }
188
+ return safeStringSlice(parts.join("\n\n"), 0, MAX_STATE_CHARS);
189
+ }
190
+
191
+ function lastUserText(messages: Message[]): string {
192
+ for (let i = messages.length - 1; i >= 0; i--) {
193
+ const message = messages[i];
194
+ if (message?.role !== "user") {
195
+ continue;
196
+ }
197
+ const text = messageToText(message).trim();
198
+ if (text.length > 0) {
199
+ return text;
200
+ }
201
+ }
202
+ return "";
203
+ }
204
+
205
+ function classifyHttpStatus(status: number): ProviderErrorReason {
206
+ if (status === 401 || status === 403) {
207
+ return "invalid_credentials";
208
+ }
209
+ if (status === 404) {
210
+ return "model_not_found";
211
+ }
212
+ if (status === 429) {
213
+ return "rate_limited";
214
+ }
215
+ if (status === 529) {
216
+ return "overloaded";
217
+ }
218
+ if (status >= 500) {
219
+ return "server_error";
220
+ }
221
+ if (status >= 400) {
222
+ return "bad_request";
223
+ }
224
+ return "unknown";
225
+ }
226
+
227
+ function extractErrorMessage(bodyText: string, status: number): string {
228
+ const trimmed = bodyText.trim();
229
+ if (trimmed.length > 0) {
230
+ try {
231
+ const parsed = JSON.parse(trimmed) as {
232
+ error?: unknown;
233
+ message?: unknown;
234
+ detail?: unknown;
235
+ };
236
+ const nested =
237
+ parsed.error && isRecord(parsed.error)
238
+ ? parsed.error.message
239
+ : parsed.error;
240
+ const candidate = nested ?? parsed.message ?? parsed.detail;
241
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
242
+ return candidate.trim();
243
+ }
244
+ } catch {
245
+ // Fall through to the raw body.
246
+ }
247
+ return safeStringSlice(trimmed, 0, 500);
248
+ }
249
+ return `TypeSafe API error (${status})`;
250
+ }
251
+
252
+ async function readResponseText(response: Response): Promise<string> {
253
+ try {
254
+ return await response.text();
255
+ } catch {
256
+ return "";
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Validate a TypeSafe API key with a tiny System One request. Definitive
262
+ * auth failures (401/403) reject the key; transient errors allow storage.
263
+ */
264
+ export async function validateJevApiKey(
265
+ apiKey: string,
266
+ options: { baseURL?: string } = {},
267
+ ): Promise<ApiKeyValidationResult> {
268
+ const baseURL = resolveBaseUrl(options.baseURL);
269
+ const controller = new AbortController();
270
+ const timeout = setTimeout(() => {
271
+ controller.abort(new Error("TypeSafe key validation timed out"));
272
+ }, VALIDATION_TIMEOUT_MS);
273
+ try {
274
+ const response = await fetch(`${baseURL}${MODELS_PATH}`, {
275
+ method: "GET",
276
+ headers: {
277
+ Authorization: `Bearer ${apiKey}`,
278
+ Accept: "application/json",
279
+ },
280
+ signal: controller.signal,
281
+ });
282
+ if (response.status === 401) {
283
+ return { valid: false, reason: "API key is invalid or expired." };
284
+ }
285
+ if (response.status === 403) {
286
+ const body = await readResponseText(response);
287
+ return {
288
+ valid: false,
289
+ reason: extractErrorMessage(body, response.status),
290
+ };
291
+ }
292
+ if (response.status === 404) {
293
+ return await validateJevApiKeyViaSystemOne(apiKey, baseURL);
294
+ }
295
+ if (response.ok) {
296
+ return { valid: true };
297
+ }
298
+ log.warn(
299
+ { status: response.status },
300
+ "TypeSafe API returned a transient error during key validation, allowing key",
301
+ );
302
+ return { valid: true };
303
+ } catch (error) {
304
+ log.warn(
305
+ { error: error instanceof Error ? error.message : String(error) },
306
+ "Network error during TypeSafe key validation, allowing key",
307
+ );
308
+ return { valid: true };
309
+ } finally {
310
+ clearTimeout(timeout);
311
+ }
312
+ }
313
+
314
+ async function validateJevApiKeyViaSystemOne(
315
+ apiKey: string,
316
+ baseURL: string,
317
+ ): Promise<ApiKeyValidationResult> {
318
+ try {
319
+ const response = await fetch(`${baseURL}${SYSTEM_ONE_PATH}`, {
320
+ method: "POST",
321
+ headers: {
322
+ Authorization: `Bearer ${apiKey}`,
323
+ Accept: "application/json",
324
+ "Content-Type": "application/json",
325
+ },
326
+ body: JSON.stringify({
327
+ state: "ok",
328
+ model: DEFAULT_JEV_MODEL,
329
+ questions: {
330
+ probe: {
331
+ type: "noul",
332
+ instructions: "Is this a non-empty string?",
333
+ },
334
+ },
335
+ } satisfies JevSystemOneRequest),
336
+ });
337
+ if (response.status === 401) {
338
+ return { valid: false, reason: "API key is invalid or expired." };
339
+ }
340
+ if (response.status === 403) {
341
+ const body = await readResponseText(response);
342
+ return {
343
+ valid: false,
344
+ reason: extractErrorMessage(body, response.status),
345
+ };
346
+ }
347
+ if (!response.ok && response.status < 500 && response.status !== 429) {
348
+ const body = await readResponseText(response);
349
+ return {
350
+ valid: false,
351
+ reason: extractErrorMessage(body, response.status),
352
+ };
353
+ }
354
+ return { valid: true };
355
+ } catch (error) {
356
+ log.warn(
357
+ { error: error instanceof Error ? error.message : String(error) },
358
+ "Network error during TypeSafe System One key validation, allowing key",
359
+ );
360
+ return { valid: true };
361
+ }
362
+ }
363
+
364
+ export class JevProvider implements Provider {
365
+ public readonly name = JEV_PROVIDER_ID;
366
+ public readonly defaultModel: string;
367
+ private readonly apiKey: string;
368
+ private readonly baseURL: string;
369
+ private readonly streamTimeoutMs: number;
370
+
371
+ constructor(apiKey: string, model: string, options: JevProviderOptions = {}) {
372
+ this.apiKey = apiKey;
373
+ this.defaultModel = model.trim() || DEFAULT_JEV_MODEL;
374
+ this.baseURL = resolveBaseUrl(options.baseURL);
375
+ this.streamTimeoutMs = options.streamTimeoutMs ?? 1_800_000;
376
+ }
377
+
378
+ /**
379
+ * Native TypeSafe System One call. `sendMessage` is a thin adapter over
380
+ * this so existing profiles and probes can reach Jev.
381
+ */
382
+ async systemOne(
383
+ request: JevSystemOneRequest,
384
+ options: {
385
+ signal?: AbortSignal;
386
+ headers?: Record<string, string>;
387
+ } = {},
388
+ ): Promise<JevSystemOneResult> {
389
+ const url = `${this.baseURL}${SYSTEM_ONE_PATH}`;
390
+ recordProviderRequestDiagnostics({ model_id: request.model });
391
+ const { signal, cleanup } = createStreamTimeout(
392
+ this.streamTimeoutMs,
393
+ options.signal,
394
+ );
395
+ try {
396
+ const response = await fetch(url, {
397
+ method: "POST",
398
+ headers: {
399
+ Authorization: `Bearer ${this.apiKey}`,
400
+ Accept: "application/json",
401
+ "Content-Type": "application/json",
402
+ ...(options.headers ?? {}),
403
+ },
404
+ body: JSON.stringify(request),
405
+ signal,
406
+ });
407
+ const bodyText = await readResponseText(response);
408
+ if (!response.ok) {
409
+ const reason = classifyHttpStatus(response.status);
410
+ throw new ProviderError(
411
+ extractErrorMessage(bodyText, response.status),
412
+ JEV_PROVIDER_ID,
413
+ response.status,
414
+ { reason, rawBody: bodyText },
415
+ );
416
+ }
417
+ let parsed: unknown;
418
+ try {
419
+ parsed = JSON.parse(bodyText) as unknown;
420
+ } catch {
421
+ throw new ProviderError(
422
+ "TypeSafe returned a non-JSON response.",
423
+ JEV_PROVIDER_ID,
424
+ response.status,
425
+ { reason: "bad_request", rawBody: bodyText },
426
+ );
427
+ }
428
+ if (!isRecord(parsed) || !isRecord(parsed.answers)) {
429
+ throw new ProviderError(
430
+ "TypeSafe returned a response without answers.",
431
+ JEV_PROVIDER_ID,
432
+ response.status,
433
+ { reason: "bad_request", rawBody: bodyText },
434
+ );
435
+ }
436
+ const usage = isRecord(parsed.usage) ? parsed.usage : undefined;
437
+ return {
438
+ model:
439
+ typeof parsed.model === "string" && parsed.model.length > 0
440
+ ? parsed.model
441
+ : request.model,
442
+ answers: parsed.answers as Record<string, JevJsonValue>,
443
+ ...(usage
444
+ ? {
445
+ usage: {
446
+ input_tokens:
447
+ typeof usage.input_tokens === "number"
448
+ ? usage.input_tokens
449
+ : undefined,
450
+ output_tokens:
451
+ typeof usage.output_tokens === "number"
452
+ ? usage.output_tokens
453
+ : undefined,
454
+ },
455
+ }
456
+ : {}),
457
+ };
458
+ } catch (error) {
459
+ if (error instanceof ProviderError) {
460
+ throw error;
461
+ }
462
+ const abortReason =
463
+ options.signal?.aborted && isAbortReason(options.signal.reason)
464
+ ? options.signal.reason
465
+ : undefined;
466
+ throw new ProviderError(
467
+ error instanceof Error ? error.message : String(error),
468
+ JEV_PROVIDER_ID,
469
+ undefined,
470
+ {
471
+ cause: error,
472
+ abortReason,
473
+ reason: abortReason ? undefined : "network_error",
474
+ },
475
+ );
476
+ } finally {
477
+ cleanup();
478
+ }
479
+ }
480
+
481
+ async sendMessage(
482
+ messages: Message[],
483
+ options?: SendMessageOptions,
484
+ ): Promise<ProviderResponse> {
485
+ const { systemPrompt, config, onEvent, signal } = options ?? {};
486
+ const configObj = config as Record<string, unknown> | undefined;
487
+ const modelOverride = configObj?.model as string | undefined;
488
+ const usageAttributionHeaders = configObj?.usageAttributionHeaders as
489
+ Record<string, string> | undefined;
490
+ const activeModel = modelOverride?.trim() || this.defaultModel;
491
+ const override = parseSystemOneOverride(lastUserText(messages));
492
+ const request: JevSystemOneRequest = {
493
+ model: activeModel,
494
+ state: override?.state ?? conversationToState(messages, systemPrompt),
495
+ questions: override?.questions ?? DEFAULT_EXPERIMENT_QUESTIONS,
496
+ };
497
+
498
+ const result = await this.systemOne(request, {
499
+ signal,
500
+ headers: usageAttributionHeaders,
501
+ });
502
+ const text = JSON.stringify(result.answers, null, 2);
503
+ onEvent?.({ type: "text_delta", text });
504
+
505
+ return {
506
+ content: [{ type: "text", text }],
507
+ model: result.model,
508
+ usage: {
509
+ inputTokens: result.usage?.input_tokens ?? 0,
510
+ outputTokens: result.usage?.output_tokens ?? 0,
511
+ },
512
+ stopReason: "end_turn",
513
+ resolvedEndpoint: `${this.baseURL}${SYSTEM_ONE_PATH}`,
514
+ rawRequest: request,
515
+ rawResponse: result,
516
+ };
517
+ }
518
+ }
@@ -1,9 +1,7 @@
1
1
  import { PLATFORM_PROVIDER_META } from "./platform-proxy/constants.js";
2
2
 
3
3
  export type LongContextMode =
4
- | "native-model"
5
- | "provider-request-option"
6
- | "unsupported";
4
+ "native-model" | "provider-request-option" | "unsupported";
7
5
 
8
6
  export interface CatalogModelPricingTier {
9
7
  /**
@@ -75,6 +73,13 @@ export interface CatalogModel {
75
73
  */
76
74
  supportsAudioInput?: boolean;
77
75
  supportsToolUse?: boolean;
76
+ /**
77
+ * Whether the model produces free-form chat text. Omit (or true) for
78
+ * ordinary chat models. False for structured-decision models that return
79
+ * answers rather than generated text; those stay out of conversation
80
+ * pickers and cannot be the conversation model.
81
+ */
82
+ supportsText?: boolean;
78
83
  supportsEffort?: boolean;
79
84
  /**
80
85
  * Whether this provider/model serving surface accepts a forced OpenAI
@@ -2526,6 +2531,38 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
2526
2531
  apiKeyUrl: "https://poolside.ai",
2527
2532
  apiKeyPlaceholder: "Your Poolside API key",
2528
2533
  },
2534
+ {
2535
+ id: "jev",
2536
+ displayName: "Jev",
2537
+ subtitle:
2538
+ "TypeSafe System One decision model. Returns structured answers, not generated text. Requires a TypeSafe API key.",
2539
+ setupMode: "api-key",
2540
+ setupHint: "Enter your TypeSafe API key to enable Jev.",
2541
+ envVar: "TYPESAFE_API_KEY",
2542
+ credentialsGuide: {
2543
+ description: "Sign in to TypeSafe and create an API key.",
2544
+ url: "https://typesafe.ai",
2545
+ linkLabel: "Open TypeSafe",
2546
+ },
2547
+ models: [
2548
+ {
2549
+ id: "jev-latest",
2550
+ displayName: "Jev",
2551
+ // TypeSafe's published request budget is about 32,000 tokens.
2552
+ contextWindowTokens: 32000,
2553
+ maxOutputTokens: 4096,
2554
+ supportsThinking: false,
2555
+ supportsCaching: false,
2556
+ supportsVision: false,
2557
+ supportsToolUse: false,
2558
+ supportsText: false,
2559
+ pricing: { inputPer1mTokens: 0.042, outputPer1mTokens: 0 },
2560
+ },
2561
+ ],
2562
+ defaultModel: "jev-latest",
2563
+ apiKeyUrl: "https://typesafe.ai",
2564
+ apiKeyPlaceholder: "Your TypeSafe API key",
2565
+ },
2529
2566
  {
2530
2567
  id: "vellum",
2531
2568
  displayName: "Vellum",
@@ -2564,6 +2601,24 @@ export const PROVIDER_CATALOG: ProviderCatalogEntry[] =
2564
2601
  supportsPlatformAuth: PLATFORM_PROVIDER_META[entry.id]?.managed === true,
2565
2602
  }));
2566
2603
 
2604
+ /**
2605
+ * Whether a catalog model produces free-form chat text. Unlisted providers
2606
+ * and model ids default to true so custom endpoints and unknown snapshots
2607
+ * stay usable as conversation models.
2608
+ */
2609
+ export function catalogModelSupportsText(
2610
+ provider: string | null | undefined,
2611
+ modelId: string | null | undefined,
2612
+ ): boolean {
2613
+ if (typeof provider !== "string" || typeof modelId !== "string") {
2614
+ return true;
2615
+ }
2616
+ const model = PROVIDER_CATALOG.find((p) => p.id === provider)?.models.find(
2617
+ (m) => m.id === modelId,
2618
+ );
2619
+ return model?.supportsText !== false;
2620
+ }
2621
+
2567
2622
  /** Check if a model ID is in the catalog for a given provider. */
2568
2623
  export function isModelInCatalog(provider: string, modelId: string): boolean {
2569
2624
  const entry = PROVIDER_CATALOG.find((p) => p.id === provider);
@@ -57,7 +57,8 @@ function contentBlocks(msg: ServerToolPairingMessage): ReadonlyArray<unknown> {
57
57
  * `tool_use`, no result emitted) and executes it on the next request, placing
58
58
  * the web_search_tool_result at the head of the next assistant message. Both
59
59
  * the deferred tail and the resulting split pair must be sent back verbatim
60
- * for the provider to pair and execute them.
60
+ * for the provider to pair and execute them, and the user message answering
61
+ * a deferred tail must hold tool_result blocks alone.
61
62
  */
62
63
  export interface ServerToolPairing {
63
64
  /**
@@ -67,8 +68,12 @@ export interface ServerToolPairing {
67
68
  resolvedPairIds: Set<string>;
68
69
  /**
69
70
  * Unanswered use ids in the final assistant message of an active tool-loop
70
- * continuation (only user messages after it, carrying tool_result blocks or
71
- * nothing at all). The provider executes these on this request.
71
+ * continuation: nothing follows it, or only user messages made solely of
72
+ * tool_result blocks. The provider executes these on this request. Any
73
+ * other trailing block (guidance text after the results, a queued user
74
+ * message) closes the assistant turn on the provider side, which then
75
+ * rejects the unanswered use as unpaired, so such a use is an orphan to
76
+ * repair rather than a deferral to preserve.
72
77
  */
73
78
  deferredUseIds: Set<string>;
74
79
  /** Ids whose use/result pair spans two messages. */
@@ -118,10 +123,14 @@ export function analyzeServerToolPairing(
118
123
  if (lastAssistantIndex >= 0) {
119
124
  const trailing = messages.slice(lastAssistantIndex + 1);
120
125
  const trailingAllUser = trailing.every((m) => m.role === "user");
121
- const trailingHasToolResult = trailing.some((m) =>
122
- contentBlocks(m).some(isToolResultShapedBlock),
123
- );
124
- if (trailingAllUser && (trailing.length === 0 || trailingHasToolResult)) {
126
+ const trailingBlocks = trailing.flatMap((m) => [...contentBlocks(m)]);
127
+ const trailingIsToolResultsOnly =
128
+ trailingBlocks.length > 0 &&
129
+ trailingBlocks.every(isToolResultShapedBlock);
130
+ if (
131
+ trailingAllUser &&
132
+ (trailing.length === 0 || trailingIsToolResultsOnly)
133
+ ) {
125
134
  for (const block of contentBlocks(messages[lastAssistantIndex])) {
126
135
  if (isServerToolUseBlock(block) && !resolvedPairIds.has(block.id)) {
127
136
  deferredUseIds.add(block.id);
@@ -301,6 +301,6 @@ The provider-level rate limiter (`providers/ratelimit.ts`) also logs warnings (m
301
301
 
302
302
  ## HTTP-Only Transport
303
303
 
304
- HTTP is the sole transport for client-daemon communication. The runtime HTTP server (`assistant/src/runtime/http-server.ts`) is the canonical API surface. Clients connect via HTTP for request/response operations and SSE (`GET /v1/events`) for streaming server-to-client events.
304
+ HTTP is the sole transport for browser and desktop clients; the CLI and the gateway reach the daemon over the IPC socket (see `assistant/AGENTS.md`). The runtime HTTP server (`assistant/src/runtime/http-server.ts`) is the canonical API surface. Clients connect via HTTP for request/response operations and SSE (`GET /v1/events`) for streaming server-to-client events.
305
305
 
306
- When writing skills that need to call daemon configuration endpoints, use `curl` with the runtime HTTP API (JWT-authenticated via `Authorization: Bearer <jwt>`). The assistant already knows how to use `curl`.
306
+ A skill never calls the runtime HTTP API directly: every request from a skill targets the gateway, and `gateway-only-guard.test.ts` fails CI on a runtime-port URL in a skill. For configuration reads, use the CLI (`assistant config get`); for control-plane writes with no CLI verb, use `$INTERNAL_GATEWAY_BASE_URL`. See "Gateway-Only API Consumption" in `gateway/AGENTS.md` for the rule and its exceptions.