@vellumai/assistant 0.10.1-staging.2 → 0.10.1-staging.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.1-staging.2",
3
+ "version": "0.10.1-staging.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -3191,3 +3191,70 @@ describe("AnthropicProvider — thinking block send-time filtering", () => {
3191
3191
  expect(signatures).toContain("sig-step2");
3192
3192
  });
3193
3193
  });
3194
+
3195
+ describe("AnthropicProvider — deprecated sampling params (temperature / top_p / top_k)", () => {
3196
+ beforeEach(() => {
3197
+ lastStreamParams = null;
3198
+ });
3199
+
3200
+ // opus-4-7 / opus-4-8 (and, conservatively, fable) reject `temperature`,
3201
+ // `top_p`, and `top_k` with a 400; the provider must strip all three.
3202
+ for (const model of [
3203
+ "claude-opus-4-8",
3204
+ "claude-opus-4-7",
3205
+ "claude-fable-5",
3206
+ ]) {
3207
+ test(`strips temperature, top_p, and top_k for ${model}`, async () => {
3208
+ const provider = new AnthropicProvider("sk-ant-test", model);
3209
+ await provider.sendMessage([userMsg("Hi")], {
3210
+ systemPrompt: "You are helpful.",
3211
+ config: { temperature: 0, top_p: 0.95, top_k: 40 },
3212
+ });
3213
+ expect(lastStreamParams!).not.toHaveProperty("temperature");
3214
+ expect(lastStreamParams!).not.toHaveProperty("top_p");
3215
+ expect(lastStreamParams!).not.toHaveProperty("top_k");
3216
+ });
3217
+ }
3218
+
3219
+ // opus-4-6 / sonnet-4-6 still accept the params — they must pass through,
3220
+ // including `temperature: 0` (a value check, not truthiness).
3221
+ test("forwards temperature (including 0), top_p, and top_k for opus-4-6", async () => {
3222
+ const provider = new AnthropicProvider("sk-ant-test", "claude-opus-4-6");
3223
+ await provider.sendMessage([userMsg("Hi")], {
3224
+ systemPrompt: "You are helpful.",
3225
+ config: { temperature: 0, top_p: 0.95, top_k: 40 },
3226
+ });
3227
+ expect(lastStreamParams!.temperature).toBe(0);
3228
+ expect(lastStreamParams!.top_p).toBe(0.95);
3229
+ expect(lastStreamParams!.top_k).toBe(40);
3230
+ });
3231
+
3232
+ test("forwards temperature, top_p, and top_k for sonnet-4-6", async () => {
3233
+ const provider = new AnthropicProvider("sk-ant-test", "claude-sonnet-4-6");
3234
+ await provider.sendMessage([userMsg("Hi")], {
3235
+ systemPrompt: "You are helpful.",
3236
+ config: { temperature: 0.7, top_p: 0.9, top_k: 20 },
3237
+ });
3238
+ expect(lastStreamParams!.temperature).toBe(0.7);
3239
+ expect(lastStreamParams!.top_p).toBe(0.9);
3240
+ expect(lastStreamParams!.top_k).toBe(20);
3241
+ });
3242
+
3243
+ // A per-call model override targeting a deprecating model must win over the
3244
+ // provider's default (accepting) model.
3245
+ test("strips params when a per-call model override deprecates them", async () => {
3246
+ const provider = new AnthropicProvider("sk-ant-test", "claude-sonnet-4-6");
3247
+ await provider.sendMessage([userMsg("Hi")], {
3248
+ systemPrompt: "You are helpful.",
3249
+ config: {
3250
+ temperature: 0,
3251
+ top_p: 0.95,
3252
+ top_k: 40,
3253
+ model: "claude-opus-4-8",
3254
+ },
3255
+ });
3256
+ expect(lastStreamParams!).not.toHaveProperty("temperature");
3257
+ expect(lastStreamParams!).not.toHaveProperty("top_p");
3258
+ expect(lastStreamParams!).not.toHaveProperty("top_k");
3259
+ });
3260
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { CardSurfaceDataSchema } from "../api/surfaces.js";
4
+
5
+ // The wire keeps surface `data` opaque and the stream drops events that fail to
6
+ // parse, so the canonical card schema must never reject a real payload: every
7
+ // field is optional and unknown keys are stripped. The daemon's `ui_show`
8
+ // normalizer parses against this schema and logs the stripped keys — that is
9
+ // how unsupported shapes are surfaced rather than silently swallowed.
10
+ describe("CardSurfaceDataSchema", () => {
11
+ test("parses an empty object", () => {
12
+ expect(CardSurfaceDataSchema.safeParse({}).success).toBe(true);
13
+ });
14
+
15
+ test("parses a title-less card and strips unknown keys", () => {
16
+ const parsed = CardSurfaceDataSchema.safeParse({
17
+ body: "hi",
18
+ surfaceWidgetHint: "ignored",
19
+ });
20
+ expect(parsed.success).toBe(true);
21
+ if (parsed.success) {
22
+ expect(parsed.data).toEqual({ body: "hi" });
23
+ }
24
+ });
25
+
26
+ test("a body-less, title-only card is still valid (renders its title)", () => {
27
+ expect(CardSurfaceDataSchema.safeParse({ title: "Heads up" }).success).toBe(
28
+ true,
29
+ );
30
+ });
31
+
32
+ test("coerces primitive metadata values to strings", () => {
33
+ const parsed = CardSurfaceDataSchema.safeParse({
34
+ metadata: [
35
+ { label: "Docs", value: 12 },
36
+ { label: "Passed", value: true },
37
+ { label: "Status", value: "OK" },
38
+ ],
39
+ });
40
+ expect(parsed.success).toBe(true);
41
+ if (parsed.success) {
42
+ expect(parsed.data.metadata).toEqual([
43
+ { label: "Docs", value: "12" },
44
+ { label: "Passed", value: "true" },
45
+ { label: "Status", value: "OK" },
46
+ ]);
47
+ }
48
+ });
49
+
50
+ test("the schema's keys define what the normalizer supports", () => {
51
+ expect(Object.keys(CardSurfaceDataSchema.shape).sort()).toEqual([
52
+ "body",
53
+ "metadata",
54
+ "subtitle",
55
+ "template",
56
+ "templateData",
57
+ "title",
58
+ ]);
59
+ });
60
+ });
@@ -184,7 +184,7 @@ describe("activation moment emission from ui_show surface commits", () => {
184
184
  await surfaceProxyResolver(ctx, "ui_show", {
185
185
  surface_type: "card",
186
186
  title: "Inbox cleaned",
187
- data: { text: "Archived 1,240 emails" },
187
+ data: { body: "Archived 1,240 emails" },
188
188
  activation_moment: "first_wow_executed",
189
189
  });
190
190
 
@@ -210,7 +210,7 @@ describe("activation moment emission from ui_show surface commits", () => {
210
210
  await surfaceProxyResolver(ctx, "ui_show", {
211
211
  surface_type: "card",
212
212
  title: "Result",
213
- data: { text: "x" },
213
+ data: { body: "x" },
214
214
  activation_moment: "first_wow_executed",
215
215
  });
216
216
  expect(queryUnreportedOnboardingEvents(0, undefined, 10)).toHaveLength(0);
@@ -283,7 +283,7 @@ describe("activation moment emission from ui_show surface commits", () => {
283
283
  await surfaceProxyResolver(ctx, "ui_show", {
284
284
  surface_type: "card",
285
285
  title: "Start something",
286
- data: { text: "Kick off a draft" },
286
+ data: { body: "Kick off a draft" },
287
287
  actions: [{ id: "go", label: "Go", style: "primary" }],
288
288
  activation_moment: "moment_1",
289
289
  });
@@ -576,3 +576,355 @@ describe("task_progress surface compatibility", () => {
576
576
  expect(sent.some((m) => m.type === "ui_surface_show")).toBe(true);
577
577
  });
578
578
  });
579
+
580
+ describe("ui_show card content recovery", () => {
581
+ function shownCard(sent: ServerMessage[]): CardSurfaceData | undefined {
582
+ const show = sent.find(
583
+ (m): m is UiSurfaceShow => m.type === "ui_surface_show",
584
+ );
585
+ if (!show || show.surfaceType !== "card") return undefined;
586
+ return show.data;
587
+ }
588
+
589
+ test("recovers body from a copy_block-style `text` field", async () => {
590
+ const sent: ServerMessage[] = [];
591
+ const ctx = makeContext(sent);
592
+
593
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
594
+ surface_type: "card",
595
+ title: "Inbox cleaned",
596
+ data: { text: "Archived 1,240 emails" },
597
+ });
598
+
599
+ expect(result.isError).toBe(false);
600
+ const card = shownCard(sent);
601
+ expect(card?.body).toBe("Archived 1,240 emails");
602
+ // The alias key is not a card field; it must not survive on the surface.
603
+ expect((card as Record<string, unknown>).text).toBeUndefined();
604
+ });
605
+
606
+ test("recovers body from a confirmation-style `message` field", async () => {
607
+ const sent: ServerMessage[] = [];
608
+ const ctx = makeContext(sent);
609
+
610
+ await surfaceProxyResolver(ctx, "ui_show", {
611
+ surface_type: "card",
612
+ data: { title: "Heads up", message: "The server will restart." },
613
+ });
614
+
615
+ expect(shownCard(sent)?.body).toBe("The server will restart.");
616
+ });
617
+
618
+ test("recovers top-level subtitle and metadata into the card", async () => {
619
+ const sent: ServerMessage[] = [];
620
+ const ctx = makeContext(sent);
621
+
622
+ await surfaceProxyResolver(ctx, "ui_show", {
623
+ surface_type: "card",
624
+ subtitle: "saved just now",
625
+ metadata: [{ label: "Total", value: "$10" }],
626
+ data: { body: "Done" },
627
+ });
628
+
629
+ const card = shownCard(sent);
630
+ expect(card?.subtitle).toBe("saved just now");
631
+ expect(card?.metadata).toEqual([{ label: "Total", value: "$10" }]);
632
+ });
633
+
634
+ test("title-only card with actions renders with actions intact", async () => {
635
+ const sent: ServerMessage[] = [];
636
+ const ctx = makeContext(sent);
637
+
638
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
639
+ surface_type: "card",
640
+ title: "Restart the server?",
641
+ actions: [{ id: "yes", label: "Yes" }],
642
+ data: {},
643
+ });
644
+
645
+ expect(result.isError).toBe(false);
646
+ expect(shownCard(sent)?.title).toBe("Restart the server?");
647
+ const show = sent.find(
648
+ (m): m is UiSurfaceShow => m.type === "ui_surface_show",
649
+ )!;
650
+ expect(show.actions).toBeDefined();
651
+ expect(show.actions!.length).toBe(1);
652
+ expect(show.actions![0].label).toBe("Yes");
653
+ });
654
+
655
+ test("title-only card without actions renders without error", async () => {
656
+ const sent: ServerMessage[] = [];
657
+ const ctx = makeContext(sent);
658
+
659
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
660
+ surface_type: "card",
661
+ title: "Status update",
662
+ data: {},
663
+ });
664
+
665
+ expect(result.isError).toBe(false);
666
+ expect(shownCard(sent)?.title).toBe("Status update");
667
+ });
668
+
669
+ test("card with body and actions is interactive", async () => {
670
+ const sent: ServerMessage[] = [];
671
+ const ctx = makeContext(sent);
672
+
673
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
674
+ surface_type: "card",
675
+ title: "Confirm",
676
+ actions: [{ id: "ok", label: "OK" }],
677
+ data: { body: "Are you sure?" },
678
+ });
679
+
680
+ expect(result.isError).toBe(false);
681
+ const show = sent.find(
682
+ (m): m is UiSurfaceShow => m.type === "ui_surface_show",
683
+ )!;
684
+ expect(show.actions).toBeDefined();
685
+ expect(show.actions!.length).toBe(1);
686
+ });
687
+
688
+ // ── Body alias recovery from cross-surface keys ────────────────────
689
+
690
+ test("recovers body from choice/form-style `description` field", async () => {
691
+ const sent: ServerMessage[] = [];
692
+ const ctx = makeContext(sent);
693
+
694
+ await surfaceProxyResolver(ctx, "ui_show", {
695
+ surface_type: "card",
696
+ title: "Search results",
697
+ data: { description: "Found 12 matching documents." },
698
+ });
699
+
700
+ const card = shownCard(sent);
701
+ expect(card?.body).toBe("Found 12 matching documents.");
702
+ expect((card as Record<string, unknown>).description).toBeUndefined();
703
+ });
704
+
705
+ test("recovers body from work_result-style `summary` field", async () => {
706
+ const sent: ServerMessage[] = [];
707
+ const ctx = makeContext(sent);
708
+
709
+ await surfaceProxyResolver(ctx, "ui_show", {
710
+ surface_type: "card",
711
+ data: { title: "Report", summary: "All tests passed." },
712
+ });
713
+
714
+ expect(shownCard(sent)?.body).toBe("All tests passed.");
715
+ });
716
+
717
+ test("recovers body from confirmation-style `detail` field", async () => {
718
+ const sent: ServerMessage[] = [];
719
+ const ctx = makeContext(sent);
720
+
721
+ await surfaceProxyResolver(ctx, "ui_show", {
722
+ surface_type: "card",
723
+ data: { title: "Warning", detail: "This action cannot be undone." },
724
+ });
725
+
726
+ expect(shownCard(sent)?.body).toBe("This action cannot be undone.");
727
+ });
728
+
729
+ test("recovers body from top-level `description`", async () => {
730
+ const sent: ServerMessage[] = [];
731
+ const ctx = makeContext(sent);
732
+
733
+ await surfaceProxyResolver(ctx, "ui_show", {
734
+ surface_type: "card",
735
+ title: "Info",
736
+ description: "Top-level description text",
737
+ data: {},
738
+ });
739
+
740
+ expect(shownCard(sent)?.body).toBe("Top-level description text");
741
+ });
742
+
743
+ test("concatenates multiple body aliases when they co-occur", async () => {
744
+ const sent: ServerMessage[] = [];
745
+ const ctx = makeContext(sent);
746
+
747
+ await surfaceProxyResolver(ctx, "ui_show", {
748
+ surface_type: "card",
749
+ title: "Multi-alias",
750
+ data: {
751
+ description: "Found 12 documents.",
752
+ summary: "Search complete.",
753
+ detail: "Checked 3 sources.",
754
+ },
755
+ });
756
+
757
+ const body = shownCard(sent)?.body;
758
+ expect(body).toContain("Found 12 documents.");
759
+ expect(body).toContain("Search complete.");
760
+ expect(body).toContain("Checked 3 sources.");
761
+ });
762
+
763
+ // ── Title alias recovery ────────────────────────────────────────────
764
+
765
+ test("recovers title from `heading` alias", async () => {
766
+ const sent: ServerMessage[] = [];
767
+ const ctx = makeContext(sent);
768
+
769
+ await surfaceProxyResolver(ctx, "ui_show", {
770
+ surface_type: "card",
771
+ data: { heading: "Results", body: "Done." },
772
+ });
773
+
774
+ expect(shownCard(sent)?.title).toBe("Results");
775
+ });
776
+
777
+ test("recovers title from `header` alias", async () => {
778
+ const sent: ServerMessage[] = [];
779
+ const ctx = makeContext(sent);
780
+
781
+ await surfaceProxyResolver(ctx, "ui_show", {
782
+ surface_type: "card",
783
+ data: { header: "Status Update", body: "All good." },
784
+ });
785
+
786
+ expect(shownCard(sent)?.title).toBe("Status Update");
787
+ });
788
+
789
+ // ── Subtitle alias recovery ─────────────────────────────────────────
790
+
791
+ test("recovers subtitle from `subheading` alias", async () => {
792
+ const sent: ServerMessage[] = [];
793
+ const ctx = makeContext(sent);
794
+
795
+ await surfaceProxyResolver(ctx, "ui_show", {
796
+ surface_type: "card",
797
+ data: { title: "Alert", body: "Check this.", subheading: "Important" },
798
+ });
799
+
800
+ expect(shownCard(sent)?.subtitle).toBe("Important");
801
+ });
802
+
803
+ test("recovers subtitle from table-style `caption` alias", async () => {
804
+ const sent: ServerMessage[] = [];
805
+ const ctx = makeContext(sent);
806
+
807
+ await surfaceProxyResolver(ctx, "ui_show", {
808
+ surface_type: "card",
809
+ data: { title: "Table Summary", body: "Data below.", caption: "Q4 2024" },
810
+ });
811
+
812
+ expect(shownCard(sent)?.subtitle).toBe("Q4 2024");
813
+ });
814
+
815
+ // ── Alias precedence ───────────────────────────────────────────────
816
+
817
+ test("canonical `body` takes precedence over aliased `description`", async () => {
818
+ const sent: ServerMessage[] = [];
819
+ const ctx = makeContext(sent);
820
+
821
+ await surfaceProxyResolver(ctx, "ui_show", {
822
+ surface_type: "card",
823
+ data: { body: "Real body", description: "Should be ignored" },
824
+ });
825
+
826
+ expect(shownCard(sent)?.body).toBe("Real body");
827
+ });
828
+
829
+ test("card with recovered `description` and actions keeps actions (has content)", async () => {
830
+ const sent: ServerMessage[] = [];
831
+ const ctx = makeContext(sent);
832
+
833
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
834
+ surface_type: "card",
835
+ title: "Confirm",
836
+ actions: [{ id: "ok", label: "OK" }],
837
+ data: { description: "Proceed with deployment?" },
838
+ });
839
+
840
+ expect(result.isError).toBe(false);
841
+ const show = sent.find(
842
+ (m): m is UiSurfaceShow => m.type === "ui_surface_show",
843
+ )!;
844
+ expect(show.actions).toBeDefined();
845
+ expect(shownCard(sent)?.body).toBe("Proceed with deployment?");
846
+ });
847
+
848
+ test("recovers actions nested inside data when top-level actions is absent", async () => {
849
+ const sent: ServerMessage[] = [];
850
+ const ctx = makeContext(sent);
851
+
852
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
853
+ surface_type: "card",
854
+ title: "Confirm deployment",
855
+ data: {
856
+ body: "Deploy to production?",
857
+ actions: [{ id: "yes", label: "Yes" }],
858
+ },
859
+ });
860
+
861
+ expect(result.isError).toBe(false);
862
+ const show = sent.find(
863
+ (m): m is UiSurfaceShow => m.type === "ui_surface_show",
864
+ )!;
865
+ expect(show.actions).toBeDefined();
866
+ expect(show.actions!.length).toBe(1);
867
+ expect(show.actions![0].label).toBe("Yes");
868
+ });
869
+
870
+ test("top-level actions take precedence over data.actions", async () => {
871
+ const sent: ServerMessage[] = [];
872
+ const ctx = makeContext(sent);
873
+
874
+ await surfaceProxyResolver(ctx, "ui_show", {
875
+ surface_type: "card",
876
+ title: "Confirm",
877
+ actions: [{ id: "top", label: "Top-level" }],
878
+ data: {
879
+ body: "Which actions?",
880
+ actions: [{ id: "nested", label: "Nested" }],
881
+ },
882
+ });
883
+
884
+ const show = sent.find(
885
+ (m): m is UiSurfaceShow => m.type === "ui_surface_show",
886
+ )!;
887
+ expect(show.actions!.length).toBe(1);
888
+ expect(show.actions![0].label).toBe("Top-level");
889
+ });
890
+
891
+ test("genuinely empty card (no title, body, subtitle, metadata, template, or actions) is rejected", async () => {
892
+ const sent: ServerMessage[] = [];
893
+ const ctx = makeContext(sent);
894
+
895
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
896
+ surface_type: "card",
897
+ data: {},
898
+ });
899
+
900
+ expect(result.isError).toBe(true);
901
+ expect(result.content).toContain("requires content");
902
+ expect(sent.filter((m) => m.type === "ui_surface_show")).toHaveLength(0);
903
+ });
904
+
905
+ test("a card with a real body broadcasts unchanged", async () => {
906
+ const sent: ServerMessage[] = [];
907
+ const ctx = makeContext(sent);
908
+
909
+ await surfaceProxyResolver(ctx, "ui_show", {
910
+ surface_type: "card",
911
+ data: { title: "Plain", body: "hi" },
912
+ });
913
+
914
+ expect(shownCard(sent)?.body).toBe("hi");
915
+ });
916
+
917
+ test("a task_progress card with empty data broadcasts (template renders a shell)", async () => {
918
+ const sent: ServerMessage[] = [];
919
+ const ctx = makeContext(sent);
920
+
921
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
922
+ surface_type: "card",
923
+ template: "task_progress",
924
+ templateData: { status: "in_progress", steps: [] },
925
+ });
926
+
927
+ expect(result.isError).toBe(false);
928
+ expect(shownCard(sent)?.template).toBe("task_progress");
929
+ });
930
+ });
@@ -257,100 +257,6 @@ describe("ui_show dynamic_page app substitute guard", () => {
257
257
  });
