@vellumai/assistant 0.8.2 → 0.8.3

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 (231) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -1
  3. package/docker-init-apt-root.sh +79 -6
  4. package/openapi.yaml +336 -21
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  7. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  8. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  9. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  10. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  11. package/src/__tests__/context-token-estimator.test.ts +30 -65
  12. package/src/__tests__/conversation-agent-loop.test.ts +57 -1
  13. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
  15. package/src/__tests__/date-context.test.ts +45 -0
  16. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  17. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  18. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  19. package/src/__tests__/heartbeat-service.test.ts +24 -164
  20. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  21. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  22. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  23. package/src/__tests__/injector-background-turn.test.ts +153 -0
  24. package/src/__tests__/injector-chain.test.ts +5 -0
  25. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  26. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  27. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  28. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  29. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  30. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  31. package/src/__tests__/llm-resolver.test.ts +255 -2
  32. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  33. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  34. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  35. package/src/__tests__/notification-deep-link.test.ts +15 -0
  36. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  37. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  38. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  39. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  40. package/src/__tests__/openai-provider.test.ts +218 -3
  41. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  42. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  43. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  44. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  45. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  46. package/src/__tests__/plugin-types.test.ts +2 -2
  47. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  48. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  49. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  50. package/src/__tests__/system-prompt.test.ts +6 -73
  51. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  52. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  53. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  54. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  55. package/src/a2a/__tests__/task-store.test.ts +246 -0
  56. package/src/a2a/agent-card.ts +58 -0
  57. package/src/a2a/feature-gate.ts +8 -0
  58. package/src/a2a/protocol-constants.ts +21 -0
  59. package/src/a2a/protocol-errors.ts +50 -0
  60. package/src/a2a/protocol-types.ts +162 -0
  61. package/src/a2a/task-store.ts +168 -0
  62. package/src/agent/loop.ts +167 -18
  63. package/src/channels/config.ts +9 -0
  64. package/src/channels/types.ts +14 -0
  65. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  66. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  67. package/src/cli/commands/notifications.ts +65 -35
  68. package/src/cli/commands/plugins.ts +67 -0
  69. package/src/cli/commands/schedules.ts +297 -5
  70. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  71. package/src/cli/lib/install-from-github.ts +8 -9
  72. package/src/cli/lib/search-plugins.ts +163 -0
  73. package/src/cli/program.ts +14 -0
  74. package/src/config/assistant-feature-flags.ts +24 -54
  75. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  76. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  77. package/src/config/call-site-defaults.ts +105 -0
  78. package/src/config/feature-flag-registry.json +21 -29
  79. package/src/config/llm-resolver.ts +52 -1
  80. package/src/config/schema.ts +2 -0
  81. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  82. package/src/config/schemas/channels.ts +9 -0
  83. package/src/config/schemas/conversations.ts +10 -0
  84. package/src/config/schemas/heartbeat.ts +14 -0
  85. package/src/config/schemas/llm.ts +1 -3
  86. package/src/config/schemas/memory-retrospective.ts +1 -1
  87. package/src/config/schemas/memory-v2.ts +4 -4
  88. package/src/config/schemas/memory.ts +3 -1
  89. package/src/config/seed-inference-profiles.ts +99 -29
  90. package/src/context/compactor.ts +72 -12
  91. package/src/context/token-estimator.ts +32 -34
  92. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  93. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  94. package/src/daemon/conversation-agent-loop.ts +29 -2
  95. package/src/daemon/conversation-runtime-assembly.ts +9 -0
  96. package/src/daemon/conversation.ts +0 -7
  97. package/src/daemon/date-context.ts +40 -0
  98. package/src/daemon/guardian-action-generators.ts +1 -125
  99. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  100. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  101. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  102. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  103. package/src/daemon/handlers/config-a2a.ts +289 -0
  104. package/src/daemon/handlers/conversations.ts +1 -0
  105. package/src/daemon/host-app-control-proxy.ts +69 -18
  106. package/src/daemon/host-proxy-preactivation.ts +85 -18
  107. package/src/daemon/lifecycle.ts +49 -61
  108. package/src/daemon/memory-v2-startup.ts +49 -13
  109. package/src/daemon/message-types/notifications.ts +21 -0
  110. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  111. package/src/daemon/pkb-reminder-builder.ts +4 -19
  112. package/src/daemon/process-message.ts +3 -0
  113. package/src/daemon/skill-memory-refresh.ts +5 -1
  114. package/src/daemon/wake-target-adapter.ts +2 -0
  115. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  116. package/src/export/transcript-formatter.ts +54 -20
  117. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
  118. package/src/heartbeat/heartbeat-service.ts +34 -191
  119. package/src/home/__tests__/feed-types.test.ts +40 -0
  120. package/src/home/feed-types.ts +14 -2
  121. package/src/ipc/cli-client.ts +147 -45
  122. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  123. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  124. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  125. package/src/memory/conversation-queries.ts +87 -1
  126. package/src/memory/conversation-title-service.ts +26 -4
  127. package/src/memory/db-init.ts +6 -0
  128. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  129. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  130. package/src/memory/graph/tools.ts +6 -37
  131. package/src/memory/invite-store.ts +53 -0
  132. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  133. package/src/memory/llm-request-log-store.ts +92 -1
  134. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  135. package/src/memory/memory-retrospective-job.ts +33 -6
  136. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  137. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  138. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  139. package/src/memory/migrations/index.ts +3 -0
  140. package/src/memory/migrations/registry.ts +8 -0
  141. package/src/memory/schema/a2a.ts +15 -0
  142. package/src/memory/schema/index.ts +1 -0
  143. package/src/memory/schema/inference.ts +2 -0
  144. package/src/memory/schema/infrastructure.ts +1 -0
  145. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  146. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  147. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  148. package/src/memory/v2/__tests__/injection.test.ts +190 -3
  149. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  150. package/src/memory/v2/activation-store.ts +14 -16
  151. package/src/memory/v2/cli-command-content.ts +19 -0
  152. package/src/memory/v2/cli-command-store.ts +304 -0
  153. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  154. package/src/memory/v2/injection.ts +49 -20
  155. package/src/memory/v2/page-index.ts +38 -13
  156. package/src/memory/v2/static-context.ts +4 -4
  157. package/src/memory/v2/types.ts +23 -0
  158. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  159. package/src/messaging/providers/a2a/deliver.ts +156 -0
  160. package/src/messaging/providers/gmail/client.ts +9 -2
  161. package/src/messaging/providers/index.ts +11 -2
  162. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  163. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  164. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  165. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  166. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  167. package/src/notifications/adapters/macos.ts +12 -2
  168. package/src/notifications/broadcaster.ts +29 -4
  169. package/src/notifications/copy-composer.ts +17 -64
  170. package/src/notifications/decision-engine.ts +111 -44
  171. package/src/notifications/deterministic-checks.ts +96 -0
  172. package/src/notifications/emit-signal.ts +1 -0
  173. package/src/notifications/home-feed-side-effect.ts +85 -6
  174. package/src/notifications/signal.ts +0 -4
  175. package/src/notifications/types.ts +8 -0
  176. package/src/oauth/platform-connection.test.ts +43 -3
  177. package/src/oauth/platform-connection.ts +13 -4
  178. package/src/plugins/defaults/injectors.ts +38 -19
  179. package/src/plugins/external-plugin-loader.ts +82 -10
  180. package/src/plugins/types.ts +16 -7
  181. package/src/prompts/__tests__/system-prompt.test.ts +6 -51
  182. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  183. package/src/prompts/system-prompt.ts +0 -8
  184. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  185. package/src/prompts/templates/system-sections.ts +0 -9
  186. package/src/providers/__tests__/inference.test.ts +2 -0
  187. package/src/providers/call-site-routing.ts +24 -6
  188. package/src/providers/connection-resolution.ts +63 -13
  189. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  190. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  191. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  192. package/src/providers/inference/adapter-factory.ts +9 -20
  193. package/src/providers/inference/auth.ts +12 -0
  194. package/src/providers/inference/backfill.ts +14 -1
  195. package/src/providers/inference/connections.ts +85 -5
  196. package/src/providers/inference/resolve-auth.ts +2 -0
  197. package/src/providers/model-catalog.ts +199 -244
  198. package/src/providers/model-intents.ts +3 -3
  199. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  200. package/src/providers/openai/chat-completions-provider.ts +159 -6
  201. package/src/providers/openrouter/client.ts +42 -4
  202. package/src/providers/platform-proxy/constants.ts +3 -4
  203. package/src/providers/provider-catalog-visibility.ts +3 -1
  204. package/src/providers/provider-send-message.ts +27 -12
  205. package/src/providers/registry.ts +30 -1
  206. package/src/runtime/agent-wake.ts +61 -1
  207. package/src/runtime/auth/route-policy.ts +13 -0
  208. package/src/runtime/http-server.ts +7 -16
  209. package/src/runtime/http-types.ts +0 -47
  210. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  211. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
  212. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  213. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  214. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  215. package/src/runtime/routes/consolidation-routes.ts +100 -0
  216. package/src/runtime/routes/conversation-query-routes.ts +70 -11
  217. package/src/runtime/routes/conversation-routes.ts +7 -0
  218. package/src/runtime/routes/index.ts +2 -0
  219. package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
  220. package/src/runtime/routes/integrations/a2a.ts +235 -0
  221. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  222. package/src/runtime/routes/subagents-routes.ts +41 -0
  223. package/src/subagent/manager.ts +2 -0
  224. package/src/tools/memory/register.ts +1 -9
  225. package/src/tools/registry.ts +2 -2
  226. package/src/tools/types.ts +37 -2
  227. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  228. package/src/workspace/migrations/registry.ts +2 -0
  229. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  230. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  231. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -17,11 +17,16 @@ import { z } from "zod";
