@vellumai/assistant 0.11.1-staging.1 → 0.11.1-staging.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 (33) hide show
  1. package/openapi.yaml +7 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/agent-loop-override-profile.test.ts +21 -14
  4. package/src/__tests__/config-callsite-patch-merge.test.ts +92 -0
  5. package/src/__tests__/config-loader-backfill.test.ts +10 -10
  6. package/src/__tests__/drain-requeue-on-contention.test.ts +124 -0
  7. package/src/__tests__/managed-profile-guard.test.ts +2 -4
  8. package/src/__tests__/queued-message-delete-contract.test.ts +339 -0
  9. package/src/__tests__/worker-entrypoint-guards.test.ts +39 -0
  10. package/src/api/events/message-requeued.ts +35 -0
  11. package/src/api/index.ts +6 -0
  12. package/src/config/__tests__/default-profile-catalog.test.ts +9 -0
  13. package/src/config/default-profile-catalog.ts +1 -1
  14. package/src/daemon/conversation-process.ts +77 -12
  15. package/src/daemon/conversation-queue-manager.ts +19 -0
  16. package/src/daemon/handlers/conversations.ts +77 -9
  17. package/src/daemon/shutdown-handlers.ts +20 -7
  18. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +755 -0
  19. package/src/persistence/embeddings/embedding-backend.ts +109 -0
  20. package/src/persistence/embeddings/embedding-local.ts +677 -135
  21. package/src/persistence/embeddings/embedding-types.ts +19 -0
  22. package/src/plugin-api/vision-support.test.ts +11 -10
  23. package/src/plugins/defaults/memory/worker.ts +54 -10
  24. package/src/providers/__tests__/preflight-resolved-config.test.ts +25 -0
  25. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +108 -1
  26. package/src/providers/connection-resolution.ts +61 -1
  27. package/src/providers/inference/connection-availability.ts +13 -1
  28. package/src/providers/inference/connections.ts +44 -3
  29. package/src/providers/routing-identity.ts +2 -1
  30. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +4 -4
  31. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +57 -4
  32. package/src/runtime/routes/conversation-query-routes.ts +38 -5
  33. package/src/runtime/routes/inference-provider-connection-routes.ts +27 -3
package/openapi.yaml CHANGED
@@ -20459,7 +20459,9 @@ paths:
20459
20459
  delete:
20460
20460
  operationId: messages_queued_by_id_delete
20461
20461
  summary: Delete a queued message
20462
- description: Remove a pending message from the conversation queue before it is processed.
20462
+ description:
20463
+ Remove a pending message from the conversation queue before it is processed. Broadcasts
20464
+ `message_queued_deleted` so every client can close out the pending row.
20463
20465
  tags:
20464
20466
  - messages
20465
20467
  parameters:
@@ -20477,6 +20479,10 @@ paths:
20477
20479
  responses:
20478
20480
  "200":
20479
20481
  description: Successful response
20482
+ "403":
20483
+ description: The queued message was enqueued by a different actor principal.
20484
+ "404":
20485
+ description: Conversation or queued message not found.
20480
20486
  /v1/messages/queued/{id}/steer:
20481
20487
  post:
20482
20488
  operationId: messages_queued_by_id_steer_post
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.1-staging.1",
3
+ "version": "0.11.1-staging.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -276,21 +276,28 @@ import { VELLUM_MANAGED_CONNECTION_NAME } from "../providers/vellum-model-routin
276
276
  // Connection-aware resolver path: satisfy
277
277
  // `tryResolveProviderForConnectionName` lookups so resolveDefaultProvider
278
278
  // returns a usable provider for any connection name the winning profile
279
- // references. The managed connection must carry the provider the catalog
280
- // `balanced` default declares (fireworks) or resolution rejects the row as a
281
- // provider mismatch; other names behave as personal anthropic connections.
279
+ // references. The managed connection must be the platform-auth sentinel row:
280
+ // a managed profile routing through anything else resolves to the platform
281
+ // instead. Other names behave as personal anthropic connections.
282
282
  mock.module("../providers/inference/connections.js", () => ({
283
- getConnection: (_db: unknown, name: string) => ({
284
- id: 1,
285
- name,
286
- provider:
287
- name === VELLUM_MANAGED_CONNECTION_NAME ? "fireworks" : "anthropic",
288
- auth_strategy: "user_managed_credential",
289
- credential_alias: null,
290
- metadata_json: null,
291
- created_at: new Date().toISOString(),
292
- updated_at: new Date().toISOString(),
293
- }),
283
+ getConnection: (_db: unknown, name: string) =>
284
+ name === VELLUM_MANAGED_CONNECTION_NAME
285
+ ? {
286
+ id: 1,
287
+ name,
288
+ provider: "vellum",
289
+ auth: { type: "platform" },
290
+ }
291
+ : {
292
+ id: 1,
293
+ name,
294
+ provider: "anthropic",
295
+ auth_strategy: "user_managed_credential",
296
+ credential_alias: null,
297
+ metadata_json: null,
298
+ created_at: new Date().toISOString(),
299
+ updated_at: new Date().toISOString(),
300
+ },
294
301
  }));
