@vellumai/assistant 0.10.6-staging.1 → 0.10.6

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 (58) hide show
  1. package/openapi.yaml +43 -6
  2. package/package.json +1 -1
  3. package/src/__tests__/attachment-stored-path-annotation.test.ts +151 -0
  4. package/src/__tests__/attachments-store.test.ts +37 -1
  5. package/src/__tests__/attachments.test.ts +53 -0
  6. package/src/__tests__/channel-reply-delivery.test.ts +214 -5
  7. package/src/__tests__/config-loader-backfill.test.ts +16 -36
  8. package/src/__tests__/image-source-path-reinject.test.ts +155 -14
  9. package/src/__tests__/managed-profile-guard.test.ts +2 -0
  10. package/src/__tests__/mtime-cache.test.ts +265 -80
  11. package/src/__tests__/plugin-config-data-migration.test.ts +2 -4
  12. package/src/__tests__/plugin-effective-enabled-set.test.ts +9 -6
  13. package/src/__tests__/workspace-migration-125-repoint-managed-connections-to-vellum.test.ts +140 -0
  14. package/src/agent/attachments.ts +55 -13
  15. package/src/cli/commands/platform/__tests__/credits.test.ts +111 -0
  16. package/src/cli/commands/platform/index.ts +86 -4
  17. package/src/cli/lib/__tests__/confirm-prompt.test.ts +9 -9
  18. package/src/cli/lib/diff-plugin.ts +7 -3
  19. package/src/cli/lib/inspect-plugin.ts +28 -10
  20. package/src/cli/lib/install-from-github.ts +69 -24
  21. package/src/cli/lib/ipc-params.ts +3 -3
  22. package/src/cli/lib/list-installed-plugins.ts +11 -5
  23. package/src/cli/lib/merge-plugin-tree.ts +15 -9
  24. package/src/cli/lib/plugin-fingerprint.ts +0 -6
  25. package/src/cli/lib/publish-plugin.ts +1 -1
  26. package/src/cli/lib/unknown-command.ts +3 -1
  27. package/src/cli/lib/upgrade-plugin.ts +2 -5
  28. package/src/config/__tests__/sync-gated-profiles.test.ts +2 -2
  29. package/src/config/seed-inference-profiles.ts +5 -4
  30. package/src/daemon/conversation-lifecycle.ts +37 -15
  31. package/src/daemon/conversation-messaging.ts +56 -28
  32. package/src/daemon/conversation.ts +2 -2
  33. package/src/heartbeat/__tests__/heartbeat-service.test.ts +6 -0
  34. package/src/hooks/__tests__/hook-live-reload.test.ts +110 -96
  35. package/src/hooks/hook-loader.ts +57 -144
  36. package/src/hooks/registry.ts +13 -9
  37. package/src/persistence/attachments-store.ts +2 -1
  38. package/src/persistence/conversation-crud.ts +24 -0
  39. package/src/plugin-api/types.ts +4 -1
  40. package/src/plugins/mtime-cache.ts +347 -33
  41. package/src/plugins/pipeline.ts +3 -2
  42. package/src/providers/__tests__/inference.test.ts +42 -31
  43. package/src/providers/inference/__tests__/connections-label.test.ts +9 -28
  44. package/src/providers/inference/auth.ts +1 -2
  45. package/src/providers/inference/backfill.ts +7 -5
  46. package/src/providers/inference/connections.ts +54 -30
  47. package/src/runtime/channel-reply-delivery.ts +83 -24
  48. package/src/runtime/channel-retry-sweep.ts +4 -0
  49. package/src/runtime/{slack-no-response.ts → no-response.ts} +26 -6
  50. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +43 -3
  51. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +61 -24
  52. package/src/runtime/routes/conversation-query-routes.ts +18 -1
  53. package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -1
  54. package/src/runtime/routes/inference-provider-connection-routes.ts +4 -3
  55. package/src/runtime/routes/platform-routes.ts +82 -1
  56. package/src/runtime/slack-reply-session.ts +1 -1
  57. package/src/workspace/migrations/125-repoint-managed-connections-to-vellum.ts +99 -0
  58. package/src/workspace/migrations/registry.ts +2 -0
