@vellumai/assistant 0.12.4-dev.202609231914.158603f → 0.12.4-dev.202609232017.d454a3d

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.
package/openapi.yaml CHANGED
@@ -2597,6 +2597,58 @@ paths:
2597
2597
  - userId
2598
2598
  - authenticated
2599
2599
  additionalProperties: false
2600
+ /v1/auto-profile/preview:
2601
+ post:
2602
+ operationId: autoprofile_preview_post
2603
+ summary: Preview the Auto profile's pick for a draft
2604
+ description:
2605
+ Ask the Auto profile which default profile a draft message would run on, using the conversation's recent
2606
+ history when one is given. The pick is kept briefly so the turn that sends the same text reuses it. Spends one
2607
+ Jev call per request.
2608
+ tags:
2609
+ - conversations
2610
+ requestBody:
2611
+ required: true
2612
+ content:
2613
+ application/json:
2614
+ schema:
2615
+ type: object
2616
+ properties:
2617
+ text:
2618
+ type: string
2619
+ minLength: 1
2620
+ maxLength: 20000
2621
+ conversationId:
2622
+ type: string
2623
+ description: Conversation the draft belongs to, for recent history. Omit for a new chat.
2624
+ required:
2625
+ - text
2626
+ responses:
2627
+ "200":
2628
+ description: Successful response
2629
+ content:
2630
+ application/json:
2631
+ schema:
2632
+ type: object
2633
+ properties:
2634
+ profile:
2635
+ anyOf:
2636
+ - type: string
2637
+ - type: "null"
2638
+ outcome:
2639
+ type: string
2640
+ enum:
2641
+ - routed
2642
+ - unavailable
2643
+ - timeout
2644
+ - error
2645
+ - fallback
2646
+ confidence:
2647
+ type: number
2648
+ required:
2649
+ - profile
2650
+ - outcome
2651
+ additionalProperties: false
2600
2652
  /v1/avatar/accent:
2601
2653
  post:
2602
2654
  operationId: avatar_accent_post
@@ -22518,6 +22570,8 @@ paths:
22518
22570
  type: boolean
22519
22571
  noResponse:
22520
22572
  type: boolean
22573
+ autoRoutedProfile:
22574
+ type: string
22521
22575
  cameraFrame:
22522
22576
  type: boolean
22523
22577
  const: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.12.4-dev.202609231914.158603f",
3
+ "version": "0.12.4-dev.202609232017.d454a3d",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -34,6 +34,10 @@ export const GenerationHandoffEventSchema = z.object({
34
34
  modeSession: ModeSessionSchema.optional(),
35
35
  attachments: z.array(AssistantOutboundAttachmentSchema).optional(),
36
36
  attachmentWarnings: z.array(z.string()).optional(),
37
+ /** The default profile the Auto profile routed the finished turn to,
38
+ * mirroring `message_complete`, so a reply that hands off to a queued
39
+ * turn still gets its badge on the live row. */
40
+ autoRoutedProfile: z.string().optional(),
37
41
  });
38
42
 
39
43
  export type GenerationHandoffEvent = z.infer<
@@ -42,6 +42,10 @@ export const MessageCompleteEventSchema = z.object({
42
42
  * its reply through the `send_user_message` tool, so a client can give the
43
43
  * live row the same per-row treatment it will get from history. */
44
44
  assistantTextVisibility: z.enum(["private", "visible"]).optional(),
45
+ /** The default profile the Auto profile routed this turn to, mirroring the
46
+ * field of the same name on `ConversationMessage`. Set only on a turn that
47
+ * ran on Auto, so the live row can show which profile answered. */
48
+ autoRoutedProfile: z.string().optional(),
45
49
  });
46
50
 
47
51
  export type MessageCompleteEvent = z.infer<typeof MessageCompleteEventSchema>;