295
302
 
296
303
  /**
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Pins the `deepMergeOverwrite` behaviour that the Action Overrides editor
3
+ * depends on when it builds an `llm.callSites` patch (LUM-2949).
4
+ *
5
+ * A persisted call-site entry may carry tuning the web editor renders no
6
+ * control for (`effort`, `thinking`, `maxTokens`, ...). The editor decides
7
+ * per entry whether to send the picker triple, send `null`, or omit the key
8
+ * entirely, and each of those choices is only correct because of how this
9
+ * merge treats it. Asserting on the patch body alone would not catch a
10
+ * change here, so the contract is pinned where it lives.
11
+ */
12
+
13
+ import { describe, expect, test } from "bun:test";
14
+
15
+ import { deepMergeOverwrite } from "../config/loader.js";
16
+
17
+ type Raw = Record<string, unknown>;
18
+
19
+ function callSites(entries: Raw): Raw {
20
+ return { llm: { callSites: entries } };
21
+ }
22
+
23
+ function mergedCallSites(before: Raw, patch: Raw): Raw {
24
+ const raw = callSites(before);
25
+ deepMergeOverwrite(raw, callSites(patch));
26
+ return (raw.llm as Raw).callSites as Raw;
27
+ }
28
+
29
+ const TUNED = {
30
+ profile: "latency-optimized",
31
+ effort: "low",
32
+ thinking: { enabled: false },
33
+ };
34
+
35
+ describe("llm.callSites patch merge", () => {
36
+ test("an omitted key keeps its persisted value", () => {
37
+ // What the editor sends for an active row: the picker triple only.
38
+ const after = mergedCallSites(
39
+ { voiceFrontDoor: TUNED },
40
+ { voiceFrontDoor: { profile: "balanced", provider: null, model: null } },
41
+ );
42
+ expect(after.voiceFrontDoor).toEqual({
43
+ profile: "balanced",
44
+ effort: "low",
45
+ thinking: { enabled: false },
46
+ });
47
+ });
48
+
49
+ test("a null on an absent key is a no-op, not a write", () => {
50
+ const after = mergedCallSites(
51
+ { voiceFrontDoor: TUNED },
52
+ { heartbeatAgent: null },
53
+ );
54
+ expect(after).toEqual({ voiceFrontDoor: TUNED });
55
+ });
56
+
57
+ test("a null deletes the whole entry, tuning included", () => {
58
+ // Why the editor must not send `null` for a row the user left alone:
59
+ // an entry holding only tuning would be erased.
60
+ const after = mergedCallSites(
61
+ { tuningOnly: { effort: "low", thinking: { enabled: false } } },
62
+ { tuningOnly: null },
63
+ );
64
+ expect("tuningOnly" in after).toBe(false);
65
+ });
66
+
67
+ test("omitting a tuning-only entry leaves it untouched", () => {
68
+ const before = {
69
+ tuningOnly: { effort: "low", thinking: { enabled: false } },
70
+ };
71
+ const after = mergedCallSites(before, {
72
+ heartbeatAgent: { profile: "balanced", provider: null, model: null },
73
+ });
74
+ expect(after.tuningOnly).toEqual({
75
+ effort: "low",
76
+ thinking: { enabled: false },
77
+ });
78
+ });
79
+
80
+ test("an explicit null clears a scalar that is present", () => {
81
+ // The picker triple relies on this to drop a stale provider/model pin.
82
+ const after = mergedCallSites(
83
+ { pinned: { provider: "anthropic", model: "claude-fable-5" } },
84
+ { pinned: { profile: "balanced", provider: null, model: null } },
85
+ );
86
+ expect(after.pinned).toEqual({
87
+ profile: "balanced",
88
+ provider: null,
89
+ model: null,
90
+ });
91
+ });
92
+ });
@@ -650,7 +650,7 @@ describe("loadConfig startup behavior", () => {
650
650
  expect(raw.llm.profiles).toEqual({});
651
651
  // Default content resolves from the code catalog via the effective view.
652
652
  const effectiveBalanced = getEffectiveProfile(raw.llm.profiles, "balanced");
653
- expect(effectiveBalanced?.model).toBe("accounts/fireworks/models/glm-5p2");
653
+ expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
654
654
  expect(effectiveBalanced?.provider).toBe("vellum");
655
655
  expect(effectiveBalanced?.provider_connection).toBeUndefined();
656
656
  });