@@ -596,9 +596,7 @@ describe("loadConfig startup behavior", () => {
596
596
  expect(config.llm.profiles.balanced?.model).toBe(
597
597
  "accounts/fireworks/models/glm-5p2",
598
598
  );
599
- expect(config.llm.profiles.balanced?.provider_connection).toBe(
600
- "fireworks-managed",
601
- );
599
+ expect(config.llm.profiles.balanced?.provider_connection).toBe("vellum");
602
600
 
603
601
  const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
604
602
  expect(raw.llm.default).toEqual({
@@ -639,9 +637,7 @@ describe("loadConfig startup behavior", () => {
639
637
  expect(config.llm.profiles.balanced?.model).toBe(
640
638
  "accounts/fireworks/models/glm-5p2",
641
639
  );
642
- expect(config.llm.profiles.balanced?.provider_connection).toBe(
643
- "fireworks-managed",
644
- );
640
+ expect(config.llm.profiles.balanced?.provider_connection).toBe("vellum");
645
641
  // No user profiles created on platform.
646
642
  expect(config.llm.profiles["custom-balanced"]).toBeUndefined();
647
643
  });
@@ -684,9 +680,7 @@ describe("loadConfig startup behavior", () => {
684
680
  );
685
681
  // Managed balanced profile is seeded for fireworks-managed (GLM 5.2).
686
682
  expect(raw.llm.profiles.balanced.provider).toBe("fireworks");
687
- expect(raw.llm.profiles.balanced.provider_connection).toBe(
688
- "fireworks-managed",
689
- );
683
+ expect(raw.llm.profiles.balanced.provider_connection).toBe("vellum");
690
684
  });
691
685
 
692
686
  test("on-platform re-hatch resets active profile to balanced", () => {
@@ -720,9 +714,7 @@ describe("loadConfig startup behavior", () => {
720
714
  // On-platform: no user profiles created, active resets to managed balanced.
721
715
  expect(raw.llm.activeProfile).toBe("balanced");
722
716
  expect(raw.llm.profiles.balanced.provider).toBe("fireworks");
723
- expect(raw.llm.profiles.balanced.provider_connection).toBe(
724
- "fireworks-managed",
725
- );
717
+ expect(raw.llm.profiles.balanced.provider_connection).toBe("vellum");
726
718
  // The old custom-balanced is preserved on disk but no longer active.
727
719
  expect(raw.llm.profiles["custom-balanced"].provider).toBe("openai");
728
720
  });
@@ -848,9 +840,7 @@ describe("loadConfig startup behavior", () => {
848
840
  // Managed profiles are also seeded. Balanced now serves GLM 5.2 on
849
841
  // Fireworks (the model Quality used to carry).
850
842
  expect(raw.llm.profiles.balanced.provider).toBe("fireworks");
851
- expect(raw.llm.profiles.balanced.provider_connection).toBe(
852
- "fireworks-managed",
853
- );
843
+ expect(raw.llm.profiles.balanced.provider_connection).toBe("vellum");
854
844
  expect(raw.llm.profiles.balanced.model).toBe(
855
845
  "accounts/fireworks/models/glm-5p2",
856
846
  );
@@ -890,9 +880,7 @@ describe("loadConfig startup behavior", () => {
890
880
  expect(raw.llm.profiles.balanced.model).toBe(
891
881
  "accounts/fireworks/models/glm-5p2",
892
882
  );
893
- expect(raw.llm.profiles.balanced.provider_connection).toBe(
894
- "fireworks-managed",
895
- );
883
+ expect(raw.llm.profiles.balanced.provider_connection).toBe("vellum");
896
884
  expect(raw.llm.activeProfile).toBe("balanced");
897
885
  });
898
886
 
@@ -961,9 +949,7 @@ describe("loadConfig startup behavior", () => {
961
949
  // The template carries no topP, and the previous entry had none, so the
962
950
  // reconciled profile has no topP override.
963
951
  expect("topP" in raw.llm.profiles.balanced).toBe(false);
964
- expect(raw.llm.profiles.balanced.provider_connection).toBe(
965
- "fireworks-managed",
966
- );
952
+ expect(raw.llm.profiles.balanced.provider_connection).toBe("vellum");
967
953
  expect(raw.llm.activeProfile).toBe("balanced");
968
954
  });
969
955
 
@@ -1234,7 +1220,7 @@ describe("loadConfig startup behavior", () => {
1234
1220
  expect(afterRestart.llm.activeProfile).toBe("balanced");
1235
1221
  expect(afterRestart.llm.profiles.balanced.provider).toBe("fireworks");
1236
1222
  expect(afterRestart.llm.profiles.balanced.provider_connection).toBe(
1237
- "fireworks-managed",
1223
+ "vellum",
1238
1224
  );
1239
1225
  expect(afterRestart.llm.profiles.balanced.model).toBe(
1240
1226
  "accounts/fireworks/models/glm-5p2",
@@ -1574,19 +1560,13 @@ describe("seedInferenceProfiles BYOK-mode managed profile labels", () => {
1574
1560
  const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
1575
1561
 
1576
1562
  expect(raw.llm.activeProfile).toBe("balanced");
1577
- // Balanced lives on `fireworks-managed`; `quality-optimized` (Fable on
1578
- // `anthropic-managed`) is on a different connection, so selecting balanced at
1579
- // hatch disables it. The default advisor falls to the only active managed
1580
- // profile, `balanced`.
1581
- expect(raw.llm.advisorProfile).toBe("balanced");
1582
- expect(raw.llm.profiles.balanced.provider_connection).toBe(
1583
- "fireworks-managed",
1584
- );
1563
+ // All managed profiles now share the single `vellum` connection, so
1564
+ // selecting `balanced` at hatch activates that connection for every managed
1565
+ // profile — none are disabled. The default advisor falls to the strongest
1566
+ // active managed profile, `quality-optimized`.
1567
+ expect(raw.llm.advisorProfile).toBe("quality-optimized");
1568
+ expect(raw.llm.profiles.balanced.provider_connection).toBe("vellum");
1585
1569
  expect("status" in raw.llm.profiles.balanced).toBe(false);
1586
- // Connections exist (status is no longer a connection-level concept).
1587
- expect(getConnection(db, "anthropic-managed")).not.toBeNull();
1588
- expect(getConnection(db, "openai-managed")).not.toBeNull();
1589
- expect(getConnection(db, "gemini-managed")).not.toBeNull();
1590
1570
  });
1591
1571
 
1592
1572
  test("off-platform managed-inference hatch respects explicit non-managed active connection", () => {
@@ -1883,11 +1863,11 @@ describe("OS Beta managed profile template", () => {
1883
1863
  const entry = materializeProfile(
1884
1864
  OS_BETA_PROFILE_TEMPLATE,
1885
1865
  "together",
1886
- "together-managed",
1866
+ "vellum",
1887
1867
  );
1888
1868
 
1889
1869
  expect(entry.model).toBe("MiniMaxAI/MiniMax-M3");
1890
- expect(entry.provider_connection).toBe("together-managed");
1870
+ expect(entry.provider_connection).toBe("vellum");
1891
1871
  expect(entry.provider).toBe("together");
1892
1872
  expect(entry.label).toBe("OS Beta");
1893
1873
  expect(entry.source).toBe("managed");
@@ -1,14 +1,21 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import { reinjectImageSourcePaths } from "../daemon/conversation-lifecycle.js";
3
+ import { enrichMessageWithSourcePaths } from "../agent/attachments.js";
4
+ import { createUserMessage } from "../agent/message-types.js";
5
+ import { reinjectAttachmentPathAnnotations } from "../daemon/conversation-lifecycle.js";
6
+ import {
7
+ extractAttachmentStoredPaths,
8
+ extractImageSourcePaths,
9
+ } from "../persistence/conversation-crud.js";
4
10
  import type { ContentBlock } from "../providers/types.js";
5
11
 
6
12
  // ---------------------------------------------------------------------------
7
- // reinjectImageSourcePaths — re-inject [Attached image source: /path]
8
- // annotations when loading conversation history from DB
13
+ // reinjectAttachmentPathAnnotations — re-inject attachment path annotations
14
+ // (image source paths + resolved stored paths) when loading conversation
15
+ // history from DB
9
16
  // ---------------------------------------------------------------------------
10
17
 
11
- describe("reinjectImageSourcePaths", () => {
18
+ describe("reinjectAttachmentPathAnnotations", () => {
12
19
  const baseContent: ContentBlock[] = [
13
20
  { type: "text", text: "what is this?" },
14
21
  {
@@ -21,7 +28,11 @@ describe("reinjectImageSourcePaths", () => {
21
28
  const metadata = JSON.stringify({
22
29
  imageSourcePaths: { "photo.jpg": "/Users/me/Desktop/photo.jpg" },
23
30
  });
24
- const result = reinjectImageSourcePaths(baseContent, "user", metadata);
31
+ const result = reinjectAttachmentPathAnnotations(
32
+ baseContent,
33
+ "user",
34
+ metadata,
35
+ );
25
36
 
26
37
  expect(result).toHaveLength(3);
27
38
  const annotation = result[2] as { type: "text"; text: string };
@@ -35,7 +46,11 @@ describe("reinjectImageSourcePaths", () => {
35
46
  const metadata = JSON.stringify({
36
47
  imageSourcePaths: { "photo.jpg": "/Users/me/Desktop/photo.jpg" },
37
48
  });
38
- const result = reinjectImageSourcePaths(baseContent, "assistant", metadata);
49
+ const result = reinjectAttachmentPathAnnotations(
50
+ baseContent,
51
+ "assistant",
52
+ metadata,
53
+ );
39
54
 
40
55
  // Should return the original content unchanged
41
56
  expect(result).toBe(baseContent);
@@ -43,7 +58,7 @@ describe("reinjectImageSourcePaths", () => {
43
58
  });
44
59
 
45
60
  test("returns content unchanged when metadata is null", () => {
46
- const result = reinjectImageSourcePaths(baseContent, "user", null);
61
+ const result = reinjectAttachmentPathAnnotations(baseContent, "user", null);
47
62
  expect(result).toBe(baseContent);
48
63
  expect(result).toHaveLength(2);
49
64
  });
@@ -52,7 +67,11 @@ describe("reinjectImageSourcePaths", () => {
52
67
  const metadata = JSON.stringify({
53
68
  userMessageChannel: "desktop",
54
69
  });
55
- const result = reinjectImageSourcePaths(baseContent, "user", metadata);
70
+ const result = reinjectAttachmentPathAnnotations(
71
+ baseContent,
72
+ "user",
73
+ metadata,
74
+ );
56
75
  expect(result).toBe(baseContent);
57
76
  expect(result).toHaveLength(2);
58
77
  });
@@ -61,7 +80,11 @@ describe("reinjectImageSourcePaths", () => {
61
80
  const metadata = JSON.stringify({
62
81
  imageSourcePaths: {},
63
82
  });
64
- const result = reinjectImageSourcePaths(baseContent, "user", metadata);
83
+ const result = reinjectAttachmentPathAnnotations(
84
+ baseContent,
85
+ "user",
86
+ metadata,
87
+ );
65
88
  expect(result).toBe(baseContent);
66
89
  expect(result).toHaveLength(2);
67
90
  });
@@ -73,7 +96,11 @@ describe("reinjectImageSourcePaths", () => {
73
96
  "b.png": "/path/to/b.png",
74
97
  },
75
98
  });
76
- const result = reinjectImageSourcePaths(baseContent, "user", metadata);
99
+ const result = reinjectAttachmentPathAnnotations(
100
+ baseContent,
101
+ "user",
102
+ metadata,
103
+ );
77
104
 
78
105
  expect(result).toHaveLength(3);
79
106
  const annotation = result[2] as { type: "text"; text: string };
@@ -84,7 +111,7 @@ describe("reinjectImageSourcePaths", () => {
84
111
  });
85
112
 
86
113
  test("gracefully handles malformed metadata JSON", () => {
87
- const result = reinjectImageSourcePaths(
114
+ const result = reinjectAttachmentPathAnnotations(
88
115
  baseContent,
89
116
  "user",
90
117
  "not-valid-json{{{",
@@ -102,7 +129,11 @@ describe("reinjectImageSourcePaths", () => {
102
129
  "also_bad.jpg": null,
103
130
  },
104
131
  });
105
- const result = reinjectImageSourcePaths(baseContent, "user", metadata);
132
+ const result = reinjectAttachmentPathAnnotations(
133
+ baseContent,
134
+ "user",
135
+ metadata,
136
+ );
106
137
 
107
138
  expect(result).toHaveLength(3);
108
139
  const annotation = result[2] as { type: "text"; text: string };
@@ -118,7 +149,11 @@ describe("reinjectImageSourcePaths", () => {
118
149
  "also_bad.jpg": null,
119
150
  },
120
151
  });
121
- const result = reinjectImageSourcePaths(baseContent, "user", metadata);
152
+ const result = reinjectAttachmentPathAnnotations(
153
+ baseContent,
154
+ "user",
155
+ metadata,
156
+ );
122
157
  expect(result).toBe(baseContent);
123
158
  expect(result).toHaveLength(2);
124
159
  });
@@ -127,10 +162,116 @@ describe("reinjectImageSourcePaths", () => {
127
162
  const metadata = JSON.stringify({
128
163
  imageSourcePaths: { "photo.jpg": "/path/photo.jpg" },
129
164
  });
130
- const result = reinjectImageSourcePaths(baseContent, "user", metadata);
165
+ const result = reinjectAttachmentPathAnnotations(
166
+ baseContent,
167
+ "user",
168
+ metadata,
169
+ );
131
170
 
132
171
  // First two blocks should be identical to the originals
133
172
  expect(result[0]).toEqual(baseContent[0]);
134
173
  expect(result[1]).toEqual(baseContent[1]);
135
174
  });
175
+
176
+ test("adds stored path annotations from attachmentStoredPaths", () => {
177
+ const metadata = JSON.stringify({
178
+ attachmentStoredPaths: {
179
+ "0:report.pdf": "/conv/attachments/report-2.pdf",
180
+ },
181
+ });
182
+ const result = reinjectAttachmentPathAnnotations(
183
+ baseContent,
184
+ "user",
185
+ metadata,
186
+ );
187
+
188
+ expect(result).toHaveLength(3);
189
+ const annotation = result[2] as { type: "text"; text: string };
190
+ expect(annotation.text).toBe(
191
+ '[Attachment "report.pdf" is stored at: /conv/attachments/report-2.pdf]',
192
+ );
193
+ });
194
+
195
+ test("recovers filenames containing colons from stored path keys", () => {
196
+ const metadata = JSON.stringify({
197
+ attachmentStoredPaths: {
198
+ "0:notes: draft.txt": "/conv/attachments/notes: draft.txt",
199
+ },
200
+ });
201
+ const result = reinjectAttachmentPathAnnotations(
202
+ baseContent,
203
+ "user",
204
+ metadata,
205
+ );
206
+
207
+ const annotation = result[2] as { type: "text"; text: string };
208
+ expect(annotation.text).toBe(
209
+ '[Attachment "notes: draft.txt" is stored at: /conv/attachments/notes: draft.txt]',
210
+ );
211
+ });
212
+
213
+ test("emits image source lines before stored path lines in one block", () => {
214
+ const metadata = JSON.stringify({
215
+ imageSourcePaths: { "1:photo.jpg": "/Users/me/Desktop/photo.jpg" },
216
+ attachmentStoredPaths: {
217
+ "0:data.csv": "/conv/attachments/data-2.csv",
218
+ "1:photo.jpg": "/conv/attachments/photo.jpg",
219
+ },
220
+ });
221
+ const result = reinjectAttachmentPathAnnotations(
222
+ baseContent,
223
+ "user",
224
+ metadata,
225
+ );
226
+
227
+ expect(result).toHaveLength(3);
228
+ const annotation = result[2] as { type: "text"; text: string };
229
+ expect(annotation.text).toBe(
230
+ "[Attached image source: /Users/me/Desktop/photo.jpg]\n" +
231
+ '[Attachment "data.csv" is stored at: /conv/attachments/data-2.csv]\n' +
232
+ '[Attachment "photo.jpg" is stored at: /conv/attachments/photo.jpg]',
233
+ );
234
+ });
235
+
236
+ test("rebuilds the exact annotation block enrichMessageWithSourcePaths appends", () => {
237
+ // Prefix-cache parity tripwire: the block appended at persist time (from
238
+ // live attachment inputs) and the block rebuilt on history reload (from
239
+ // persisted metadata) must be byte-identical.
240
+ const attachments = [
241
+ {
242
+ filename: "data.csv",
243
+ mimeType: "text/csv",
244
+ data: "csvdata",
245
+ storedPath: "/conv/attachments/data-2.csv",
246
+ },
247
+ {
248
+ filename: "photo.jpg",
249
+ mimeType: "image/jpeg",
250
+ data: "img",
251
+ filePath: "/Users/me/Desktop/photo.jpg",
252
+ storedPath: "/conv/attachments/photo.jpg",
253
+ },
254
+ ];
255
+ const enriched = enrichMessageWithSourcePaths(
256
+ createUserMessage("compare", attachments),
257
+ attachments,
258
+ );
259
+ const liveBlock = enriched.content.at(-1) as {
260
+ type: "text";
261
+ text: string;
262
+ };
263
+
264
+ const metadata = JSON.stringify({
265
+ imageSourcePaths: extractImageSourcePaths(attachments),
266
+ attachmentStoredPaths: extractAttachmentStoredPaths(attachments),
267
+ });
268
+ const rebuilt = reinjectAttachmentPathAnnotations(
269
+ [{ type: "text", text: "compare" }],
270
+ "user",
271
+ metadata,
272
+ );
273
+ const rebuiltBlock = rebuilt.at(-1) as { type: "text"; text: string };
274
+
275
+ expect(rebuiltBlock.text).toBe(liveBlock.text);
276
+ });
136
277
  });
@@ -120,6 +120,8 @@ mock.module("../persistence/embeddings/embedding-backend.js", () => ({
120
120
  mock.module("../providers/inference/connections.js", () => ({
121
121
  listConnections: () => [],
122
122
  createConnection: () => ({ ok: false, error: { code: "already_exists" } }),
123
+ getConnection: () => null,
124
+ VELLUM_MANAGED_CONNECTION_NAME: "vellum",
123
125
  PROVIDERS_REQUIRING_BASE_URL_AND_MODELS: new Set(["openai-compatible"]),
124
126
  }));
125
127