@@ -624,6 +624,10 @@ export const ConversationMessageSchema = z.object({
624
624
  * sentinel text, and treat the row as the turn's reply so nothing keeps
625
625
  * waiting for one. */
626
626
  noResponse: z.boolean().optional(),
627
+ /** The default profile key the Auto profile routed this turn to
628
+ * (`metadata.autoRoutedProfile`). Set only on an assistant row produced by
629
+ * a turn that ran on Auto; clients show which profile answered. */
630
+ autoRoutedProfile: z.string().optional(),
627
631
  /** Set only on standalone ambient camera-frame rows, derived from
628
632
  * `messageMetadataIsAmbientSightKeep`. Clients may fold consecutive frames
629
633
  * into the following user message. Absent on shutter photos, spoken turns
@@ -1483,6 +1483,30 @@
1483
1483
  "logo": "webull-mcp.png",
1484
1484
  "category": "finance"
1485
1485
  }
1486
+ },
1487
+ {
1488
+ "name": "wix",
1489
+ "source": {
1490
+ "source": "local",
1491
+ "path": "plugins/mcp-catalog/wix",
1492
+ "version": "1.0.2"
1493
+ },
1494
+ "description": "Manage Wix sites, stores, bookings, and CMS content.",
1495
+ "homepage": "https://dev.wix.com/docs/sdk/articles/use-the-wix-mcp/about-the-wix-mcp",
1496
+ "license": "MIT",
1497
+ "integration": {
1498
+ "kind": "mcp",
1499
+ "displayName": "Wix",
1500
+ "documentationUrl": "https://dev.wix.com/docs/sdk/articles/use-the-wix-mcp/about-the-wix-mcp",
1501
+ "verifiedAt": "2026-09-23",
1502
+ "verification": "documentation-only",
1503
+ "setup": {
1504
+ "mode": "oauth",
1505
+ "instructions": "Sign in with the Wix account that owns the sites you want to manage."
1506
+ },
1507
+ "logo": "wix-mcp.png",
1508
+ "category": "commerce"
1509
+ }
1486
1510
  }
1487
1511
  ]
1488
1512
  }
@@ -781,6 +781,27 @@
781
781
  "contentBase64": "ewogICIkc2NoZW1hIjogImh0dHBzOi8vYWdlbnQtcGx1Z2lucy5vcmcvc2NoZW1hcy8xLjAuMC9wbHVnaW4uc2NoZW1hLmpzb24iLAogICJuYW1lIjogIndlYnVsbCIsCiAgInZlcnNpb24iOiAiMS4wLjEiLAogICJkZXNjcmlwdGlvbiI6ICJSZXZpZXcgYnJva2VyYWdlIGFjY291bnRzLCBvcmRlcnMsIHBvc2l0aW9ucywgYW5kIG1hcmtldCBkYXRhLiIsCiAgImhvbWVwYWdlIjogImh0dHBzOi8vZGV2ZWxvcGVyLndlYnVsbC5jb20vYXBpcy9kb2NzL0FJLWZyaWVuZGx5LVJlc291cmNlcy9tY3AvIiwKICAibGljZW5zZSI6ICJNSVQiCn0K"
782
782
  }
783
783
  ]
784
+ },
785
+ "plugins/mcp-catalog/wix": {
786
+ "version": "1.0.2",
787
+ "files": [
788
+ {
789
+ "path": "ICON_ATTRIBUTION.md",
790
+ "contentBase64": "IyBJY29uIGF0dHJpYnV0aW9uCgpTb3VyY2U6IFtXaXgncyBvZmZpY2lhbCAxOTJweCB3ZWJzaXRlIGljb25dKGh0dHBzOi8vd3d3LndpeC5jb20vZmF2aWNvbi5pY28pLCBsaW5rZWQgZnJvbSBbd2l4LmNvbV0oaHR0cHM6Ly93d3cud2l4LmNvbS8pLgoKVGhlIGFzc2V0IHdhcyByZXNpemVkIHRvIDEyOHgxMjggYW5kIGZsYXR0ZW5lZCBvbnRvIGFuIG9wYXF1ZSB3aGl0ZSBiYWNrZ3JvdW5kIGZvciB0aGVtZSBjb250cmFzdC4gSXQgaXMgYSB0cmFkZW1hcmsgb2YgV2l4LmNvbSBhbmQgaXMgdXNlZCBvbmx5IHRvIGlkZW50aWZ5IHRoaXMgaW50ZWdyYXRpb24uCg=="
791
+ },
792
+ {
793
+ "path": "icon.png",
794
+ "contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAN7ElEQVR42u2dbUxbVdzA710pUNpJxxilpIW+QAuMlo6iRDNf5tucQ9miyURNiLr5gnOyGsj0w3xLnI7xQR0YmfODxheMCSMBgppl4gLZ5lyBlkGBAisd71sLLS194z4f7pPmPue2BW5vcj485/eJ/XvuufeeX+8553/ugeEEQWAIeGyBfQH/30ECIIMEQAYJgAwSABkkADJIAGSQAMggAZBBAiCDBEAGCYAMEgAZJAAySABkkADIIAGQQQIggwRABgmADBIAGSQAMkgAZJAAyCABkEECIIMEQAYJgAwSABkkADJIAGSQAMggAZBBAiCDBEAGCYAMEgAZJAAySABkkADIIAGQQQIggwRABgmATMJmDzAajV999dXq6iqPx9PpdPv371cqlRs8dnl52WQydXd32+32pKQkuVy+e/dujUbD5XI3cjhBECaTqaura2hoyOv1pqenv/vuu3l5edQC/f39v/32m81mCwaDUqn05MmTW7duJT+9evVqU1OT3+/HcZz8EyUikai8vPzBBx9MSkraeAsEAoErV650dHTY7fZQKETWxuVyq6qqHnvssU0bIDbJtWvXsrOzyWO5XK5er+/o6AiFQuseODg4eOjQobS0NA6HEz5cJBLV1tZOTk6ue3gwGGxpaVGpVAkJ//ulKS4uHhsbo5Zpb28vKCgI31peXt7CwkL40+np6X379gG3n5aW9vHHHy8vL2/w9j0ez6effpqZmQnUU1ZWNjo6utnGJAhi0wLm5+cfffRR6rl37tz577//xj7KbDYDR1GpqKiwWq2xa+jo6MjJyaEe9fzzz/v9/nCBxcXF3bt3UwsUFhZSBRAEcfnyZYlEApxdIBB88803a2trG7n9lpYWoVAI1JCZmXnhwgUGrc9EAEEQb775JnAFR44c8Xq90crfuXPn2Wefjf0gHjlyxO12R6thaWnp8ccfp5bncDiffPIJtUx3dzfQNHQBa2trp0+fpnc4ubm5169fX/fG+/r6du3aBRzL4XDq6upi3D77Ar788svExETqRSgUCpvNFq38jz/+mJycHFvA9u3b//jjj2g1GI3GHTt2AF/btrY2apm2trZwdx9NAEEQc3Nzzz33HP0CXnzxxbt378a4a5fL9dprr+E4Dhz40EMPjY+PM2t9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"
795
+ },
796
+ {
797
+ "path": "mcp.json",
798
+ "contentBase64": "ewogICIkc2NoZW1hIjogImh0dHBzOi8vYWdlbnQtcGx1Z2lucy5vcmcvc2NoZW1hcy8xLjAuMC9tY3Auc2NoZW1hLmpzb24iLAogICJtY3BTZXJ2ZXJzIjogewogICAgIndpeCI6IHsKICAgICAgInR5cGUiOiAic3RyZWFtYWJsZS1odHRwIiwKICAgICAgInVybCI6ICJodHRwczovL21jcC53aXguY29tL21jcCIKICAgIH0KICB9Cn0K"
799
+ },
800
+ {
801
+ "path": "plugin.json",
802
+ "contentBase64": "ewogICIkc2NoZW1hIjogImh0dHBzOi8vYWdlbnQtcGx1Z2lucy5vcmcvc2NoZW1hcy8xLjAuMC9wbHVnaW4uc2NoZW1hLmpzb24iLAogICJuYW1lIjogIndpeCIsCiAgInZlcnNpb24iOiAiMS4wLjIiLAogICJkZXNjcmlwdGlvbiI6ICJNYW5hZ2UgV2l4IHNpdGVzLCBzdG9yZXMsIGJvb2tpbmdzLCBhbmQgQ01TIGNvbnRlbnQuIiwKICAiaG9tZXBhZ2UiOiAiaHR0cHM6Ly9kZXYud2l4LmNvbS9kb2NzL3Nkay9hcnRpY2xlcy91c2UtdGhlLXdpeC1tY3AvYWJvdXQtdGhlLXdpeC1tY3AiLAogICJsaWNlbnNlIjogIk1JVCIKfQo="
803
+ }
804
+ ]
784
805
  }
785
806
  }
786
807
  }
@@ -9,10 +9,16 @@ import type {
9
9
  } from "../../providers/types.js";
10
10
  import {
11
11
  AUTO_PROFILE_FALLBACK,
12
+ AUTO_PROFILE_PREVIEW_REUSE_MS,
12
13
  autoProfileCandidates,
13
14
  autoProfileFallback,
15
+ autoProfilePreviewContext,
16
+ type AutoProfileRoute,
17
+ clearAutoProfilePreviewsForTesting,
14
18
  recentConversationForRouter,
19
+ rememberAutoProfilePreview,
15
20
  routeAutoProfile,
21
+ takeAutoProfilePreview,
16
22
  } from "../auto-profile-router.js";
17
23
 
18
24
  function text(role: "user" | "assistant", body: string): Message {
@@ -54,6 +60,17 @@ const profiles: Record<string, ProfileEntry> = {
54
60
  "latency-optimized": managed("Fast"),
55
61
  };
56
62
 
63
+ const contextFor = (
64
+ history: readonly Message[],
65
+ userMessage: string,
66
+ candidatesFrom: Record<string, ProfileEntry>,
67
+ ) =>
68
+ autoProfilePreviewContext(
69
+ history,
70
+ userMessage,
71
+ autoProfileCandidates(candidatesFrom),
72
+ );
73
+
57
74
  const base = {
58
75
  conversationId: "conv-1",
59
76
  history: [text("user", "hi"), text("assistant", "hello")],
@@ -226,3 +243,138 @@ describe("routeAutoProfile", () => {
226
243
  expect(autoProfileFallback([])).toBe(AUTO_PROFILE_FALLBACK);
227
244
  });
228
245
  });
246
+
247
+ describe("draft preview reuse", () => {
248
+ const routed: AutoProfileRoute = {
249
+ profile: "quality-optimized",
250
+ outcome: "routed",
251
+ confidence: 0.9,
252
+ latencyMs: 120,
253
+ };
254
+ const history = [text("user", "hi"), text("assistant", "hello")];
255
+ const context = (h: Message[] = history) =>
256
+ contextFor(h, "Refactor the auth module", profiles);
257
+ const take = (
258
+ overrides: Partial<Parameters<typeof takeAutoProfilePreview>[0]> = {},
259
+ ) =>
260
+ takeAutoProfilePreview({
261
+ conversationId: "conv-1",
262
+ text: "Refactor the auth module",
263
+ history,
264
+ profiles,
265
+ ...overrides,
266
+ });
267
+
268
+ test("a routed preview is reused once by the turn that sends the same text", () => {
269
+ clearAutoProfilePreviewsForTesting();
270
+ rememberAutoProfilePreview(
271
+ "conv-1",
272
+ "Refactor the auth\nmodule",
273
+ routed,
274
+ context(),
275
+ );
276
+ expect(take()).toEqual(routed);
277
+ expect(take()).toBeUndefined();
278
+ });
279
+
280
+ test("a different draft, another conversation, or an expired preview is not reused", () => {
281
+ clearAutoProfilePreviewsForTesting();
282
+ const at = Date.now();
283
+ rememberAutoProfilePreview("conv-1", "hello", routed, context(), at);
284
+ expect(take({ text: "hello there" })).toBeUndefined();
285
+ expect(take({ text: "hello", conversationId: "conv-2" })).toBeUndefined();
286
+ expect(
287
+ take({ text: "hello", now: at + AUTO_PROFILE_PREVIEW_REUSE_MS + 1 }),
288
+ ).toBeUndefined();
289
+ });
290
+
291
+ test("a preview is not reused once the conversation or the candidates moved on", () => {
292
+ clearAutoProfilePreviewsForTesting();
293
+ rememberAutoProfilePreview("conv-1", "do that too", routed, context());
294
+ expect(
295
+ take({
296
+ text: "do that too",
297
+ history: [...history, text("assistant", "Anything else?")],
298
+ }),
299
+ ).toBeUndefined();
300
+ rememberAutoProfilePreview("conv-1", "do that too", routed, context());
301
+ expect(
302
+ take({
303
+ text: "do that too",
304
+ profiles: {
305
+ ...profiles,
306
+ "cost-optimized": managed("Budget", "disabled"),
307
+ },
308
+ }),
309
+ ).toBeUndefined();
310
+ });
311
+
312
+ test("a preview made before the conversation existed fits only a turn with no history", () => {
313
+ clearAutoProfilePreviewsForTesting();
314
+ rememberAutoProfilePreview(
315
+ undefined,
316
+ "first message",
317
+ routed,
318
+ contextFor([], "first message", profiles),
319
+ );
320
+ expect(
321
+ take({ conversationId: "conv-old", text: "first message" }),
322
+ ).toBeUndefined();
323
+ expect(
324
+ take({ conversationId: "conv-new", text: "first message", history: [] }),
325
+ ).toEqual(routed);
326
+ });
327
+
328
+ test("a fallback is not remembered, so the turn asks Jev again", () => {
329
+ clearAutoProfilePreviewsForTesting();
330
+ rememberAutoProfilePreview(
331
+ "conv-1",
332
+ "hi",
333
+ { profile: AUTO_PROFILE_FALLBACK, outcome: "timeout", latencyMs: 1000 },
334
+ contextFor(history, "hi", profiles),
335
+ );
336
+ expect(take({ text: "hi" })).toBeUndefined();
337
+ });
338
+
339
+ test("abandoned previews expire and the map stays bounded", () => {
340
+ clearAutoProfilePreviewsForTesting();
341
+ const at = Date.now();
342
+ rememberAutoProfilePreview(
343
+ "conv-stale",
344
+ "old draft",
345
+ routed,
346
+ context(),
347
+ at,
348
+ );
349
+ for (let i = 0; i < 250; i += 1) {
350
+ rememberAutoProfilePreview(
351
+ `conv-${i}`,
352
+ "draft",
353
+ routed,
354
+ context(),
355
+ at + AUTO_PROFILE_PREVIEW_REUSE_MS + 1,
356
+ );
357
+ }
358
+ expect(
359
+ take({
360
+ conversationId: "conv-stale",
361
+ text: "old draft",
362
+ now: at + AUTO_PROFILE_PREVIEW_REUSE_MS + 2,
363
+ }),
364
+ ).toBeUndefined();
365
+ expect(
366
+ take({
367
+ conversationId: "conv-0",
368
+ text: "draft",
369
+ now: at + AUTO_PROFILE_PREVIEW_REUSE_MS + 2,
370
+ }),
371
+ ).toBeUndefined();
372
+ expect(
373
+ take({
374
+ conversationId: "conv-249",
375
+ text: "draft",
376
+ now: at + AUTO_PROFILE_PREVIEW_REUSE_MS + 2,
377
+ }),
378
+ ).toEqual(routed);
379
+ });
380
+ });
@@ -9,13 +9,18 @@
9
9
  * cannot judge runs exactly as a Balanced turn would.
10
10
  */
11
11
 
12
+ import { getConversationProfilesForProvider } from "../config/default-profile-catalog.js";
12
13
  import {
14
+ AUTO_PROFILE_KEY,
13
15
  AUTO_PROFILE_ROUTER_CALL_SITE,
14
16
  DEFAULT_PROFILE_KEYS,
15
17
  type DefaultProfileKey,
16
18
  isDefaultProfileKey,
17
19
  } from "../config/default-profile-names.js";
20
+ import { getConfig } from "../config/loader.js";
18
21
  import type { ProfileEntry } from "../config/schemas/llm.js";
22
+ import { getMessagesPaginated } from "../persistence/conversation-crud.js";
23
+ import { resolveConversationId } from "../persistence/conversation-key-store.js";
19
24
  import { askTypesafe } from "../providers/jev/ask.js";
20
25
  import type { ContentBlock, Message, Provider } from "../providers/types.js";
21
26
  import { getLogger } from "../util/logger.js";
@@ -218,3 +223,209 @@ export async function routeAutoProfile(args: {
218
223
  latencyMs: result.latencyMs,
219
224
  };
220
225
  }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Draft preview and send-time reuse
229
+ // ---------------------------------------------------------------------------
230
+
231
+ /**
232
+ * How long a draft preview stays reusable by the turn that sends the same
233
+ * text. Long enough to cover the pause between the preview and the send,
234
+ * short enough that a stale pick never outlives the draft it was made for.
235
+ */
236
+ export const AUTO_PROFILE_PREVIEW_REUSE_MS = 60_000;
237
+
238
+ /** Recent history rows the preview shows the router, matching the turn's window. */
239
+ const PREVIEW_HISTORY_ROWS = 12;
240
+
241
+ /** Cache key for a preview made before the conversation has a server id. */
242
+ const NO_CONVERSATION_KEY = "*";
243
+
244
+ interface RememberedPreview {
245
+ text: string;
246
+ /** What the router saw besides the text: the candidate set and the last
247
+ * line of recent history. A send whose context differs re-routes. */
248
+ context: string;
249
+ route: AutoProfileRoute;
250
+ at: number;
251
+ }
252
+
253
+ /**
254
+ * Bound on remembered previews. Abandoned drafts never get taken, so
255
+ * expiry alone would let the map grow with every conversation ever
256
+ * previewed; oldest entries go first once the bound is reached.
257
+ */
258
+ const MAX_REMEMBERED_PREVIEWS = 200;
259
+
260
+ const rememberedPreviews = new Map<string, RememberedPreview>();
261
+
262
+ function normalizeDraft(text: string): string {
263
+ return text.replace(/\s+/g, " ").trim();
264
+ }
265
+
266
+ /**
267
+ * The routing inputs other than the draft, reduced to a comparable string:
268
+ * which profiles were candidates, and the last line of history the router
269
+ * saw. Both sides (preview from persisted rows, turn from the live history)
270
+ * render history through `recentConversationForRouter`, so the last line
271
+ * agrees whenever the conversation has not moved.
272
+ */
273
+ export function autoProfilePreviewContext(
274
+ history: readonly Message[],
275
+ userMessage: string,
276
+ candidates: readonly DefaultProfileKey[],
277
+ ): string {
278
+ const rendered = recentConversationForRouter(history, userMessage);
279
+ const lastLine = rendered.slice(rendered.lastIndexOf("\n") + 1);
280
+ return `${candidates.join(",")}\u0000${lastLine}`;
281
+ }
282
+
283
+ function pruneRememberedPreviews(now: number): void {
284
+ for (const [key, entry] of rememberedPreviews) {
285
+ if (now - entry.at > AUTO_PROFILE_PREVIEW_REUSE_MS) {
286
+ rememberedPreviews.delete(key);
287
+ }
288
+ }
289
+ while (rememberedPreviews.size >= MAX_REMEMBERED_PREVIEWS) {
290
+ const oldest = rememberedPreviews.keys().next().value;
291
+ if (oldest === undefined) {
292
+ break;
293
+ }
294
+ rememberedPreviews.delete(oldest);
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Keep a routed preview so the turn that sends the same text reuses it: one
300
+ * Jev call per message instead of two, and the reply's badge matches what
301
+ * the composer showed. Only a routed pick is kept; a fallback is worth
302
+ * retrying at send time.
303
+ */
304
+ export function rememberAutoProfilePreview(
305
+ conversationId: string | undefined,
306
+ text: string,
307
+ route: AutoProfileRoute,
308
+ context: string,
309
+ now: number = Date.now(),
310
+ ): void {
311
+ if (route.outcome !== "routed") {
312
+ return;
313
+ }
314
+ pruneRememberedPreviews(now);
315
+ rememberedPreviews.set(conversationId ?? NO_CONVERSATION_KEY, {
316
+ text: normalizeDraft(text),
317
+ context,
318
+ route,
319
+ at: now,
320
+ });
321
+ }
322
+
323
+ /**
324
+ * The remembered preview for this turn, consumed on read. It has to match
325
+ * the text and the routing context the turn would otherwise send to Jev.
326
+ * A preview made before the conversation existed saw an empty history, so
327
+ * it fits only a turn whose history is still empty: with no history and the
328
+ * same text and candidates, the two requests are identical.
329
+ */
330
+ export function takeAutoProfilePreview(args: {
331
+ conversationId: string;
332
+ text: string;
333
+ history: readonly Message[];
334
+ profiles: Readonly<Record<string, ProfileEntry>>;
335
+ now?: number;
336
+ }): AutoProfileRoute | undefined {
337
+ const now = args.now ?? Date.now();
338
+ const wanted = normalizeDraft(args.text);
339
+ const candidates = autoProfileCandidates(args.profiles);
340
+ const context = autoProfilePreviewContext(args.history, wanted, candidates);
341
+ const historyIsEmpty =
342
+ recentConversationForRouter(args.history, wanted).length === 0;
343
+ const keys = historyIsEmpty
344
+ ? [args.conversationId, NO_CONVERSATION_KEY]
345
+ : [args.conversationId];
346
+ for (const key of keys) {
347
+ const remembered = rememberedPreviews.get(key);
348
+ if (!remembered) {
349
+ continue;
350
+ }
351
+ if (now - remembered.at > AUTO_PROFILE_PREVIEW_REUSE_MS) {
352
+ rememberedPreviews.delete(key);
353
+ continue;
354
+ }
355
+ if (remembered.text === wanted && remembered.context === context) {
356
+ rememberedPreviews.delete(key);
357
+ return remembered.route;
358
+ }
359
+ }
360
+ return undefined;
361
+ }
362
+
363
+ /** For tests. */
364
+ export function clearAutoProfilePreviewsForTesting(): void {
365
+ rememberedPreviews.clear();
366
+ }
367
+
368
+ /**
369
+ * Recent conversational rows for the preview: the same window the turn sees,
370
+ * counted over rows that carry spoken text so tool traffic cannot crowd a
371
+ * short follow-up's context out of the page.
372
+ */
373
+ function recentHistoryFromDb(conversationId: string | undefined): Message[] {
374
+ const resolved = conversationId
375
+ ? resolveConversationId(conversationId)
376
+ : null;
377
+ if (!resolved) {
378
+ return [];
379
+ }
380
+ return getMessagesPaginated(
381
+ resolved,
382
+ PREVIEW_HISTORY_ROWS,
383
+ undefined,
384
+ (row) =>
385
+ (row.role === "user" || row.role === "assistant") &&
386
+ plainTextOf(row.content).length > 0,
387
+ ).messages.map((row) => ({
388
+ role: row.role as Message["role"],
389
+ content: row.content,
390
+ }));
391
+ }
392
+
393
+ /**
394
+ * Route a draft the user has not sent yet, so the composer can show the pick
395
+ * as they type. Returns null when the Auto profile is not available on this
396
+ * install; otherwise the same route a turn on the draft would get, remembered
397
+ * for that turn to reuse.
398
+ */
399
+ export async function previewAutoProfile(args: {
400
+ conversationId?: string;
401
+ text: string;
402
+ signal?: AbortSignal;
403
+ }): Promise<AutoProfileRoute | null> {
404
+ const { llm } = getConfig();
405
+ const profiles = getConversationProfilesForProvider(
406
+ llm.profiles,
407
+ llm.defaultProvider ?? null,
408
+ );
409
+ if (profiles[AUTO_PROFILE_KEY]?.source !== "managed") {
410
+ return null;
411
+ }
412
+ const history = recentHistoryFromDb(args.conversationId);
413
+ const route = await routeAutoProfile({
414
+ conversationId: args.conversationId ?? NO_CONVERSATION_KEY,
415
+ history,
416
+ userMessage: args.text,
417
+ profiles,
418
+ ...(args.signal ? { signal: args.signal } : {}),
419
+ });
420
+ rememberAutoProfilePreview(
421
+ args.conversationId,
422
+ args.text,
423
+ route,
424
+ autoProfilePreviewContext(
425
+ history,
426
+ normalizeDraft(args.text),
427
+ autoProfileCandidates(profiles),
428
+ ),
429
+ );
430
+ return route;
431
+ }
@@ -53,6 +53,7 @@ import {
53
53
  updateMessageContent,
54
54
  } from "../persistence/conversation-crud.js";
55
55
  import { syncMessageToDisk } from "../persistence/conversation-disk-view.js";
56
+ import { AUTO_ROUTED_PROFILE_METADATA_KEY } from "../persistence/conversation-types.js";
56
57
  import { enqueueLexicalIndexForMessage } from "../persistence/job-handlers/message-lexical.js";
57
58
  import {
58
59
  backfillMessageIdOnLogs,
@@ -275,6 +276,12 @@ export interface EventHandlerState {
275
276
  * the model ever having seen it.
276
277
  */
277
278
  pendingNotifiedInferenceProfile: string | null;
279
+ /**
280
+ * The default profile the Auto profile routed this turn to, stamped on the
281
+ * turn's assistant rows and carried on `message_complete` so clients can
282
+ * show which profile answered. `undefined` on every turn not run on Auto.
283
+ */
284
+ autoRoutedProfile: string | undefined;
278
285
  pendingDirectiveDisplayBuffer: string;
279
286
  firstAssistantText: string;
280
287
  /** Most recent resolved provider for the current exchange's usage accounting. */
@@ -698,6 +705,7 @@ export interface EventHandlerDeps {
698
705
  export function createEventHandlerState(): EventHandlerState {
699
706
  return {
700
707
  pendingNotifiedInferenceProfile: null,
708
+ autoRoutedProfile: undefined,
701
709
  pendingDirectiveDisplayBuffer: "",
702
710
  firstAssistantText: "",
703
711
  exchangeProviderName: undefined,
@@ -3305,6 +3313,9 @@ export async function handleMessageComplete(
3305
3313
  ...(event.assistantTextVisibility
3306
3314
  ? { [ASSISTANT_TEXT_VISIBILITY_KEY]: event.assistantTextVisibility }
3307
3315
  : {}),
3316
+ ...(state.autoRoutedProfile
3317
+ ? { [AUTO_ROUTED_PROFILE_METADATA_KEY]: state.autoRoutedProfile }
3318
+ : {}),
3308
3319
  };
3309
3320
  state.lastAssistantTextVisibility = event.assistantTextVisibility;
3310
3321
  const persisted = await finalizeInflightContent(
@@ -105,6 +105,7 @@ import {
105
105
  type AutoProfileRoute,
106
106
  plainTextOf,
107
107
  routeAutoProfile,
108
+ takeAutoProfilePreview,
108
109
  } from "./auto-profile-router.js";
109
110
  import { conversationSupportsDynamicUi } from "./channel-ui-capability.js";
110
111
  import type { Conversation } from "./conversation.js";
@@ -688,21 +689,33 @@ export async function runAgentLoopImpl(
688
689
  if (profiles[AUTO_PROFILE_KEY]?.source !== "managed") {
689
690
  return null;
690
691
  }
691
- const route = await routeAutoProfile({
692
+ const userMessage = plainTextOf(
693
+ getMessageById(userMessageId)?.content ?? [],
694
+ );
695
+ // A draft the composer already previewed carries its pick over, so
696
+ // the reply lands on the profile the pill showed with no second call.
697
+ const previewed = takeAutoProfilePreview({
692
698
  conversationId: ctx.conversationId,
699
+ text: userMessage,
693
700
  history: ctx.messages,
694
- userMessage: plainTextOf(
695
- getMessageById(userMessageId)?.content ?? [],
696
- ),
697
701
  profiles,
698
- signal: abortController.signal,
699
702
  });
703
+ const route =
704
+ previewed ??
705
+ (await routeAutoProfile({
706
+ conversationId: ctx.conversationId,
707
+ history: ctx.messages,
708
+ userMessage,
709
+ profiles,
710
+ signal: abortController.signal,
711
+ }));
700
712
  rlog.info(
701
713
  {
702
714
  outcome: route.outcome,
703
715
  profile: route.profile,
704
716
  confidence: route.confidence ?? null,
705
717
  latencyMs: route.latencyMs,
718
+ reusedPreview: previewed !== undefined,
706
719
  },
707
720
  "Auto profile routed the turn",
708
721
  );
@@ -963,6 +976,7 @@ export async function runAgentLoopImpl(
963
976
  startToolProfilingRequest(ctx.conversationId);
964
977
  let turnStarted = false;
965
978
  const state = createEventHandlerState();
979
+ state.autoRoutedProfile = autoRoute?.profile;
966
980
  // Publish this turn's flushed-content watermark so the worker → daemon
967
981
  // persist hand-off can cap a snapshot anchor at flushed content rather than
968
982
  // the live seq counter, which runs ahead while the turn streams. Cleared in
@@ -2240,6 +2254,9 @@ export async function runAgentLoopImpl(
2240
2254
  ...(state.lastAssistantMessageId
2241
2255
  ? { messageId: state.lastAssistantMessageId }
2242
2256
  : {}),
2257
+ ...(state.autoRoutedProfile
2258
+ ? { autoRoutedProfile: state.autoRoutedProfile }
2259
+ : {}),
2243
2260
  modeSession: ctx.modeSessions.getTurnOwner(reqId),
2244
2261
  });
2245
2262
  publishLoopMessagesChanged();
@@ -2268,6 +2285,9 @@ export async function runAgentLoopImpl(
2268
2285
  ...(state.lastAssistantTextVisibility
2269
2286
  ? { assistantTextVisibility: state.lastAssistantTextVisibility }
2270
2287
  : {}),
2288
+ ...(state.autoRoutedProfile
2289
+ ? { autoRoutedProfile: state.autoRoutedProfile }
2290
+ : {}),
2271
2291
  });
2272
2292
  if (shouldEmitQueuedConversationNotices) {
2273
2293
  for (const notice of drainConversationNotices(ctx.conversationId)) {
@@ -167,6 +167,14 @@ export function isSystemCardMetadata(
167
167
  */
168
168
  export const NO_RESPONSE_MESSAGE_KIND = "no_response";
169
169
 
170
+ /**
171
+ * Metadata key on an assistant row from a turn that ran on the Auto profile:
172
+ * the default profile key the router picked for the turn. Projected to
173
+ * `ConversationMessage.autoRoutedProfile` so clients can show which profile
174
+ * answered. Absent on every other row.
175
+ */
176
+ export const AUTO_ROUTED_PROFILE_METADATA_KEY = "autoRoutedProfile";
177
+
170
178
  /**
171
179
  * Shared predicate for the deliberate-silence marker, mirroring
172
180
  * {@link isSystemCardMetadata} so display merging, transcript rendering, and
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Auto profile routes.
3
+ *
4
+ * POST /v1/auto-profile/preview — which default profile the Auto profile
5
+ * would pick for a draft the user has not sent yet.
6
+ */
7
+ import { z } from "zod";
8
+
9
+ import { previewAutoProfile } from "../../daemon/auto-profile-router.js";
10
+ import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
11
+ import { BadRequestError } from "./errors.js";
12
+ import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
13
+
14
+ const MAX_DRAFT_CHARS = 20_000;
15
+
16
+ const previewRequestSchema = z.object({
17
+ text: z.string().min(1).max(MAX_DRAFT_CHARS),
18
+ conversationId: z
19
+ .string()
20
+ .describe(
21
+ "Conversation the draft belongs to, for recent history. Omit for a new chat.",
22
+ )
23
+ .optional(),
24
+ });
25
+
26
+ const previewResponseSchema = z.object({
27
+ /** The default profile key the draft would run on; null when the Auto
28
+ * profile is not available on this install. */
29
+ profile: z.string().nullable(),
30
+ outcome: z.enum(["routed", "unavailable", "timeout", "error", "fallback"]),
31
+ confidence: z.number().optional(),
32
+ });
33
+
34
+ async function handlePreviewAutoProfile(
35
+ args: RouteHandlerArgs,
36
+ ): Promise<z.infer<typeof previewResponseSchema>> {
37
+ const parsed = previewRequestSchema.safeParse(args.body ?? {});
38
+ if (!parsed.success) {
39
+ throw new BadRequestError(
40
+ `text must be a non-empty string of at most ${MAX_DRAFT_CHARS} characters`,
41
+ );
42
+ }
43
+ // The composer aborts a preview the moment the draft changes; carrying the
44
+ // abort through stops the retired Jev call and keeps its late answer out
45
+ // of the reuse cache.
46
+ const route = await previewAutoProfile({
47
+ text: parsed.data.text,
48
+ ...(parsed.data.conversationId
49
+ ? { conversationId: parsed.data.conversationId }
50
+ : {}),
51
+ ...(args.abortSignal ? { signal: args.abortSignal } : {}),
52
+ });
53
+ if (!route) {
54
+ return { profile: null, outcome: "unavailable" };
55
+ }
56
+ return {
57
+ profile: route.profile,
58
+ outcome: route.outcome,
59
+ ...(route.confidence !== undefined ? { confidence: route.confidence } : {}),
60
+ };
61
+ }
62
+
63
+ export const ROUTES: RouteDefinition[] = [
64
+ {
65
+ operationId: "previewAutoProfile",
66
+ endpoint: "auto-profile/preview",
67
+ method: "POST",
68
+ policy: {
69
+ requiredScopes: ["chat.write"],
70
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
71
+ },
72
+ summary: "Preview the Auto profile's pick for a draft",
73
+ description:
74
+ "Ask the Auto profile which default profile a draft message would run on, " +
75
+ "using the conversation's recent history when one is given. The pick is " +
76
+ "kept briefly so the turn that sends the same text reuses it. Spends one " +
77
+ "Jev call per request.",
78
+ tags: ["conversations"],
79
+ requestBody: previewRequestSchema,
80
+ responseBody: previewResponseSchema,
81
+ handler: handlePreviewAutoProfile,
82
+ },
83
+ ];
@@ -142,6 +142,7 @@ import {
142
142
  import { listConversationModeSessionsByIds } from "../../persistence/conversation-mode-sessions.js";
143
143
  import { searchConversations } from "../../persistence/conversation-queries.js";
144
144
  import {
145
+ AUTO_ROUTED_PROFILE_METADATA_KEY,
145
146
  computerUseScreenshotAttachmentIdsFromMetadata,
146
147
  isNoResponseMetadata,
147
148
  messageMetadataIsAmbientSightKeep,
@@ -1110,6 +1111,7 @@ export async function handleListMessages({
1110
1111
  let backgroundToolCompletion: ConversationMessage["backgroundToolCompletion"];
1111
1112
  let systemCard: boolean | undefined;
1112
1113
  let noResponse: boolean | undefined;
1114
+ let autoRoutedProfile: string | undefined;
1113
1115
  const cameraFrame = messageMetadataIsAmbientSightKeep(msg.metadata)
1114
1116
  ? true
1115
1117
  : undefined;
@@ -1130,6 +1132,9 @@ export async function handleListMessages({
1130
1132
  if (isNoResponseMetadata(meta)) {
1131
1133
  noResponse = true;
1132
1134
  }
1135
+ if (typeof meta[AUTO_ROUTED_PROFILE_METADATA_KEY] === "string") {
1136
+ autoRoutedProfile = meta[AUTO_ROUTED_PROFILE_METADATA_KEY];
1137
+ }
1133
1138
  // Channel facts live in the row's provider envelope. A reaction row,
1134
1139
  // either direction, projects its structured fact so clients never
1135
1140
  // render the stored "[reaction]" sentinel; a row deleted on its
@@ -1212,6 +1217,7 @@ export async function handleListMessages({
1212
1217
  backgroundToolCompletion,
1213
1218
  systemCard,
1214
1219
  noResponse,
1220
+ autoRoutedProfile,
1215
1221
  cameraFrame,
1216
1222
  reaction,
1217
1223
  providerError,
@@ -1451,6 +1457,9 @@ export async function handleListMessages({
1451
1457
  : {}),
1452
1458
  ...(m.systemCard ? { systemCard: true } : {}),
1453
1459
  ...(m.noResponse ? { noResponse: true } : {}),
1460
+ ...(m.autoRoutedProfile
1461
+ ? { autoRoutedProfile: m.autoRoutedProfile }
1462
+ : {}),
1454
1463
  ...(m.cameraFrame ? { cameraFrame: true as const } : {}),
1455
1464
  // The row's own marker, so a client gates per-row presentation on what
1456
1465
  // this row was written with rather than on the live flag.
@@ -25,6 +25,7 @@ import { ROUTES as ATTACHMENT_ROUTES } from "./attachment-routes.js";
25
25
  import { ROUTES as AUDIO_ROUTES } from "./audio-routes.js";
26
26
  import { ROUTES as AUDIT_ROUTES } from "./audit-routes.js";
27
27
  import { ROUTES as AUTH_ROUTES } from "./auth-routes.js";
28
+ import { ROUTES as AUTO_PROFILE_ROUTES } from "./auto-profile-routes.js";
28
29
  import { ROUTES as AVATAR_ROUTES } from "./avatar-routes.js";
29
30
  import { ROUTES as BACKGROUND_TOOL_ROUTES } from "./background-tool-routes.js";
30
31
  import { ROUTES as BACKGROUND_WAKE_ROUTES } from "./background-wake-routes.js";
@@ -217,6 +218,7 @@ export const ROUTES: RouteDefinition[] = [
217
218
  ...CONVERSATION_QUERY_ROUTES,
218
219
  ...CONVERSATION_STARTER_ROUTES,
219
220
  ...ACTIVATION_ROUTES,
221
+ ...AUTO_PROFILE_ROUTES,
220
222
  ...DEBUG_ROUTES,
221
223
  ...DEFAULT_PROVIDER_ROUTES,
222
224
  ...DESKTOP_SETUP_ROUTES,