@@ -978,7 +978,7 @@ describe("loadConfig startup behavior", () => {
978
978
  // Resolution ignores the drifted body: a managed-source entry contributes
979
979
  // only label/status/topP, everything else comes from the catalog.
980
980
  const effectiveBalanced = getEffectiveProfile(raw.llm.profiles, "balanced");
981
- expect(effectiveBalanced?.model).toBe("accounts/fireworks/models/glm-5p2");
981
+ expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
982
982
  expect(effectiveBalanced?.provider_connection).toBeUndefined();
983
983
  });
984
984
 
@@ -1058,7 +1058,7 @@ describe("loadConfig startup behavior", () => {
1058
1058
  expect(raw.llm.profiles.balanced).toEqual(drifted);
1059
1059
  expect(raw.llm.activeProfile).toBe("balanced");
1060
1060
  const effectiveBalanced = getEffectiveProfile(raw.llm.profiles, "balanced");
1061
- expect(effectiveBalanced?.model).toBe("accounts/fireworks/models/glm-5p2");
1061
+ expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1062
1062
  expect(effectiveBalanced?.maxTokens).toBe(32000);
1063
1063
  expect(effectiveBalanced?.provider_connection).toBeUndefined();
1064
1064
  // The catalog body carries no topP and the entry has none, so the
@@ -1090,7 +1090,7 @@ describe("loadConfig startup behavior", () => {
1090
1090
  expect(raw.llm.profiles.balanced).toEqual(edited);
1091
1091
  const effectiveBalanced = getEffectiveProfile(raw.llm.profiles, "balanced");
1092
1092
  // Content is served from the catalog...
1093
- expect(effectiveBalanced?.model).toBe("accounts/fireworks/models/glm-5p2");
1093
+ expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1094
1094
  // ...with the user's label and status overlaid.
1095
1095
  expect(effectiveBalanced?.label).toBe("My Default");
1096
1096
  expect(effectiveBalanced?.status).toBe("disabled");
@@ -1115,7 +1115,7 @@ describe("loadConfig startup behavior", () => {
1115
1115
  expect(raw.llm.profiles.balanced).toEqual(stub);
1116
1116
  const effectiveBalanced = getEffectiveProfile(raw.llm.profiles, "balanced");
1117
1117
  expect(effectiveBalanced?.label).toBe("My Default");
1118
- expect(effectiveBalanced?.model).toBe("accounts/fireworks/models/glm-5p2");
1118
+ expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1119
1119
  });
1120
1120
 
1121
1121
  test("off-platform boot preserves user-toggled status on a managed stub", () => {
@@ -1137,7 +1137,7 @@ describe("loadConfig startup behavior", () => {
1137
1137
  expect(effectiveBalanced?.status).toBe("disabled");
1138
1138
  // Content still comes from the catalog — only label/status/topP are
1139
1139
  // workspace-owned.
1140
- expect(effectiveBalanced?.model).toBe("accounts/fireworks/models/glm-5p2");
1140
+ expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1141
1141
  });
1142
1142
 
1143
1143
  test("boot preserves a user-edited topP override on a managed stub", () => {
@@ -1161,7 +1161,7 @@ describe("loadConfig startup behavior", () => {
1161
1161
  expect(effectiveBalanced?.topP).toBe(0.5);
1162
1162
  // Content still comes from the catalog — topP is workspace-owned, the
1163
1163
  // rest is code-owned.
1164
- expect(effectiveBalanced?.model).toBe("accounts/fireworks/models/glm-5p2");
1164
+ expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1165
1165
  });
1166
1166
 
1167
1167
  test("effective balanced profile carries no topP override by default", () => {
@@ -1216,7 +1216,7 @@ describe("loadConfig startup behavior", () => {
1216
1216
  expect(raw.llm.profiles.balanced).toBeUndefined();
1217
1217
  const effectiveBalanced = getEffectiveProfile(raw.llm.profiles, "balanced");
1218
1218
  expect(effectiveBalanced?.label).toBe("Balanced");
1219
- expect(effectiveBalanced?.model).toBe("accounts/fireworks/models/glm-5p2");
1219
+ expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1220
1220
  // Status is unset — the default resolves active.
1221
1221
  expect("status" in (effectiveBalanced ?? {})).toBe(false);
1222
1222
  });
@@ -1295,7 +1295,7 @@ describe("loadConfig startup behavior", () => {
1295
1295
  // overlay boot. The overlay-set label is what shows through the
1296
1296
  // effective view.
1297
1297
  expect(mainAgentConfig.provider).toBe("vellum");
1298
- expect(mainAgentConfig.model).toBe("accounts/fireworks/models/glm-5p2");
1298
+ expect(mainAgentConfig.model).toBe("gpt-5.6-luna");
1299
1299
 
1300
1300
  const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
1301
1301
  expect(raw.llm.profiles.balanced).toEqual({
@@ -1326,7 +1326,7 @@ describe("loadConfig startup behavior", () => {
1326
1326
  );
1327
1327
  expect(effectiveBalanced?.provider).toBe("vellum");
1328
1328
  expect(effectiveBalanced?.provider_connection).toBeUndefined();
1329
- expect(effectiveBalanced?.model).toBe("accounts/fireworks/models/glm-5p2");
1329
+ expect(effectiveBalanced?.model).toBe("gpt-5.6-luna");
1330
1330
  expect(effectiveBalanced?.maxTokens).toBe(32000);
1331
1331
  expect(effectiveBalanced?.thinking).toEqual({
1332
1332
  enabled: true,
@@ -19,12 +19,17 @@ import {
19
19
  interface FakeEvent {
20
20
  type: string;
21
21
  message?: string;
22
+ conversationId?: string;
23
+ requestId?: string;
24
+ position?: number;
25
+ clientMessageId?: string;
22
26
  }
23
27
 
24
28
  function makeQueued(
25
29
  content: string,
26
30
  requestId: string,
27
31
  events: FakeEvent[],
32
+ extra?: Partial<QueuedMessage>,
28
33
  ): QueuedMessage {
29
34
  return {
30
35
  content,
@@ -34,6 +39,7 @@ function makeQueued(
34
39
  events.push(event);
35
40
  },
36
41
  sentAt: Date.now(),
42
+ ...extra,
37
43
  } as unknown as QueuedMessage;
38
44
  }
39
45
 
@@ -200,3 +206,121 @@ describe("drainQueue under processing-lock contention", () => {
200
206
  ).toBe(1);
201
207
  });
202
208
  });
209
+
210
+ /**
211
+ * The corrective `message_requeued` event.
212
+ *
213
+ * A drain announces `message_dequeued` before the steps that can send the
214
+ * message back, so a client that cleared its pending indicator on that
215
+ * announcement needs to be told the message is queued again. The rule is
216
+ * announcement-scoped: a requeue that happens before the announcement owes
217
+ * clients nothing, because they never stopped showing the row as queued.
218
+ */
219
+ describe("message_requeued corrective event", () => {
220
+ test("a busy persist tells the sender its announced message is queued again", async () => {
221
+ const events: FakeEvent[] = [];
222
+ const { conversation, queue } = makeFakeConversation({
223
+ persistError: new Error(CONVERSATION_BUSY_MESSAGE),
224
+ });
225
+ queue.push(
226
+ makeQueued("hello there", "r1", events, {
227
+ clientMessageId: "nonce-1",
228
+ }),
229
+ );
230
+ queue.push(makeQueued("and another", "r2", events));
231
+
232
+ await drainQueue(conversation as never);
233
+
234
+ expect(events.map((event) => event.type)).toEqual([
235
+ "message_dequeued",
236
+ "message_requeued",
237
+ ]);
238
+ expect(events[1]).toEqual({
239
+ type: "message_requeued",
240
+ conversationId: "conv-drain-requeue",
241
+ requestId: "r1",
242
+ // Back at the head, so 1 of the 2 visible queued items.
243
+ position: 1,
244
+ clientMessageId: "nonce-1",
245
+ });
246
+ });
247
+
248
+ test("a requeue before the dequeue announcement stays silent", async () => {
249
+ const events: FakeEvent[] = [];
250
+ const { conversation, queue } = makeFakeConversation({ processing: true });
251
+ queue.push(makeQueued("hello there", "r1", events));
252
+
253
+ await drainQueue(conversation as never);
254
+
255
+ // The early lock check requeues before announcing anything, so a
256
+ // corrective event here would contradict what the client is showing.
257
+ expect(events).toEqual([]);
258
+ expect(queue.length).toBe(1);
259
+ });
260
+
261
+ test("only the batch member whose dequeue was announced is corrected", async () => {
262
+ const headEvents: FakeEvent[] = [];
263
+ const tailEvents: FakeEvent[] = [];
264
+ const { conversation, queue } = makeFakeConversation({
265
+ persistError: new Error(CONVERSATION_BUSY_MESSAGE),
266
+ });
267
+ queue.push(makeQueued("hello there", "r1", headEvents));
268
+ queue.push(makeQueued("and another", "r2", tailEvents));
269
+
270
+ await drainQueue(conversation as never);
271
+
272
+ // The batch drain announces per member as it walks the batch, and the
273
+ // head's busy persist ends the walk before the tail is announced.
274
+ expect(headEvents.map((event) => event.type)).toEqual([
275
+ "message_dequeued",
276
+ "message_requeued",
277
+ ]);
278
+ expect(tailEvents).toEqual([]);
279
+ expect(queue.peek(0)?.requestId).toBe("r1");
280
+ expect(queue.peek(1)?.requestId).toBe("r2");
281
+ });
282
+
283
+ test("a hidden send is announced but never corrected", async () => {
284
+ const events: FakeEvent[] = [];
285
+ const { conversation, queue } = makeFakeConversation({
286
+ persistError: new Error(CONVERSATION_BUSY_MESSAGE),
287
+ });
288
+ queue.push(
289
+ makeQueued("wizard closed", "r1", events, {
290
+ metadata: { hidden: true },
291
+ }),
292
+ );
293
+
294
+ await drainQueue(conversation as never);
295
+
296
+ // Hidden sends get no queued ack and render no client row, so there is
297
+ // nothing for a corrective event to restore.
298
+ expect(events.some((event) => event.type === "message_requeued")).toBe(
299
+ false,
300
+ );
301
+ expect(queue.peek(0)?.requestId).toBe("r1");
302
+ });
303
+
304
+ test("a second contention round corrects the message again", async () => {
305
+ const events: FakeEvent[] = [];
306
+ const { conversation, queue } = makeFakeConversation({
307
+ persistErrors: [
308
+ new Error(CONVERSATION_BUSY_MESSAGE),
309
+ new Error(CONVERSATION_BUSY_MESSAGE),
310
+ ],
311
+ });
312
+ queue.push(makeQueued("hello there", "r1", events));
313
+
314
+ await drainQueue(conversation as never);
315
+ await drainQueue(conversation as never);
316
+
317
+ // Each round re-announces the dequeue, so each round owes its own
318
+ // correction: the flag settles per announcement, not once per message.
319
+ expect(events.map((event) => event.type)).toEqual([
320
+ "message_dequeued",
321
+ "message_requeued",
322
+ "message_dequeued",
323
+ "message_requeued",
324
+ ]);
325
+ });
326
+ });
@@ -1067,7 +1067,7 @@ describe("code-owned default profiles — wire view and write normalization", ()
1067
1067
  });
1068
1068
  const response = (await getRoute.handler({})) as Record<string, any>;
1069
1069
  const wireBalanced = response.llm.profiles.balanced;
1070
- expect(wireBalanced.model).toBe("accounts/fireworks/models/glm-5p2");
1070
+ expect(wireBalanced.model).toBe("gpt-5.6-luna");
1071
1071
  expect(wireBalanced.provider).toBe("vellum");
1072
1072
  expect(wireBalanced.provider_connection).toBeUndefined();
1073
1073
  expect(wireBalanced.status).toBe("disabled");
@@ -1314,9 +1314,7 @@ describe("code-owned default profiles — echoes over stale on-disk bodies", ()
1314
1314
  });
1315
1315
  const response = (await getRoute.handler({})) as Record<string, any>;
1316
1316
  // The wire view serves catalog content, not the stale body.
1317
- expect(response.llm.profiles.balanced.model).toBe(
1318
- "accounts/fireworks/models/glm-5p2",
1319
- );
1317
+ expect(response.llm.profiles.balanced.model).toBe("gpt-5.6-luna");
1320
1318
  const result = await patchRoute.handler({
1321
1319
  body: { llm: { profiles: response.llm.profiles } },
1322
1320
  });