17
17
 
18
18
  import { getConfig } from "../../config/loader.js";
19
19
  import { getMemoryCheckpoint } from "../../memory/checkpoints.js";
20
+ import {
21
+ getMessageRoleStatsByConversation,
22
+ listConversationsBySource,
23
+ } from "../../memory/conversation-queries.js";
20
24
  import {
21
25
  enqueueMemoryJob,
22
26
  hasActiveJobOfType,
23
27
  } from "../../memory/jobs-store.js";
24
28
  import { GRAPH_MAINTENANCE_CHECKPOINTS } from "../../memory/jobs-worker.js";
29
+ import { MEMORY_V2_CONSOLIDATION_SOURCE } from "../../memory/v2/constants.js";
25
30
  import { BadRequestError } from "./errors.js";
26
31
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
27
32
 
@@ -111,4 +116,99 @@ export const ROUTES: RouteDefinition[] = [
111
116
  return { success: true, ran: true, jobId };
112
117
  },
113
118
  },
119
+ {
120
+ operationId: "listConsolidationRuns",
121
+ endpoint: "consolidation/runs",
122
+ method: "GET",
123
+ policyKey: "consolidation",
124
+ summary: "List consolidation runs",
125
+ description:
126
+ "Return recent memory v2 consolidation conversations as run records. " +
127
+ "Each consolidation dispatch creates exactly one background conversation " +
128
+ "tagged with `source = memory_v2_consolidation`; that conversation IS " +
129
+ "the run. Synthetic fields: `id` mirrors `conversationId` (no separate " +
130
+ "run row exists), `scheduledFor` and `startedAt` both equal " +
131
+ "`conversation.createdAt` (no separate schedule timestamp), " +
132
+ "`finishedAt` is the `createdAt` of the latest assistant message in " +
133
+ "the conversation (NOT `conversation.lastMessageAt`, which the kickoff " +
134
+ "user prompt bumps before the agent runs). `status` is `'ok'` when " +
135
+ "the conversation has at least one assistant message — i.e. positive " +
136
+ "evidence the agent emitted output — otherwise `'running'`. This is a " +
137
+ "weaker signal than heartbeat's `'ok'`: without a dedicated runs " +
138
+ "table we cannot distinguish 'ran cleanly' from 'crashed after " +
139
+ "emitting at least one assistant message'. `skipReason` and `error` " +
140
+ "are always null — skipped runs (lock held, disabled, empty buffer) " +
141
+ "never create a conversation, and run failure detail is not stored " +
142
+ "on the conversation row. Shape mirrors `heartbeat/runs` so the " +
143
+ "schedules settings UI can reuse its run-row component.",
144
+ tags: ["consolidation"],
145
+ queryParams: [
146
+ {
147
+ name: "limit",
148
+ schema: { type: "integer" },
149
+ description: "Max runs to return (default 20, max 100)",
150
+ },
151
+ ],
152
+ responseBody: z.object({
153
+ runs: z
154
+ .array(
155
+ z.object({
156
+ id: z.string(),
157
+ scheduledFor: z.number(),
158
+ startedAt: z.number().nullable(),
159
+ finishedAt: z.number().nullable(),
160
+ durationMs: z.number().nullable(),
161
+ status: z.enum(["ok", "running"]),
162
+ skipReason: z.string().nullable(),
163
+ error: z.string().nullable(),
164
+ conversationId: z.string().nullable(),
165
+ createdAt: z.number(),
166
+ }),
167
+ )
168
+ .describe("Consolidation run records"),
169
+ }),
170
+ handler: async ({ queryParams }: RouteHandlerArgs) => {
171
+ const params = queryParams ?? {};
172
+ const rawLimit = Number(params.limit ?? 20);
173
+ const limit = Number.isFinite(rawLimit)
174
+ ? Math.min(Math.max(Math.floor(rawLimit), 1), 100)
175
+ : 20;
176
+ const rows = listConversationsBySource(
177
+ MEMORY_V2_CONSOLIDATION_SOURCE,
178
+ limit,
179
+ );
180
+ // Aggregate assistant-message stats in one batched query: presence of
181
+ // an assistant message is the strongest "agent emitted output" signal
182
+ // available without a dedicated consolidation runs table. The kickoff
183
+ // user prompt is persisted via `addMessage` before the agent run,
184
+ // which bumps `conversations.lastMessageAt` — so that field cannot
185
+ // be used to infer completion.
186
+ const assistantStats = getMessageRoleStatsByConversation(
187
+ rows.map((r) => r.id),
188
+ "assistant",
189
+ );
190
+ return {
191
+ runs: rows.map((c) => {
192
+ const stat = assistantStats.get(c.id);
193
+ const hasAssistantOutput = (stat?.count ?? 0) > 0;
194
+ const finishedAt = hasAssistantOutput ? stat!.lastAt : null;
195
+ return {
196
+ id: c.id,
197
+ scheduledFor: c.createdAt,
198
+ startedAt: c.createdAt,
199
+ finishedAt,
200
+ durationMs:
201
+ finishedAt != null ? finishedAt - c.createdAt : null,
202
+ status: (hasAssistantOutput ? "ok" : "running") as
203
+ | "ok"
204
+ | "running",
205
+ skipReason: null,
206
+ error: null,
207
+ conversationId: c.id,
208
+ createdAt: c.createdAt,
209
+ };
210
+ }),
211
+ };
212
+ },
213
+ },
114
214
  ];