258
258
  });
259
259
 
260
- describe("ui_show empty card guard", () => {
261
- function makeCtx(onProxy: () => void) {
262
- return {
263
- conversationId: "conversation-123",
264
- workingDir: "/tmp",
265
- trustClass: "guardian" as const,
266
- proxyToolResolver: async () => {
267
- onProxy();
268
- return { content: "proxied", isError: false };
269
- },
270
- };
271
- }
272
-
273
- test("rejects a card carrying only a title and does not proxy", async () => {
274
- let proxied = false;
275
- const result = await uiShowTool.execute(
276
- {
277
- surface_type: "card",
278
- title: "Vellum Internal Usage app",
279
- activity: "Showing progress",
280
- data: {},
281
- },
282
- makeCtx(() => {
283
- proxied = true;
284
- }),
285
- );
286
-
287
- expect(result.isError).toBe(true);
288
- expect(result.content).toContain("card requires content");
289
- expect(proxied).toBe(false);
290
- });
291
-
292
- test("rejects a card with no content at all", async () => {
293
- let proxied = false;
294
- const result = await uiShowTool.execute(
295
- { surface_type: "card", data: {} },
296
- makeCtx(() => {
297
- proxied = true;
298
- }),
299
- );
300
-
301
- expect(result.isError).toBe(true);
302
- expect(proxied).toBe(false);
303
- });
304
-
305
- test("allows a card with a body", async () => {
306
- let proxied = false;
307
- const result = await uiShowTool.execute(
308
- { surface_type: "card", data: { title: "Plain", body: "hi" } },
309
- makeCtx(() => {
310
- proxied = true;
311
- }),
312
- );
313
-
314
- expect(result.isError).toBe(false);
315
- expect(proxied).toBe(true);
316
- });
317
-
318
- test("allows a task_progress card with empty data (template renders a shell)", async () => {
319
- let proxied = false;
320
- const result = await uiShowTool.execute(
321
- {
322
- surface_type: "card",
323
- template: "task_progress",
324
- templateData: { status: "in_progress", steps: [] },
325
- },
326
- makeCtx(() => {
327
- proxied = true;
328
- }),
329
- );
330
-
331
- expect(result.isError).toBe(false);
332
- expect(proxied).toBe(true);
333
- });
334
-
335
- test("allows an action-only card", async () => {
336
- let proxied = false;
337
- const result = await uiShowTool.execute(
338
- {
339
- surface_type: "card",
340
- title: "Confirm",
341
- actions: [{ id: "ok", label: "OK" }],
342
- data: {},
343
- },
344
- makeCtx(() => {
345
- proxied = true;
346
- }),
347
- );
348
-
349
- expect(result.isError).toBe(false);
350
- expect(proxied).toBe(true);
351
- });
352
- });
353
-
354
260
  describe("ui_update empty payload guard", () => {
355
261
  function makeCtx(onProxy: () => void) {
356
262
  return {