@@ -59,12 +59,20 @@ import {
59
59
  getConversation,
60
60
  getMessageById,
61
61
  } from "../../memory/conversation-crud.js";
62
+ import { getDb } from "../../memory/db-connection.js";
62
63
  import { clearEmbeddingBackendCache } from "../../memory/embedding-backend.js";
63
64
  import { getLlmRequestLogSource } from "../../memory/llm-request-log-source.js";
64
65
  import { getMemoryRecallLogByMessageIds } from "../../memory/memory-recall-log-store.js";
65
66
  import { getMemoryV2ActivationLogByMessageIds } from "../../memory/memory-v2-activation-log-store.js";
66
67
  import { MEMORY_V2_CONSOLIDATION_SOURCE } from "../../memory/v2/constants.js";
68
+ import {
69
+ createConnection,
70
+ listConnections,
71
+ PROVIDERS_REQUIRING_BASE_URL_AND_MODELS,
72
+ } from "../../providers/inference/connections.js";
73
+ import { PROVIDER_CATALOG } from "../../providers/model-catalog.js";
67
74
  import { initializeProviders } from "../../providers/registry.js";
75
+ import { credentialKey } from "../../security/credential-key.js";
68
76
  import { validateAllowlistFile } from "../../security/secret-allowlist.js";
69
77
  import { resolvePricingForUsage } from "../../util/pricing.js";
70
78
  import { BadRequestError, InternalError, NotFoundError } from "./errors.js";
@@ -379,13 +387,42 @@ function readPlainObject(value: unknown): Record<string, unknown> | undefined {
379
387
 
380
388
  function handleGetConfig() {
381
389
  try {
382
- return applyContextDefaultsToRawConfig(loadRawConfig());
390
+ const config = applyContextDefaultsToRawConfig(loadRawConfig());
391
+ enrichProfilesWithVisionFlag(config);
392
+ return config;
383
393
  } catch (err) {
384
394
  const message = err instanceof Error ? err.message : String(err);
385
395
  throw new InternalError(`Failed to read config: ${message}`);
386
396
  }
387
397
  }
388
398
 
399
+ /**
400
+ * Annotate each profile in `config.llm.profiles` with `supportsVision`
401
+ * resolved from the model catalog. The flag is wire-only — it is never
402
+ * persisted to disk. Unknown (provider, model) pairs default to `true`
403
+ * (fail-open) so image upload remains available for custom / unlisted models.
404
+ */
405
+ function enrichProfilesWithVisionFlag(config: unknown): void {
406
+ const root = readPlainObject(config);
407
+ if (!root) return;
408
+ const llm = readPlainObject(root.llm);
409
+ if (!llm) return;
410
+ const profiles = readPlainObject(llm.profiles);
411
+ if (!profiles) return;
412
+
413
+ for (const profile of Object.values(profiles)) {
414
+ const entry = readPlainObject(profile);
415
+ if (!entry) continue;
416
+ const provider = entry.provider;
417
+ const model = entry.model;
418
+ if (typeof provider !== "string" || typeof model !== "string") continue;
419
+
420
+ const catalogProvider = PROVIDER_CATALOG.find((p) => p.id === provider);
421
+ const catalogModel = catalogProvider?.models.find((m) => m.id === model);
422
+ entry.supportsVision = catalogModel?.supportsVision ?? true;
423
+ }
424
+ }
425
+
389
426
  /**
390
427
  * Return the JSON Schema for the assistant config (full or scoped).
391
428
  *
@@ -617,6 +654,36 @@ async function handleReplaceInferenceProfile({
617
654
  );
618
655
  }
619
656
  }
657
+ // When the UI sends provider but no provider_connection ("Any active X
658
+ // connection"), derive the connection now so the config deep-merge doesn't
659
+ // inherit a stale connection from the default layer.
660
+ const fragment = parsed.data as Record<string, unknown>;
661
+ if (!isManaged && fragment.provider && !fragment.provider_connection) {
662
+ const provider = fragment.provider as string;
663
+ const db = getDb();
664
+ const candidates = listConnections(db, { provider });
665
+ const active = candidates.find((c) => c.status === "active");
666
+ if (active) {
667
+ fragment.provider_connection = active.name;
668
+ } else if (!PROVIDERS_REQUIRING_BASE_URL_AND_MODELS.has(provider)) {
669
+ const connectionName = `${provider}-personal`;
670
+ const isKeyless = provider === "ollama";
671
+ const result = createConnection(db, {
672
+ name: connectionName,
673
+ provider,
674
+ auth: isKeyless
675
+ ? { type: "none" }
676
+ : {
677
+ type: "api_key",
678
+ credential: credentialKey(provider, "api_key"),
679
+ },
680
+ });
681
+ if (result.ok) {
682
+ fragment.provider_connection = connectionName;
683
+ }
684
+ }
685
+ }
686
+
620
687
  const raw = loadRawConfig();
621
688
  if (isManaged) {
622
689
  // Partial overlay: keep every existing key intact, only update label
@@ -624,17 +691,9 @@ async function handleReplaceInferenceProfile({
624
691
  // here would wipe the UI-owned seed fields (provider, model, advanced
625
692
  // params) because that function assumes the body carries the full UI
626
693
  // surface.
627
- patchManagedProfileFields(
628
- raw,
629
- name,
630
- parsed.data as Record<string, unknown>,
631
- );
694
+ patchManagedProfileFields(raw, name, fragment);
632
695
  } else {
633
- replaceInferenceProfileConfig(
634
- raw,
635
- name,
636
- parsed.data as Record<string, unknown>,
637
- );
696
+ replaceInferenceProfileConfig(raw, name, fragment);
638
697
  }
639
698
  // Route through `commitConfigWrite` so profile edits flow through the
640
699
  // post-write side effects shared with `handlePatchConfig` /
@@ -101,6 +101,7 @@ import { writeOnboardingSection } from "../../prompts/persona-resolver.js";
101
101
  import { getConfiguredProvider } from "../../providers/provider-send-message.js";
102
102
  import type { Provider } from "../../providers/types.js";
103
103
  import { checkIngressForSecrets } from "../../security/secret-ingress.js";
104
+ import { getSubagentManager } from "../../subagent/index.js";
104
105
  import { getLogger } from "../../util/logger.js";
105
106
  import {
106
107
  getInterfacesDir,
@@ -1492,6 +1493,12 @@ export async function handleSendMessage(
1492
1493
  // that can service host_browser_request events; we restore that single
1493
1494
  // proxy explicitly below without relaxing `hasNoClient`.
1494
1495
  conversation.updateClient(broadcastMessage, !isInteractive);
1496
+ if (isInteractive) {
1497
+ getSubagentManager().updateParentSender(
1498
+ mapping.conversationId,
1499
+ broadcastMessage,
1500
+ );
1501
+ }
1495
1502
 
1496
1503
  // ── Canned first-greeting fast path ──
1497
1504
  // On a completely fresh workspace, skip LLM inference for the macOS
@@ -72,6 +72,7 @@ import { ROUTES as IMAGE_GENERATION_ROUTES } from "./image-generation-routes.js"
72
72
  import { ROUTES as INFERENCE_PROFILE_SESSION_ROUTES } from "./inference-profile-session-routes.js";
73
73
  import { ROUTES as INFERENCE_PROVIDER_CONNECTION_ROUTES } from "./inference-provider-connection-routes.js";
74
74
  import { ROUTES as INFERENCE_SEND_ROUTES } from "./inference-send-routes.js";
75
+ import { ROUTES as A2A_ROUTES } from "./integrations/a2a.js";
75
76
  import { ROUTES as SLACK_CHANNEL_ROUTES } from "./integrations/slack/channel.js";
76
77
  import { ROUTES as SLACK_SHARE_ROUTES } from "./integrations/slack/share.js";
77
78
  import { ROUTES as TELEGRAM_ROUTES } from "./integrations/telegram.js";
@@ -223,6 +224,7 @@ export const ROUTES: RouteDefinition[] = [
223
224
  ...SECRET_ROUTES,
224
225
  ...SETTINGS_ROUTES,
225
226
  ...SKILL_ROUTES,
227
+ ...A2A_ROUTES,
226
228
  ...SLACK_CHANNEL_ROUTES,
227
229
  ...SLACK_SHARE_ROUTES,
228
230
  ...STT_ROUTES,
@@ -10,10 +10,13 @@
10
10
 
11
11
  import { z } from "zod";
12
12
 
13
+ import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
13
14
  import { getConfigReadOnly } from "../../config/loader.js";
14
15
  import { getDb } from "../../memory/db-connection.js";
15
16
  import {
16
17
  AuthSchema,
18
+ type ConnectionModel,
19
+ ConnectionModelSchema,
17
20
  ConnectionProviderSchema,
18
21
  ConnectionStatusSchema,
19
22
  ProviderConnectionSchema,
@@ -35,6 +38,75 @@ import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
35
38
  // ---------------------------------------------------------------------------
36
39
 
37
40
  const providerConnectionResponseSchema = ProviderConnectionSchema;
41
+ const OPENAI_COMPATIBLE_ENDPOINTS_FLAG = "openai-compatible-endpoints";
42
+
43
+ function openAICompatibleEndpointsEnabled(): boolean {
44
+ return isAssistantFeatureFlagEnabled(
45
+ OPENAI_COMPATIBLE_ENDPOINTS_FLAG,
46
+ getConfigReadOnly(),
47
+ );
48
+ }
49
+
50
+ function rejectDisabledOpenAICompatibleProvider(provider: string): void {
51
+ if (provider !== "openai-compatible") return;
52
+ if (openAICompatibleEndpointsEnabled()) return;
53
+ throw new BadRequestError(
54
+ "OpenAI-compatible endpoints are disabled. Enable the openai-compatible-endpoints feature flag to configure this provider.",
55
+ );
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Custom provider field parsing (openai-compatible base_url + models)
60
+ // ---------------------------------------------------------------------------
61
+
62
+ function parseCustomProviderFields(body: Record<string, unknown>): {
63
+ baseUrl?: string | null;
64
+ models?: ConnectionModel[] | null;
65
+ } {
66
+ const out: {
67
+ baseUrl?: string | null;
68
+ models?: ConnectionModel[] | null;
69
+ } = {};
70
+
71
+ if ("base_url" in body) {
72
+ const raw = body.base_url;
73
+ if (raw === null) {
74
+ out.baseUrl = null;
75
+ } else if (typeof raw === "string" && raw.length > 0) {
76
+ try {
77
+ const parsed = new URL(raw);
78
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
79
+ throw new BadRequestError(`Invalid base_url: must be an http(s) URL`);
80
+ }
81
+ } catch (err) {
82
+ if (err instanceof BadRequestError) throw err;
83
+ throw new BadRequestError(
84
+ `Invalid base_url: must be a valid http(s) URL`,
85
+ );
86
+ }
87
+ out.baseUrl = raw;
88
+ } else {
89
+ throw new BadRequestError(
90
+ `Invalid base_url: must be a non-empty string or null`,
91
+ );
92
+ }
93
+ }
94
+
95
+ if ("models" in body) {
96
+ const raw = body.models;
97
+ if (raw === null) {
98
+ out.models = null;
99
+ } else {
100
+ const parsed = z.array(ConnectionModelSchema).safeParse(raw);
101
+ if (!parsed.success) {
102
+ throw new BadRequestError(`Invalid models: ${parsed.error.message}`);
103
+ }
104
+ out.models = parsed.data;
105
+ }
106
+ }
107
+
108
+ return out;
109
+ }
38
110
 
39
111
  // ---------------------------------------------------------------------------
40
112
  // Handlers
@@ -42,10 +114,18 @@ const providerConnectionResponseSchema = ProviderConnectionSchema;
42
114
 
43
115
  function handleListConnections({ queryParams = {} }: RouteHandlerArgs) {
44
116
  const provider = queryParams.provider;
117
+ if (provider) rejectDisabledOpenAICompatibleProvider(provider);
45
118
  const connections = listConnections(
46
119
  getDb(),
47
120
  provider ? { provider } : undefined,
48
121
  );
122
+ if (!openAICompatibleEndpointsEnabled()) {
123
+ return {
124
+ connections: connections.filter(
125
+ (conn) => conn.provider !== "openai-compatible",
126
+ ),
127
+ };
128
+ }
49
129
  return { connections };
50
130
  }
51
131
 
@@ -55,6 +135,12 @@ function handleGetConnection({ pathParams = {} }: RouteHandlerArgs) {
55
135
 
56
136
  const conn = getConnection(getDb(), name);
57
137
  if (!conn) throw new NotFoundError(`Connection "${name}" not found.`);
138
+ if (
139
+ conn.provider === "openai-compatible" &&
140
+ !openAICompatibleEndpointsEnabled()
141
+ ) {
142
+ throw new NotFoundError(`Connection "${name}" not found.`);
143
+ }
58
144
 
59
145
  return conn;
60
146
  }
@@ -74,6 +160,7 @@ function handleCreateConnection({ body = {} }: RouteHandlerArgs) {
74
160
  `Invalid provider "${String(provider)}". Valid: ${VALID_CONNECTION_PROVIDERS.join(", ")}`,
75
161
  );
76
162
  }
163
+ rejectDisabledOpenAICompatibleProvider(providerResult.data);
77
164
 
78
165
  const authResult = AuthSchema.safeParse(auth);
79
166
  if (!authResult.success) {
@@ -99,12 +186,15 @@ function handleCreateConnection({ body = {} }: RouteHandlerArgs) {
99
186
  );
100
187
  }
101
188
 
189
+ const customFields = parseCustomProviderFields(body);
190
+
102
191
  const result = createConnection(getDb(), {
103
192
  name,
104
193
  provider: providerResult.data,
105
194
  auth: authResult.data,
106
195
  ...(statusResult ? { status: statusResult.data } : {}),
107
196
  ...(labelRaw !== undefined ? { label: labelRaw as string | null } : {}),
197
+ ...customFields,
108
198
  });
109
199
 
110
200
  if (!result.ok) {
@@ -118,6 +208,16 @@ function handleCreateConnection({ body = {} }: RouteHandlerArgs) {
118
208
  `Invalid provider "${result.error.provider}". Valid: ${VALID_CONNECTION_PROVIDERS.join(", ")}`,
119
209
  );
120
210
  }
211
+ if (result.error.code === "base_url_required") {
212
+ throw new BadRequestError(
213
+ "base_url is required for openai-compatible connections.",
214
+ );
215
+ }
216
+ if (result.error.code === "models_required") {
217
+ throw new BadRequestError(
218
+ "At least one model is required for openai-compatible connections.",
219
+ );
220
+ }
121
221
  throw new BadRequestError("Invalid auth configuration.");
122
222
  }
123
223
 
@@ -131,6 +231,15 @@ function handleUpdateConnection({
131
231
  const { name } = pathParams;
132
232
  if (!name) throw new BadRequestError("name is required");
133
233
 
234
+ const existing = getConnection(getDb(), name);
235
+ if (!existing) throw new NotFoundError(`Connection "${name}" not found.`);
236
+ if (
237
+ existing.provider === "openai-compatible" &&
238
+ !openAICompatibleEndpointsEnabled()
239
+ ) {
240
+ throw new NotFoundError(`Connection "${name}" not found.`);
241
+ }
242
+
134
243
  const auth = body.auth;
135
244
  const authResult = AuthSchema.safeParse(auth);
136
245
  if (!authResult.success) {
@@ -169,16 +278,29 @@ function handleUpdateConnection({
169
278
  );
170
279
  }
171
280
 
281
+ const customFields = parseCustomProviderFields(body);
282
+
172
283
  const result = updateConnection(getDb(), name, {
173
284
  auth: authResult.data,
174
285
  ...(statusResult ? { status: statusResult.data } : {}),
175
286
  ...(labelRaw !== undefined ? { label: labelRaw as string | null } : {}),
287
+ ...customFields,
176
288
  });
177
289
 
178
290
  if (!result.ok) {
179
291
  if (result.error.code === "not_found") {
180
292
  throw new NotFoundError(`Connection "${name}" not found.`);
181
293
  }
294
+ if (result.error.code === "base_url_required") {
295
+ throw new BadRequestError(
296
+ "base_url is required for openai-compatible connections.",
297
+ );
298
+ }
299
+ if (result.error.code === "models_required") {
300
+ throw new BadRequestError(
301
+ "At least one model is required for openai-compatible connections.",
302
+ );
303
+ }
182
304
  throw new BadRequestError("Invalid auth configuration.");
183
305
  }
184
306
 
@@ -191,7 +313,14 @@ function handleDeleteConnection({ pathParams = {} }: RouteHandlerArgs) {
191
313
 
192
314
  // Existence check first so a stale `llm.default.provider_connection`
193
315
  // reference to a missing connection returns 404 (not 409).
194
- if (!getConnection(getDb(), name)) {
316
+ const existing = getConnection(getDb(), name);
317
+ if (!existing) {
318
+ throw new NotFoundError(`Connection "${name}" not found.`);
319
+ }
320
+ if (
321
+ existing.provider === "openai-compatible" &&
322
+ !openAICompatibleEndpointsEnabled()
323
+ ) {
195
324
  throw new NotFoundError(`Connection "${name}" not found.`);
196
325
  }
197
326
 
@@ -301,6 +430,8 @@ export const ROUTES: RouteDefinition[] = [
301
430
  auth: AuthSchema,
302
431
  label: z.string().min(1).optional(),
303
432
  status: ConnectionStatusSchema.optional(),
433
+ base_url: z.string().url().nullable().optional(),
434
+ models: z.array(ConnectionModelSchema).nullable().optional(),
304
435
  }),
305
436
  responseBody: providerConnectionResponseSchema,
306
437
  responseStatus: "201",
@@ -324,6 +455,8 @@ export const ROUTES: RouteDefinition[] = [
324
455
  auth: AuthSchema,
325
456
  status: ConnectionStatusSchema.optional(),
326
457
  label: z.string().min(1).nullable().optional(),
458
+ base_url: z.string().url().nullable().optional(),
459
+ models: z.array(ConnectionModelSchema).nullable().optional(),
327
460
  }),
328
461
  responseBody: providerConnectionResponseSchema,
329
462
  additionalResponses: {